flagrm 0.4.0 → 0.4.1
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 +17 -0
- package/README.md +4 -1
- package/dist/core/baseline.js +9 -4
- package/dist/core/change-filter.js +1 -1
- package/dist/core/config-snapshot.js +17 -0
- package/dist/core/detect.js +2 -2
- package/dist/core/git.js +11 -1
- package/dist/core/hook.js +13 -2
- package/dist/core/run-cache.js +13 -3
- package/dist/core/scan.js +30 -3
- package/dist/core/scope.js +3 -3
- package/dist/core/test-runner.js +2 -2
- package/dist/core/verify/test-results.js +2 -3
- package/package.json +4 -4
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,22 @@ contain breaking changes.
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.4.1] - 2026-10-12
|
|
11
|
+
|
|
12
|
+
### Security
|
|
13
|
+
|
|
14
|
+
- Replaced `fast-glob` with `tinyglobby`, which drops the vulnerable `braces`
|
|
15
|
+
dependency (GHSA-vfj7-8cjw-p6xm) from installs.
|
|
16
|
+
- The Stop hook no longer lets a removal finish unverified when the config's
|
|
17
|
+
`exclude` (or anything else it scans) changed since the baseline: widening
|
|
18
|
+
`exclude` used to hide the removal's edits from the hook.
|
|
19
|
+
- A `git.sha` in `baseline.json` that isn't a commit id is refused, and no
|
|
20
|
+
revision starting with `-` reaches git, where `--output=<file>` would have
|
|
21
|
+
written a file.
|
|
22
|
+
- SECURITY.md says what flagrm guarantees and what the Stop hook doesn't:
|
|
23
|
+
it guards against finishing a removal by accident, not an agent working
|
|
24
|
+
around it.
|
|
25
|
+
|
|
10
26
|
## [0.4.0] - 2026-10-11
|
|
11
27
|
|
|
12
28
|
### Added
|
|
@@ -271,6 +287,7 @@ removes the flag through the `flagrm-remove` skill, and flagrm checks the result
|
|
|
271
287
|
`doctor` warns about an incremental `dotnet build`, which hides existing
|
|
272
288
|
warnings from the dead-code comparison.
|
|
273
289
|
|
|
290
|
+
[0.4.1]: https://github.com/dino-cosic/flagrm/releases/tag/v0.4.1
|
|
274
291
|
[0.4.0]: https://github.com/dino-cosic/flagrm/releases/tag/v0.4.0
|
|
275
292
|
[0.3.0]: https://github.com/dino-cosic/flagrm/releases/tag/v0.3.0
|
|
276
293
|
[0.2.1]: https://github.com/dino-cosic/flagrm/releases/tag/v0.2.1
|
package/README.md
CHANGED
|
@@ -104,7 +104,10 @@ of these reasons is a warning, for the agent to explain in its notes.
|
|
|
104
104
|
removal is in progress and not verified. In progress means a file git
|
|
105
105
|
tracks changed on top of the baseline commit (untracked files and
|
|
106
106
|
flagrm's own setup files don't count, so a removal paused on a question to
|
|
107
|
-
the user doesn't block),
|
|
107
|
+
the user doesn't block), the config changed in what it scans or ignores
|
|
108
|
+
(such as `exclude`), or the last verify used `--skip`. It guards against
|
|
109
|
+
finishing by accident, not an agent working around it: see
|
|
110
|
+
[SECURITY.md](SECURITY.md#what-it-doesnt-guarantee).
|
|
108
111
|
4. **`/flagrm-verify <flag>`** checks a removal later: runs
|
|
109
112
|
`flagrm verify <flag> --md` and adds notes and a commit message.
|
|
110
113
|
|
package/dist/core/baseline.js
CHANGED
|
@@ -2,12 +2,12 @@ import { spawn, spawnSync } from "node:child_process";
|
|
|
2
2
|
import { createHash } from "node:crypto";
|
|
3
3
|
import fs from "node:fs";
|
|
4
4
|
import path from "node:path";
|
|
5
|
-
import fg from "fast-glob";
|
|
6
5
|
import { changeFilter } from "./change-filter.js";
|
|
7
6
|
import { configSnapshot, projectCommands } from "./config-snapshot.js";
|
|
8
7
|
import { environmentProblem } from "./environment.js";
|
|
9
8
|
import { changedFilesSince } from "./git.js";
|
|
10
9
|
import { buildInventory } from "./inventory.js";
|
|
10
|
+
import { globAbsolute } from "./scan.js";
|
|
11
11
|
import { withScopeFiles } from "./scope-files.js";
|
|
12
12
|
import { JSON_SCHEMA_VERSION, READABLE_BASELINE_VERSIONS, } from "./types.js";
|
|
13
13
|
import { readJson, relativePath } from "./util.js";
|
|
@@ -80,8 +80,15 @@ export function readBaseline(root, flag) {
|
|
|
80
80
|
throw new UsageError(`${file} was written by schema version ${baseline.schemaVersion}; this flagrm expects ${JSON_SCHEMA_VERSION}. ` +
|
|
81
81
|
`Re-run \`flagrm baseline ${flag} --force\` on the code as it was before the removal.`);
|
|
82
82
|
}
|
|
83
|
+
const sha = baseline.git?.sha;
|
|
84
|
+
if (sha != null && !COMMIT_ID.test(sha)) {
|
|
85
|
+
throw new UsageError(`${file} has git.sha "${sha}", which is not a git commit id. ` +
|
|
86
|
+
`Re-run \`flagrm baseline ${flag} --force\` on the code as it was before the removal.`);
|
|
87
|
+
}
|
|
83
88
|
return { baseline, file };
|
|
84
89
|
}
|
|
90
|
+
/** A full SHA-1 or SHA-256 commit id, as `git rev-parse HEAD` prints it. */
|
|
91
|
+
const COMMIT_ID = /^(?:[0-9a-f]{40}|[0-9a-f]{64})$/;
|
|
85
92
|
/**
|
|
86
93
|
* Run one shell command inside a project, streaming its output to `logFile`
|
|
87
94
|
* (no buffer limit, stdout and stderr interleaved as printed). A command that
|
|
@@ -312,9 +319,7 @@ export function editsSince(root, sha, testResults = [], ignore = changeFilter())
|
|
|
312
319
|
}
|
|
313
320
|
/** The files (relative to `root`) matching absolute paths or globs, such as `testResults` patterns. */
|
|
314
321
|
export function matchingFiles(root, patterns) {
|
|
315
|
-
return new Set(patterns.flatMap((pattern) =>
|
|
316
|
-
.sync(path.sep === "\\" ? pattern.replace(/\\/g, "/") : pattern, { absolute: true, dot: true })
|
|
317
|
-
.map((file) => relativePath(root, file))));
|
|
322
|
+
return new Set(patterns.flatMap((pattern) => globAbsolute(pattern).map((file) => relativePath(root, file))));
|
|
318
323
|
}
|
|
319
324
|
/**
|
|
320
325
|
* `baseline --force` starts over only before the removal began: the working
|
|
@@ -42,7 +42,7 @@ export function changeFilter(config) {
|
|
|
42
42
|
};
|
|
43
43
|
}
|
|
44
44
|
/**
|
|
45
|
-
* Matches `exclude` the way scanning's
|
|
45
|
+
* Matches `exclude` the way scanning's tinyglobby `ignore` does: a pattern
|
|
46
46
|
* that matches a directory (`graphify-out`, `graphify-out/`) excludes
|
|
47
47
|
* everything under it, so test the path and each of its parent directories.
|
|
48
48
|
* `covers` reads the same way.
|
|
@@ -142,6 +142,23 @@ export function configChanges(before, now) {
|
|
|
142
142
|
}
|
|
143
143
|
return changes;
|
|
144
144
|
}
|
|
145
|
+
/**
|
|
146
|
+
* Whether the config file under `root` changed since `before` in what is
|
|
147
|
+
* scanned or ignored (`exclude`, globs, paths), not just in its commands.
|
|
148
|
+
* False when `before` recorded no config file.
|
|
149
|
+
*/
|
|
150
|
+
export function scanConfigChanged(before, root) {
|
|
151
|
+
if (!before?.file)
|
|
152
|
+
return false;
|
|
153
|
+
let text;
|
|
154
|
+
try {
|
|
155
|
+
text = fs.readFileSync(path.join(root, before.file.path), "utf8");
|
|
156
|
+
}
|
|
157
|
+
catch {
|
|
158
|
+
return true;
|
|
159
|
+
}
|
|
160
|
+
return withoutCommands(before.file.text) !== withoutCommands(text);
|
|
161
|
+
}
|
|
145
162
|
const quote = (text) => (text === undefined ? "(none)" : `\`${text}\``);
|
|
146
163
|
function changedFiles(a, b) {
|
|
147
164
|
const hashes = new Map(a.map((f) => [f.path, f.sha256]));
|
package/dist/core/detect.js
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* saves the user from editing a template, and they check the result.
|
|
6
6
|
*/
|
|
7
7
|
import path from "node:path";
|
|
8
|
-
import
|
|
8
|
+
import { globSync } from "tinyglobby";
|
|
9
9
|
import { detectTestRunner } from "./test-runner.js";
|
|
10
10
|
const IGNORE = ["**/node_modules/**", "**/.git/**", "**/bin/**", "**/obj/**", "**/dist/**"];
|
|
11
11
|
const depth = (file) => file.split("/").length;
|
|
@@ -32,7 +32,7 @@ function commonDir(files) {
|
|
|
32
32
|
return common.join("/") || ".";
|
|
33
33
|
}
|
|
34
34
|
export function detectProjects(cwd) {
|
|
35
|
-
const find = (pattern) =>
|
|
35
|
+
const find = (pattern) => globSync(pattern, { expandDirectories: false, cwd, deep: 3, ignore: IGNORE });
|
|
36
36
|
const found = [];
|
|
37
37
|
for (const marker of topmost(find("**/angular.json"))) {
|
|
38
38
|
const dir = path.posix.dirname(marker);
|
package/dist/core/git.js
CHANGED
|
@@ -13,6 +13,8 @@ function git(cwd, ...args) {
|
|
|
13
13
|
const result = spawnSync("git", args, { cwd, encoding: "utf8", maxBuffer: 256 * 1024 * 1024 });
|
|
14
14
|
return { ok: result.status === 0, stdout: result.stdout ?? "" };
|
|
15
15
|
}
|
|
16
|
+
/** Whether git reads `rev` as a revision: one starting with `-` (`--output=<file>`) would be an option. */
|
|
17
|
+
const isRevision = (rev) => rev !== "" && !rev.startsWith("-");
|
|
16
18
|
/** NUL-separated paths (`-z`), which git never quotes or escapes, whatever the file name. */
|
|
17
19
|
const paths = (text) => text.split("\0").filter(Boolean);
|
|
18
20
|
/** Untracked (and not ignored) files under `cwd`. */
|
|
@@ -60,6 +62,8 @@ export function blobIds(cwd, files) {
|
|
|
60
62
|
* diff plus untracked files), without the ones `ignore` names.
|
|
61
63
|
*/
|
|
62
64
|
export function changedFilesSince(cwd, sha, ignore) {
|
|
65
|
+
if (!isRevision(sha))
|
|
66
|
+
return undefined;
|
|
63
67
|
const diff = git(cwd, "diff", "-z", "--name-only", "--no-renames", "--relative", sha, ...PATHSPEC);
|
|
64
68
|
if (!diff.ok)
|
|
65
69
|
return undefined;
|
|
@@ -72,6 +76,8 @@ export function changedFilesSince(cwd, sha, ignore) {
|
|
|
72
76
|
* Undefined when git can't tell.
|
|
73
77
|
*/
|
|
74
78
|
export function trackedChangesSince(cwd, sha, ignore) {
|
|
79
|
+
if (!isRevision(sha))
|
|
80
|
+
return undefined;
|
|
75
81
|
const diff = git(cwd, "diff", "-z", "--name-only", "--no-renames", "--relative", sha, ...PATHSPEC);
|
|
76
82
|
if (!diff.ok)
|
|
77
83
|
return undefined;
|
|
@@ -80,6 +86,8 @@ export function trackedChangesSince(cwd, sha, ignore) {
|
|
|
80
86
|
}
|
|
81
87
|
/** A file's content at `sha`, or undefined when it did not exist there. */
|
|
82
88
|
export function fileAt(cwd, sha, relativePath) {
|
|
89
|
+
if (!isRevision(sha))
|
|
90
|
+
return undefined;
|
|
83
91
|
const result = git(cwd, "show", `${sha}:./${relativePath.split(path.sep).join("/")}`);
|
|
84
92
|
return result.ok ? result.stdout : undefined;
|
|
85
93
|
}
|
|
@@ -156,6 +164,8 @@ function dropIgnored(cwd, index, ignore) {
|
|
|
156
164
|
}
|
|
157
165
|
/** The tree id of `cwd` at commit `sha`. */
|
|
158
166
|
export function treeAt(cwd, sha) {
|
|
167
|
+
if (!isRevision(sha))
|
|
168
|
+
return undefined;
|
|
159
169
|
const prefix = repoPrefix(cwd);
|
|
160
170
|
if (prefix === undefined)
|
|
161
171
|
return undefined;
|
|
@@ -169,5 +179,5 @@ export function headSha(cwd) {
|
|
|
169
179
|
}
|
|
170
180
|
/** Whether `sha` is HEAD or one of its ancestors. */
|
|
171
181
|
export function isAncestor(cwd, sha) {
|
|
172
|
-
return git(cwd, "merge-base", "--is-ancestor", sha, "HEAD").ok;
|
|
182
|
+
return isRevision(sha) && git(cwd, "merge-base", "--is-ancestor", sha, "HEAD").ok;
|
|
173
183
|
}
|
package/dist/core/hook.js
CHANGED
|
@@ -8,6 +8,7 @@ import fs from "node:fs";
|
|
|
8
8
|
import path from "node:path";
|
|
9
9
|
import { changeFilterAt } from "./change-filter.js";
|
|
10
10
|
import { findConfigRoot } from "./config.js";
|
|
11
|
+
import { scanConfigChanged } from "./config-snapshot.js";
|
|
11
12
|
import { headSha, trackedChangesSince, treeFingerprint } from "./git.js";
|
|
12
13
|
import { readJson } from "./util.js";
|
|
13
14
|
/** The hook's stdin: only a JSON object counts, and only fields of the expected type are kept. */
|
|
@@ -38,7 +39,8 @@ export function hookRoot(cwd) {
|
|
|
38
39
|
* matches the current tree. In progress means HEAD is still the baseline
|
|
39
40
|
* commit and a file git tracks changed, other than flagrm's setup files and
|
|
40
41
|
* paths matching `exclude` (a removal paused on a question to the user, or a
|
|
41
|
-
* stray test result file, doesn't count),
|
|
42
|
+
* stray test result file, doesn't count), the config changed in what it
|
|
43
|
+
* scans or ignores, or the last verify skipped checks.
|
|
42
44
|
* The fingerprint leaves out the same paths, as verify's does. Once HEAD moves (a
|
|
43
45
|
* commit, a branch switch, a merge), that removal is no longer guarded.
|
|
44
46
|
*/
|
|
@@ -61,7 +63,7 @@ export function stopDecision(root, input) {
|
|
|
61
63
|
return { block: false };
|
|
62
64
|
const ignore = changeFilterAt(root);
|
|
63
65
|
const edited = (trackedChangesSince(root, head, ignore) ?? []).length > 0;
|
|
64
|
-
const started = active.filter((flagDir) => edited || lastVerifySkipped(flagDir));
|
|
66
|
+
const started = active.filter((flagDir) => edited || lastVerifySkipped(flagDir) || configChanged(flagDir, root));
|
|
65
67
|
if (started.length === 0)
|
|
66
68
|
return { block: false };
|
|
67
69
|
const current = treeFingerprint(root, ignore);
|
|
@@ -74,6 +76,15 @@ export function stopDecision(root, input) {
|
|
|
74
76
|
return { block: false };
|
|
75
77
|
}
|
|
76
78
|
}
|
|
79
|
+
/**
|
|
80
|
+
* Whether the config changed since the baseline (or `--accept config`) in what
|
|
81
|
+
* is scanned or ignored: widening `exclude` would otherwise hide the removal's
|
|
82
|
+
* edits from the hook. A verify passing on this tree shows the change and allows.
|
|
83
|
+
*/
|
|
84
|
+
function configChanged(flagDir, root) {
|
|
85
|
+
const baseline = readJson(path.join(flagDir, "baseline.json"));
|
|
86
|
+
return scanConfigChanged(baseline?.acceptedConfig ?? baseline?.config, root);
|
|
87
|
+
}
|
|
77
88
|
function lastVerifySkipped(flagDir) {
|
|
78
89
|
const skipped = readJson(path.join(flagDir, "verify.json"))?.skipped;
|
|
79
90
|
return Array.isArray(skipped) && skipped.length > 0;
|
package/dist/core/run-cache.js
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
*/
|
|
7
7
|
import fs from "node:fs";
|
|
8
8
|
import path from "node:path";
|
|
9
|
-
import
|
|
9
|
+
import { globSync } from "tinyglobby";
|
|
10
10
|
import { flagDir, matchingFiles, runChecks } from "./baseline.js";
|
|
11
11
|
import { changeFilter } from "./change-filter.js";
|
|
12
12
|
import { configSnapshot, projectCommands } from "./config-snapshot.js";
|
|
@@ -69,7 +69,12 @@ export function saveRuns(root, flag, projects, key, runs, createdAt = new Date()
|
|
|
69
69
|
}
|
|
70
70
|
/** Forget every saved set, of any flag, for this tree: a fresh run on it failed, so they may hide a flaky test or a changed machine. */
|
|
71
71
|
export function dropRuns(root, key) {
|
|
72
|
-
for (const file of
|
|
72
|
+
for (const file of globSync(".flagrm/*/verify/runs.json", {
|
|
73
|
+
expandDirectories: false,
|
|
74
|
+
cwd: root,
|
|
75
|
+
absolute: true,
|
|
76
|
+
dot: true,
|
|
77
|
+
})) {
|
|
73
78
|
if (readJson(file)?.key === key)
|
|
74
79
|
fs.rmSync(file, { force: true });
|
|
75
80
|
}
|
|
@@ -81,7 +86,12 @@ export function findRuns(root, projects, key, needed) {
|
|
|
81
86
|
const commands = JSON.stringify(configSnapshot(projects).projects);
|
|
82
87
|
const version = packageVersion();
|
|
83
88
|
let best;
|
|
84
|
-
for (const file of
|
|
89
|
+
for (const file of globSync(".flagrm/*/verify/runs.json", {
|
|
90
|
+
expandDirectories: false,
|
|
91
|
+
cwd: root,
|
|
92
|
+
absolute: true,
|
|
93
|
+
dot: true,
|
|
94
|
+
})) {
|
|
85
95
|
const saved = readJson(file);
|
|
86
96
|
if (!saved || !Array.isArray(saved.runs) || typeof saved.createdAt !== "string")
|
|
87
97
|
continue;
|
package/dist/core/scan.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
1
2
|
import path from "node:path";
|
|
2
|
-
import
|
|
3
|
+
import { globSync, isDynamicPattern } from "tinyglobby";
|
|
3
4
|
export const BACKEND_GLOBS = [
|
|
4
5
|
"**/*.cs",
|
|
5
6
|
"**/*.cshtml",
|
|
@@ -19,11 +20,37 @@ export const IGNORE_GLOBS = [
|
|
|
19
20
|
"**/.angular/**",
|
|
20
21
|
"**/.git/**",
|
|
21
22
|
];
|
|
22
|
-
/**
|
|
23
|
+
/**
|
|
24
|
+
* Files matching an absolute path or glob, such as a `testResults` pattern,
|
|
25
|
+
* as absolute paths with `/`. The leading directories that exist are taken
|
|
26
|
+
* literally and become the glob's `cwd`, so a repository path containing
|
|
27
|
+
* glob characters (`/work/p[x]/TestResults/*.trx`) still matches.
|
|
28
|
+
*/
|
|
29
|
+
export function globAbsolute(pattern) {
|
|
30
|
+
const segments = (path.sep === "\\" ? pattern.replace(/\\/g, "/") : pattern).split("/");
|
|
31
|
+
let base = segments[0] === "" ? "/" : `${segments[0]}/`;
|
|
32
|
+
let i = 1;
|
|
33
|
+
for (; i < segments.length - 1; i++) {
|
|
34
|
+
const dir = path.posix.join(base, segments[i]);
|
|
35
|
+
if (isDynamicPattern(segments[i]) && !isDirectory(dir))
|
|
36
|
+
break;
|
|
37
|
+
base = dir;
|
|
38
|
+
}
|
|
39
|
+
return globSync(segments.slice(i).join("/"), { expandDirectories: false, cwd: base, absolute: true, dot: true });
|
|
40
|
+
}
|
|
41
|
+
function isDirectory(dir) {
|
|
42
|
+
try {
|
|
43
|
+
return fs.statSync(dir).isDirectory();
|
|
44
|
+
}
|
|
45
|
+
catch {
|
|
46
|
+
return false;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
/** Absolute paths with the platform's separators (tinyglobby returns `/` on Windows too), so they match `path.join`. */
|
|
23
50
|
export function listProjectFiles(root, globs, overrides) {
|
|
24
51
|
const patterns = [...globs, ...(overrides?.include ?? [])];
|
|
25
52
|
const ignore = [...IGNORE_GLOBS, ...(overrides?.exclude ?? [])];
|
|
26
|
-
return
|
|
53
|
+
return globSync(patterns, { expandDirectories: false, cwd: root, ignore, absolute: true, dot: false }).map((file) => path.normalize(file));
|
|
27
54
|
}
|
|
28
55
|
export function escapeRegExp(s) {
|
|
29
56
|
return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
package/dist/core/scope.js
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
import { spawnSync } from "node:child_process";
|
|
9
9
|
import fs from "node:fs";
|
|
10
10
|
import path from "node:path";
|
|
11
|
-
import
|
|
11
|
+
import { globSync } from "tinyglobby";
|
|
12
12
|
import YAML from "yaml";
|
|
13
13
|
import { sdkProblem, solutionSetup } from "../adapters/dotnet/setup.js";
|
|
14
14
|
import { projectCommands } from "./config-snapshot.js";
|
|
@@ -81,8 +81,8 @@ function solutionFor(ctx, commands) {
|
|
|
81
81
|
return { note: `can't read ${arg}` };
|
|
82
82
|
}
|
|
83
83
|
}
|
|
84
|
-
const found =
|
|
85
|
-
//
|
|
84
|
+
const found = globSync("*.{sln,slnx}", { expandDirectories: false, cwd: ctx.root, absolute: true });
|
|
85
|
+
// tinyglobby returns forward slashes, also on Windows.
|
|
86
86
|
if (found.length === 1)
|
|
87
87
|
return { solution: path.normalize(found[0]) };
|
|
88
88
|
return { note: found.length ? "several solutions: name one in build/test" : "no solution file found" };
|
package/dist/core/test-runner.js
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
*/
|
|
7
7
|
import fs from "node:fs";
|
|
8
8
|
import path from "node:path";
|
|
9
|
-
import
|
|
9
|
+
import { globSync } from "tinyglobby";
|
|
10
10
|
import { readJson, relativePath } from "./util.js";
|
|
11
11
|
/** The test target of the first `angular.json` project that has one. */
|
|
12
12
|
function angularTestTarget(root) {
|
|
@@ -22,7 +22,7 @@ function dependencies(root) {
|
|
|
22
22
|
const pkg = readJson(path.join(root, "package.json"));
|
|
23
23
|
return new Set([...Object.keys(pkg?.dependencies ?? {}), ...Object.keys(pkg?.devDependencies ?? {})]);
|
|
24
24
|
}
|
|
25
|
-
const hasFile = (root, pattern) =>
|
|
25
|
+
const hasFile = (root, pattern) => globSync(pattern, { expandDirectories: false, cwd: root, deep: 1 }).length > 0;
|
|
26
26
|
function karma(root, options) {
|
|
27
27
|
const conf = path.resolve(root, typeof options.karmaConfig === "string" ? options.karmaConfig : "karma.conf.js");
|
|
28
28
|
let text;
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
*/
|
|
8
8
|
import fs from "node:fs";
|
|
9
9
|
import path from "node:path";
|
|
10
|
-
import
|
|
10
|
+
import { globAbsolute } from "../scan.js";
|
|
11
11
|
function attributes(tag) {
|
|
12
12
|
const out = {};
|
|
13
13
|
for (const m of tag.matchAll(/([\w:-]+)\s*=\s*(?:"([^"]*)"|'([^']*)')/g))
|
|
@@ -200,8 +200,7 @@ export function hasTestNames(results) {
|
|
|
200
200
|
export function collectTestResults(pattern, since, claimed = new Map()) {
|
|
201
201
|
// Filesystem timestamps can be coarser than Date.now().
|
|
202
202
|
const cutoff = since - 2000;
|
|
203
|
-
const files =
|
|
204
|
-
.sync(path.sep === "\\" ? pattern.replace(/\\/g, "/") : pattern, { absolute: true, dot: true })
|
|
203
|
+
const files = globAbsolute(pattern)
|
|
205
204
|
.map((file) => ({ file, mtime: fs.statSync(file).mtimeMs }))
|
|
206
205
|
.filter(({ file, mtime }) => mtime >= cutoff && claimed.get(file) !== mtime)
|
|
207
206
|
.sort((a, b) => a.file.localeCompare(b.file));
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "flagrm",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.1",
|
|
4
4
|
"description": "Guardrails for removing feature flags with AI coding agents (Claude Code, Copilot, Codex): skills, a baseline, a verify gate and a Stop hook.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "Apache-2.0",
|
|
@@ -53,17 +53,17 @@
|
|
|
53
53
|
],
|
|
54
54
|
"dependencies": {
|
|
55
55
|
"commander": "^13.1.0",
|
|
56
|
-
"fast-glob": "^3.3.3",
|
|
57
56
|
"picocolors": "^1.1.1",
|
|
58
57
|
"picomatch": "^4.0.7",
|
|
58
|
+
"tinyglobby": "^0.2.17",
|
|
59
59
|
"typescript": "^5.7.0",
|
|
60
60
|
"yaml": "^2.7.0"
|
|
61
61
|
},
|
|
62
62
|
"devDependencies": {
|
|
63
|
-
"@biomejs/biome": "2.5.
|
|
63
|
+
"@biomejs/biome": "2.5.15",
|
|
64
64
|
"@types/node": "^22.13.0",
|
|
65
65
|
"@types/picomatch": "^4.0.3",
|
|
66
66
|
"tsx": "^4.19.0",
|
|
67
|
-
"vitest": "^
|
|
67
|
+
"vitest": "^4.1.11"
|
|
68
68
|
}
|
|
69
69
|
}
|