flagrm 0.5.0 → 1.0.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 +40 -2
- package/README.md +23 -5
- package/dist/cli.js +18 -9
- package/dist/core/change-filter.js +3 -3
- package/dist/core/config-snapshot.js +0 -4
- package/dist/core/config.js +27 -58
- package/dist/core/discover.js +1 -1
- package/dist/core/doctor.js +1 -6
- package/dist/core/install.js +3 -54
- package/dist/core/report.js +0 -1
- package/package.json +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -2,10 +2,47 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to this project are documented here. The format follows
|
|
4
4
|
[Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and the project uses
|
|
5
|
-
[Semantic Versioning](https://semver.org/)
|
|
5
|
+
[Semantic Versioning](https://semver.org/); the README's "Versioning" section
|
|
6
|
+
says what a minor release won't break. Before 1.0, a minor version could
|
|
6
7
|
contain breaking changes.
|
|
7
8
|
|
|
8
|
-
## [
|
|
9
|
+
## [1.0.0] - 2026-11-08
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
|
|
13
|
+
- README "Versioning" section: from 1.0, commands, options, exit codes,
|
|
14
|
+
config keys, `--json` output, the `.flagrm/` files, the skill names and the
|
|
15
|
+
Stop hook command stay compatible within a major version.
|
|
16
|
+
- Releases are staged on npm from a `v*` tag by GitHub Actions, with
|
|
17
|
+
provenance, and go live only after a maintainer approves them with 2FA.
|
|
18
|
+
|
|
19
|
+
### Fixed
|
|
20
|
+
|
|
21
|
+
- A usage error found while parsing the command line (unknown option or
|
|
22
|
+
command, missing argument) exits 2, as documented, instead of 1, which
|
|
23
|
+
`verify` uses for a failed removal.
|
|
24
|
+
|
|
25
|
+
### Removed
|
|
26
|
+
|
|
27
|
+
- **Breaking:** the `--frontend` and `--backend` options, and the top-level
|
|
28
|
+
`frontend:`, `backend:`, `frontendMethods:`, `backendMethods:` and
|
|
29
|
+
`backendAttributes:` config keys. List each project under `projects:`
|
|
30
|
+
instead, with `methods:` and `attributes:` on the project. A config that
|
|
31
|
+
still has one of these keys is rejected with a message naming its
|
|
32
|
+
replacement:
|
|
33
|
+
|
|
34
|
+
```yaml
|
|
35
|
+
projects:
|
|
36
|
+
- { name: frontend, adapter: angular, path: ./frontend }
|
|
37
|
+
- { name: backend, adapter: dotnet, path: ./backend }
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
- **Breaking:** `flagrm update` no longer replaces the `flagrm-verify` skill
|
|
41
|
+
and prompt file of flagrm 0.4 and earlier, and `doctor` no longer warns
|
|
42
|
+
about them. Coming from 0.4 or earlier, run `flagrm update` with flagrm 0.5
|
|
43
|
+
first, or run `flagrm init` (it adds `flagrm-review`) and delete the
|
|
44
|
+
`flagrm-verify` skill directories and `.github/prompts/flagrm-verify.prompt.md`
|
|
45
|
+
by hand.
|
|
9
46
|
|
|
10
47
|
## [0.5.0] - 2026-10-23
|
|
11
48
|
|
|
@@ -317,6 +354,7 @@ removes the flag through the `flagrm-remove` skill, and flagrm checks the result
|
|
|
317
354
|
`doctor` warns about an incremental `dotnet build`, which hides existing
|
|
318
355
|
warnings from the dead-code comparison.
|
|
319
356
|
|
|
357
|
+
[1.0.0]: https://github.com/dino-cosic/flagrm/releases/tag/v1.0.0
|
|
320
358
|
[0.5.0]: https://github.com/dino-cosic/flagrm/releases/tag/v0.5.0
|
|
321
359
|
[0.4.1]: https://github.com/dino-cosic/flagrm/releases/tag/v0.4.1
|
|
322
360
|
[0.4.0]: https://github.com/dino-cosic/flagrm/releases/tag/v0.4.0
|
package/README.md
CHANGED
|
@@ -31,8 +31,8 @@ deterministic definition of "done":
|
|
|
31
31
|
Angular and .NET projects work out of the box. Any other stack works through the
|
|
32
32
|
generic adapter.
|
|
33
33
|
|
|
34
|
-
> **Status:** flagrm
|
|
35
|
-
>
|
|
34
|
+
> **Status:** flagrm follows semantic versioning from 1.0. See
|
|
35
|
+
> [Versioning](#versioning) for what a minor release won't break.
|
|
36
36
|
|
|
37
37
|
## Quick start
|
|
38
38
|
|
|
@@ -232,9 +232,7 @@ narrower test command makes "no new failures" mean less.
|
|
|
232
232
|
`flagrm.slnf`, adds it to `.git/info/exclude` and points the project's build
|
|
233
233
|
and test at it. See [When the machine can't build everything](#when-the-machine-cant-build-everything).
|
|
234
234
|
- **`update`**: refreshes files from this flagrm version, for the config's
|
|
235
|
-
`tools:`.
|
|
236
|
-
versions with `flagrm-review`, deleting them only when flagrm installed
|
|
237
|
-
them.
|
|
235
|
+
`tools:`.
|
|
238
236
|
- **`list`**: flow names include renamed imports of a flag registry
|
|
239
237
|
(`import { FeatureFlags as AppFeatureFlags }`) and, in Angular, the fields,
|
|
240
238
|
getters, methods and locals holding a flag and a child component's input
|
|
@@ -314,7 +312,11 @@ path. `build` and `test` are resolved one at a time:
|
|
|
314
312
|
| `lint` | Optional lint or type-check command, run like `build` and `test` |
|
|
315
313
|
| `covers` | Globs (relative to `path`) of the files the project's commands check |
|
|
316
314
|
| `timeout` | Seconds after which a build or test command fails, for example a test runner left in watch mode |
|
|
315
|
+
| `globs` | Globs (relative to `path`) of the files to scan for flags, replacing the adapter's default language globs |
|
|
316
|
+
| `methods` | Method names that evaluate a flag (`isEnabled`, `IsEnabledAsync`, ...), replacing the adapter's defaults |
|
|
317
|
+
| `attributes` | .NET attributes that gate code on a flag, replacing the defaults `FeatureGate` and `Feature` |
|
|
317
318
|
| `tools` | The AI coding tools whose files `init` and `update` manage (`claude-code`, `copilot`, `codex`) |
|
|
319
|
+
| `include` | Extra globs (relative to each project's path) to scan, on top of the language globs, such as `**/*.razor.cs` |
|
|
318
320
|
| `exclude` | Paths that don't count as part of a removal |
|
|
319
321
|
|
|
320
322
|
**`testResults`.** Only files written during the run are read, and every
|
|
@@ -455,6 +457,22 @@ flagrm does not coordinate the two: each repository gets its own verdict, the
|
|
|
455
457
|
agent has to record the flag's name in each (it may differ between UI and
|
|
456
458
|
API), and the order of the two removals is up to you.
|
|
457
459
|
|
|
460
|
+
## Versioning
|
|
461
|
+
|
|
462
|
+
flagrm follows [semantic versioning](https://semver.org/). Within 1.x, these
|
|
463
|
+
stay compatible:
|
|
464
|
+
|
|
465
|
+
- commands, their options and exit codes;
|
|
466
|
+
- `flagrm.config.yaml` keys and their meaning;
|
|
467
|
+
- `--json` output and the `.flagrm/` files: fields may be added, none removed
|
|
468
|
+
or changed, and a baseline recorded by 1.x is read by every later 1.x;
|
|
469
|
+
- the skill names (`/flagrm-remove`, `/flagrm-review`) and the Stop hook
|
|
470
|
+
command `init` installs.
|
|
471
|
+
|
|
472
|
+
Not covered: the text and markdown output, the wording of the skills, and
|
|
473
|
+
detection itself. A minor release may find more flag usages or unused code,
|
|
474
|
+
so a removal that passed `verify` before can fail it after an upgrade.
|
|
475
|
+
|
|
458
476
|
## Development
|
|
459
477
|
|
|
460
478
|
```sh
|
package/dist/cli.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import fs from "node:fs";
|
|
3
3
|
import path from "node:path";
|
|
4
|
-
import { Command } from "commander";
|
|
4
|
+
import { Command, CommanderError } from "commander";
|
|
5
5
|
import pc from "picocolors";
|
|
6
6
|
import { realProbes } from "./adapters/dotnet/setup.js";
|
|
7
7
|
import { acceptBaseline, addNames, assertNoEditsSinceBaseline, baselinePath, baselineSummary, discoverFlag, mergeNames, parseAccept, validateAgentNames, writeBaseline, } from "./core/baseline.js";
|
|
@@ -34,7 +34,9 @@ program
|
|
|
34
34
|
.description("Guardrails for removing feature flags with an AI coding agent: set up the skills and hook (init, update, " +
|
|
35
35
|
"doctor), find flags (list) and those LaunchDarkly says are ready to remove (stale), record the state before a " +
|
|
36
36
|
"removal (baseline) and gate the result (verify).")
|
|
37
|
-
.version(packageVersion())
|
|
37
|
+
.version(packageVersion())
|
|
38
|
+
// Throw instead of exiting, so a usage error commander finds (unknown option, missing argument) exits 2 too.
|
|
39
|
+
.exitOverride();
|
|
38
40
|
/** Accumulate repeatable options (`--exclude a --exclude b`) into an array. */
|
|
39
41
|
function collect(value, previous) {
|
|
40
42
|
return previous.concat([value]);
|
|
@@ -46,8 +48,6 @@ function addGlobOptions(cmd) {
|
|
|
46
48
|
}
|
|
47
49
|
function addCommonOptions(cmd) {
|
|
48
50
|
return addGlobOptions(cmd
|
|
49
|
-
.option("--frontend <path>", "path to an Angular project (adds or replaces the project named 'frontend')")
|
|
50
|
-
.option("--backend <path>", "path to a .NET project (adds or replaces the project named 'backend')")
|
|
51
51
|
.option("--config <path>", "path to a config file (default: the nearest flagrm.config.yaml up to the git repository root)")
|
|
52
52
|
.option("--json", "machine-readable JSON output"));
|
|
53
53
|
}
|
|
@@ -382,9 +382,18 @@ addCommonOptions(program
|
|
|
382
382
|
process.exitCode = missing ? 1 : 0;
|
|
383
383
|
});
|
|
384
384
|
// exitCode, not exit(): exit() can cut off output still being flushed to a pipe.
|
|
385
|
-
|
|
386
|
-
|
|
385
|
+
try {
|
|
386
|
+
await program.parseAsync();
|
|
387
|
+
}
|
|
388
|
+
catch (err) {
|
|
389
|
+
if (err instanceof CommanderError) {
|
|
390
|
+
// Commander printed the message already; --help and --version end with 0.
|
|
391
|
+
process.exitCode = err.exitCode === 0 ? 0 : 2;
|
|
392
|
+
}
|
|
393
|
+
else if (err instanceof CliError) {
|
|
394
|
+
console.error(pc.red(err.message));
|
|
395
|
+
process.exitCode = 2;
|
|
396
|
+
}
|
|
397
|
+
else
|
|
387
398
|
throw err;
|
|
388
|
-
|
|
389
|
-
process.exitCode = 2;
|
|
390
|
-
});
|
|
399
|
+
}
|
|
@@ -7,17 +7,17 @@
|
|
|
7
7
|
*/
|
|
8
8
|
import picomatch from "picomatch";
|
|
9
9
|
import { CONFIG_FILENAMES, loadConfig } from "./config.js";
|
|
10
|
-
import { AGENT_SKILL_DIRS,
|
|
10
|
+
import { AGENT_SKILL_DIRS, SKILLS } from "./install.js";
|
|
11
11
|
import { relativePath } from "./util.js";
|
|
12
12
|
/**
|
|
13
13
|
* The files `flagrm init`, `update` and `scope --write` write, relative to the
|
|
14
|
-
* config root.
|
|
14
|
+
* config root.
|
|
15
15
|
*/
|
|
16
16
|
export const SETUP_PATTERNS = [
|
|
17
17
|
...CONFIG_FILENAMES,
|
|
18
18
|
".gitignore",
|
|
19
19
|
".claude/settings.json",
|
|
20
|
-
...AGENT_SKILL_DIRS.flatMap((dir) =>
|
|
20
|
+
...AGENT_SKILL_DIRS.flatMap((dir) => SKILLS.map((skill) => `${dir}/${skill}/**`)),
|
|
21
21
|
".github/prompts/flagrm-*.prompt.md",
|
|
22
22
|
"AGENTS.md",
|
|
23
23
|
"**/flagrm.slnf",
|
|
@@ -193,10 +193,6 @@ function withoutCommands(text) {
|
|
|
193
193
|
delete config.tools;
|
|
194
194
|
if (Array.isArray(config.projects))
|
|
195
195
|
config.projects = config.projects.map(strip);
|
|
196
|
-
for (const legacy of ["frontend", "backend"]) {
|
|
197
|
-
if (config[legacy] && typeof config[legacy] === "object")
|
|
198
|
-
config[legacy] = strip(config[legacy]);
|
|
199
|
-
}
|
|
200
196
|
return JSON.stringify(config);
|
|
201
197
|
}
|
|
202
198
|
return JSON.stringify(parsed);
|
package/dist/core/config.js
CHANGED
|
@@ -33,18 +33,7 @@ export const DEFAULT_BACKEND_METHODS = [
|
|
|
33
33
|
export const DEFAULT_BACKEND_ATTRIBUTES = ["FeatureGate", "Feature"];
|
|
34
34
|
export const CONFIG_FILENAMES = ["flagrm.config.yaml", "flagrm.config.yml", "flagrm.config.json"];
|
|
35
35
|
/** Known top-level `flagrm.config.yaml` keys — anything else is likely a typo. */
|
|
36
|
-
const KNOWN_CONFIG_KEYS = [
|
|
37
|
-
"projects",
|
|
38
|
-
"tools",
|
|
39
|
-
"frontend",
|
|
40
|
-
"backend",
|
|
41
|
-
"frontendMethods",
|
|
42
|
-
"backendMethods",
|
|
43
|
-
"backendAttributes",
|
|
44
|
-
"exclude",
|
|
45
|
-
"include",
|
|
46
|
-
"launchdarkly",
|
|
47
|
-
];
|
|
36
|
+
const KNOWN_CONFIG_KEYS = ["projects", "tools", "exclude", "include", "launchdarkly"];
|
|
48
37
|
const KNOWN_LAUNCHDARKLY_KEYS = [
|
|
49
38
|
"project",
|
|
50
39
|
"baseUrl",
|
|
@@ -69,6 +58,14 @@ const KNOWN_PROJECT_KEYS = [
|
|
|
69
58
|
"methods",
|
|
70
59
|
"attributes",
|
|
71
60
|
];
|
|
61
|
+
/** Top-level keys removed in 1.0, with what replaces them. */
|
|
62
|
+
const REMOVED_CONFIG_KEYS = {
|
|
63
|
+
frontend: "a `projects:` entry with `adapter: angular`",
|
|
64
|
+
backend: "a `projects:` entry with `adapter: dotnet`",
|
|
65
|
+
frontendMethods: "`methods:` on the angular project",
|
|
66
|
+
backendMethods: "`methods:` on the dotnet project",
|
|
67
|
+
backendAttributes: "`attributes:` on the dotnet project",
|
|
68
|
+
};
|
|
72
69
|
const PROJECT_NAME = /^[A-Za-z0-9][\w.-]*$/;
|
|
73
70
|
function readConfigFile(configPath) {
|
|
74
71
|
const raw = fs.readFileSync(configPath, "utf8");
|
|
@@ -77,10 +74,12 @@ function readConfigFile(configPath) {
|
|
|
77
74
|
throw new Error(`Config file ${configPath} does not contain an object.`);
|
|
78
75
|
}
|
|
79
76
|
const file = parsed;
|
|
77
|
+
for (const [key, replacement] of Object.entries(REMOVED_CONFIG_KEYS)) {
|
|
78
|
+
if (key in file)
|
|
79
|
+
throw new Error(`${configPath}: "${key}" was removed in flagrm 1.0; use ${replacement}.`);
|
|
80
|
+
}
|
|
80
81
|
// Checked here so a YAML string (`exclude: "**/gen/**"`) fails instead of being spread into characters.
|
|
81
|
-
for (const key of ["
|
|
82
|
-
optionalString(file[key], key);
|
|
83
|
-
for (const key of ["frontendMethods", "backendMethods", "backendAttributes", "exclude", "include"]) {
|
|
82
|
+
for (const key of ["exclude", "include"]) {
|
|
84
83
|
optionalStringList(file[key], key);
|
|
85
84
|
}
|
|
86
85
|
return file;
|
|
@@ -255,18 +254,18 @@ function parseLaunchDarkly(raw, quiet = false) {
|
|
|
255
254
|
map: optionalStringMap(raw.map, `${where}.map`) ?? {},
|
|
256
255
|
};
|
|
257
256
|
}
|
|
258
|
-
/** Per-adapter defaults for `methods`/`attributes
|
|
259
|
-
function adapterDefaults(adapter
|
|
260
|
-
const frontend = file.frontendMethods?.length ? file.frontendMethods : DEFAULT_FRONTEND_METHODS;
|
|
261
|
-
const backend = file.backendMethods?.length ? file.backendMethods : DEFAULT_BACKEND_METHODS;
|
|
262
|
-
const attributes = file.backendAttributes?.length ? file.backendAttributes : DEFAULT_BACKEND_ATTRIBUTES;
|
|
257
|
+
/** Per-adapter defaults for `methods`/`attributes`. */
|
|
258
|
+
function adapterDefaults(adapter) {
|
|
263
259
|
if (adapter === "angular")
|
|
264
|
-
return { methods:
|
|
260
|
+
return { methods: DEFAULT_FRONTEND_METHODS, attributes: [] };
|
|
265
261
|
if (adapter === "dotnet")
|
|
266
|
-
return { methods:
|
|
267
|
-
return {
|
|
262
|
+
return { methods: DEFAULT_BACKEND_METHODS, attributes: DEFAULT_BACKEND_ATTRIBUTES };
|
|
263
|
+
return {
|
|
264
|
+
methods: [...new Set([...DEFAULT_FRONTEND_METHODS, ...DEFAULT_BACKEND_METHODS])],
|
|
265
|
+
attributes: DEFAULT_BACKEND_ATTRIBUTES,
|
|
266
|
+
};
|
|
268
267
|
}
|
|
269
|
-
function parseProject(raw, index,
|
|
268
|
+
function parseProject(raw, index, fileDir, quiet = false) {
|
|
270
269
|
const where = `projects[${index}]`;
|
|
271
270
|
if (raw === null || typeof raw !== "object" || Array.isArray(raw)) {
|
|
272
271
|
throw new Error(`${where} must be an object with name, adapter and path.`);
|
|
@@ -286,7 +285,7 @@ function parseProject(raw, index, file, fileDir, quiet = false) {
|
|
|
286
285
|
const adapter = p.adapter;
|
|
287
286
|
if (typeof p.path !== "string" || !p.path)
|
|
288
287
|
throw new Error(`${where} ("${name}") is missing a "path".`);
|
|
289
|
-
const defaults = adapterDefaults(adapter
|
|
288
|
+
const defaults = adapterDefaults(adapter);
|
|
290
289
|
const testResults = optionalString(p.testResults, `${where}.testResults`);
|
|
291
290
|
const project = {
|
|
292
291
|
name,
|
|
@@ -312,10 +311,7 @@ function parseProject(raw, index, file, fileDir, quiet = false) {
|
|
|
312
311
|
/**
|
|
313
312
|
* Merge CLI options with an optional config file and validate everything.
|
|
314
313
|
*
|
|
315
|
-
* Projects come from
|
|
316
|
-
* which become an `angular` project named `frontend` and a `dotnet` project
|
|
317
|
-
* named `backend`. `--frontend`/`--backend` on the CLI replace the project of
|
|
318
|
-
* that name (or add it).
|
|
314
|
+
* Projects come from the config file's `projects:` list.
|
|
319
315
|
*/
|
|
320
316
|
export function loadConfig(cli, cwd = process.cwd()) {
|
|
321
317
|
let file = {};
|
|
@@ -334,42 +330,15 @@ export function loadConfig(cli, cwd = process.cwd()) {
|
|
|
334
330
|
if (file.projects !== undefined && !Array.isArray(file.projects)) {
|
|
335
331
|
throw new Error(`"projects" must be a list of { name, adapter, path } entries.`);
|
|
336
332
|
}
|
|
337
|
-
const projects = (file.projects ?? []).map((raw, i) => parseProject(raw, i,
|
|
338
|
-
const legacyProject = (name, adapter, dir) => ({
|
|
339
|
-
name,
|
|
340
|
-
adapter,
|
|
341
|
-
path: dir,
|
|
342
|
-
...adapterDefaults(adapter, file),
|
|
343
|
-
});
|
|
344
|
-
if (file.frontend) {
|
|
345
|
-
projects.push(legacyProject("frontend", "angular", resolveProjectDir(fileDir, file.frontend, "Frontend")));
|
|
346
|
-
}
|
|
347
|
-
if (file.backend) {
|
|
348
|
-
projects.push(legacyProject("backend", "dotnet", resolveProjectDir(fileDir, file.backend, "Backend")));
|
|
349
|
-
}
|
|
333
|
+
const projects = (file.projects ?? []).map((raw, i) => parseProject(raw, i, fileDir, cli.quiet));
|
|
350
334
|
const seen = new Set();
|
|
351
335
|
for (const p of projects) {
|
|
352
336
|
if (seen.has(p.name))
|
|
353
337
|
throw new Error(`Duplicate project name "${p.name}" in ${configPath ?? "config"}.`);
|
|
354
338
|
seen.add(p.name);
|
|
355
339
|
}
|
|
356
|
-
// CLI shortcuts replace a same-named project (keeping its position) or append one.
|
|
357
|
-
for (const [name, adapter, raw] of [
|
|
358
|
-
["frontend", "angular", cli.frontend],
|
|
359
|
-
["backend", "dotnet", cli.backend],
|
|
360
|
-
]) {
|
|
361
|
-
if (!raw)
|
|
362
|
-
continue;
|
|
363
|
-
const label = name === "frontend" ? "Frontend" : "Backend";
|
|
364
|
-
const project = legacyProject(name, adapter, resolveProjectDir(cwd, raw, label));
|
|
365
|
-
const existing = projects.findIndex((p) => p.name === name);
|
|
366
|
-
if (existing === -1)
|
|
367
|
-
projects.push(project);
|
|
368
|
-
else
|
|
369
|
-
projects[existing] = project;
|
|
370
|
-
}
|
|
371
340
|
if (projects.length === 0) {
|
|
372
|
-
throw new Error("No projects configured. Add a `projects:` list to flagrm.config.yaml
|
|
341
|
+
throw new Error("No projects configured. Add a `projects:` list to flagrm.config.yaml (`flagrm init` writes one).");
|
|
373
342
|
}
|
|
374
343
|
const tools = parseTools(file.tools);
|
|
375
344
|
const launchdarkly = parseLaunchDarkly(file.launchdarkly, cli.quiet);
|
package/dist/core/discover.js
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* This is a UX aid: false negatives (a flag it misses) are fine — the agent
|
|
5
5
|
* can still remove a flag by name. False positives are mildly annoying but
|
|
6
6
|
* harmless since nothing is written. So the heuristics here are intentionally
|
|
7
|
-
*
|
|
7
|
+
* loose.
|
|
8
8
|
*/
|
|
9
9
|
import path from "node:path";
|
|
10
10
|
import { computeCodeMask, computeCommentMask, computeLexicalMask, escapeRegExp, LEX_COMMENT, LEX_STRING, methodNamePattern, } from "./scan.js";
|
package/dist/core/doctor.js
CHANGED
|
@@ -11,7 +11,7 @@ import { changeFilter, changeFilterAt } from "./change-filter.js";
|
|
|
11
11
|
import { findConfigFile, loadConfig, TOOL_IDS } from "./config.js";
|
|
12
12
|
import { projectCommands } from "./config-snapshot.js";
|
|
13
13
|
import { gitState, isAncestor } from "./git.js";
|
|
14
|
-
import { installedSkillVersion,
|
|
14
|
+
import { installedSkillVersion, SKILLS, stopHookState, TOOL_SKILL_DIRS } from "./install.js";
|
|
15
15
|
import { packageVersion } from "./package.js";
|
|
16
16
|
import { resolveProjects } from "./registry.js";
|
|
17
17
|
import { setupReport } from "./scope.js";
|
|
@@ -73,11 +73,6 @@ export async function runDoctor(cwd, options = {}) {
|
|
|
73
73
|
}
|
|
74
74
|
else
|
|
75
75
|
add("ok", `${dir}: skills ${version}`);
|
|
76
|
-
for (const [legacy, skill] of Object.entries(LEGACY_SKILLS)) {
|
|
77
|
-
if (fs.existsSync(path.join(root, dir, legacy))) {
|
|
78
|
-
add("warn", `${dir}/${legacy}: replaced by ${skill} (run flagrm update)`);
|
|
79
|
-
}
|
|
80
|
-
}
|
|
81
76
|
}
|
|
82
77
|
const hook = tools.includes("claude-code") ? stopHookState(root) : undefined;
|
|
83
78
|
if (hook === "current")
|
package/dist/core/install.js
CHANGED
|
@@ -4,8 +4,7 @@
|
|
|
4
4
|
* files: for Claude Code the skills and the Stop hook, for GitHub Copilot the
|
|
5
5
|
* skills and `/flagrm-*` prompt files, for Codex the skills and the AGENTS.md
|
|
6
6
|
* pointer. Every step reports what it did; nothing the user wrote is
|
|
7
|
-
* overwritten
|
|
8
|
-
* installed (see {@link LEGACY_SKILLS}).
|
|
7
|
+
* overwritten or deleted.
|
|
9
8
|
*/
|
|
10
9
|
import fs from "node:fs";
|
|
11
10
|
import path from "node:path";
|
|
@@ -16,12 +15,6 @@ import { runnerTestCommands, testResultsHint } from "./test-runner.js";
|
|
|
16
15
|
import { saveTools } from "./tools.js";
|
|
17
16
|
import { relativePath } from "./util.js";
|
|
18
17
|
export const SKILLS = ["flagrm-remove", "flagrm-review"];
|
|
19
|
-
/**
|
|
20
|
-
* Skills an earlier flagrm installed under another name, and the skill that
|
|
21
|
-
* replaces each. `update` deletes the old one (only when flagrm's stamp shows
|
|
22
|
-
* flagrm wrote it) and installs its replacement; `init` deletes nothing.
|
|
23
|
-
*/
|
|
24
|
-
export const LEGACY_SKILLS = { "flagrm-verify": "flagrm-review" };
|
|
25
18
|
/** Where each supported tool discovers repository skills. */
|
|
26
19
|
export const TOOL_SKILL_DIRS = {
|
|
27
20
|
"claude-code": ".claude/skills",
|
|
@@ -151,28 +144,6 @@ export function writeConfigTemplate(cwd, tools) {
|
|
|
151
144
|
export function installSkills(cwd, mode, version = packageVersion(), source = path.join(packageRoot(), "skills"), tools = TOOL_IDS) {
|
|
152
145
|
const results = [];
|
|
153
146
|
for (const dir of tools.map((t) => TOOL_SKILL_DIRS[t])) {
|
|
154
|
-
const replaced = new Set();
|
|
155
|
-
for (const [legacy, skill] of Object.entries(LEGACY_SKILLS)) {
|
|
156
|
-
const target = path.join(cwd, dir, legacy);
|
|
157
|
-
if (!fs.existsSync(target))
|
|
158
|
-
continue;
|
|
159
|
-
const step = relativePath(cwd, target);
|
|
160
|
-
if (mode === "init") {
|
|
161
|
-
results.push({ path: step, status: "skipped", note: `replaced by ${skill}: run flagrm update to delete it` });
|
|
162
|
-
}
|
|
163
|
-
else if (installedSkillVersion(target) === undefined) {
|
|
164
|
-
results.push({
|
|
165
|
-
path: step,
|
|
166
|
-
status: "skipped",
|
|
167
|
-
note: "no flagrm-version stamp, so not deleted: delete it by hand",
|
|
168
|
-
});
|
|
169
|
-
}
|
|
170
|
-
else {
|
|
171
|
-
fs.rmSync(target, { recursive: true, force: true });
|
|
172
|
-
results.push({ path: step, status: "deleted", note: `replaced by ${skill}` });
|
|
173
|
-
replaced.add(skill);
|
|
174
|
-
}
|
|
175
|
-
}
|
|
176
147
|
for (const skill of SKILLS) {
|
|
177
148
|
const target = path.join(cwd, dir, skill);
|
|
178
149
|
const exists = fs.existsSync(target);
|
|
@@ -180,7 +151,7 @@ export function installSkills(cwd, mode, version = packageVersion(), source = pa
|
|
|
180
151
|
results.push({ path: relativePath(cwd, target), status: "skipped" });
|
|
181
152
|
continue;
|
|
182
153
|
}
|
|
183
|
-
if (mode === "update" && !exists
|
|
154
|
+
if (mode === "update" && !exists)
|
|
184
155
|
continue;
|
|
185
156
|
const before = exists ? snapshot(target) : undefined;
|
|
186
157
|
fs.rmSync(target, { recursive: true, force: true });
|
|
@@ -226,28 +197,6 @@ export function installedSkillVersion(skillDir) {
|
|
|
226
197
|
export function installPrompts(cwd, mode) {
|
|
227
198
|
const results = [];
|
|
228
199
|
const prompt = (skill) => path.join(cwd, ".github", "prompts", `${skill}.prompt.md`);
|
|
229
|
-
const replaced = new Set();
|
|
230
|
-
for (const [legacy, skill] of Object.entries(LEGACY_SKILLS)) {
|
|
231
|
-
const file = prompt(legacy);
|
|
232
|
-
if (!fs.existsSync(file))
|
|
233
|
-
continue;
|
|
234
|
-
const step = relativePath(cwd, file);
|
|
235
|
-
if (mode === "init") {
|
|
236
|
-
results.push({ path: step, status: "skipped", note: `replaced by ${skill}: run flagrm update to delete it` });
|
|
237
|
-
}
|
|
238
|
-
else if (!fs.readFileSync(file, "utf8").includes(`.github/skills/${legacy}/SKILL.md`)) {
|
|
239
|
-
results.push({
|
|
240
|
-
path: step,
|
|
241
|
-
status: "skipped",
|
|
242
|
-
note: `doesn't point at flagrm's ${legacy} skill, so not deleted`,
|
|
243
|
-
});
|
|
244
|
-
}
|
|
245
|
-
else {
|
|
246
|
-
fs.rmSync(file);
|
|
247
|
-
results.push({ path: step, status: "deleted", note: `replaced by ${skill}` });
|
|
248
|
-
replaced.add(skill);
|
|
249
|
-
}
|
|
250
|
-
}
|
|
251
200
|
for (const skill of SKILLS) {
|
|
252
201
|
const file = prompt(skill);
|
|
253
202
|
const step = relativePath(cwd, file);
|
|
@@ -256,7 +205,7 @@ export function installPrompts(cwd, mode) {
|
|
|
256
205
|
results.push({ path: step, status: "skipped" });
|
|
257
206
|
continue;
|
|
258
207
|
}
|
|
259
|
-
if (mode === "update" && !exists
|
|
208
|
+
if (mode === "update" && !exists)
|
|
260
209
|
continue;
|
|
261
210
|
const before = exists ? fs.readFileSync(file, "utf8") : undefined;
|
|
262
211
|
if (before === PROMPT_FILES[skill]) {
|
package/dist/core/report.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "flagrm",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "1.0.0",
|
|
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",
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
"type": "git",
|
|
9
9
|
"url": "git+https://github.com/dino-cosic/flagrm.git"
|
|
10
10
|
},
|
|
11
|
-
"homepage": "https://
|
|
11
|
+
"homepage": "https://flagrm.com/",
|
|
12
12
|
"bugs": {
|
|
13
13
|
"url": "https://github.com/dino-cosic/flagrm/issues"
|
|
14
14
|
},
|