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 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/). Before 1.0, a minor version may
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
- ## [Unreleased]
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 is at version 0.4. Until 1.0, a minor release may contain
35
- > breaking changes.
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:`. It replaces the `flagrm-verify` skill and prompt file of earlier
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
- program.parseAsync().catch((err) => {
386
- if (!(err instanceof CliError))
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
- console.error(pc.red(err.message));
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, LEGACY_SKILLS, SKILLS } from "./install.js";
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. Renamed skills count too: `update` deletes them, possibly mid-removal.
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) => [...SKILLS, ...Object.keys(LEGACY_SKILLS)].map((skill) => `${dir}/${skill}/**`)),
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);
@@ -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 ["frontend", "backend"])
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`; the legacy top-level keys override the built-ins. */
259
- function adapterDefaults(adapter, file) {
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: frontend, attributes: [] };
260
+ return { methods: DEFAULT_FRONTEND_METHODS, attributes: [] };
265
261
  if (adapter === "dotnet")
266
- return { methods: backend, attributes };
267
- return { methods: [...new Set([...frontend, ...backend])], attributes };
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, file, fileDir, quiet = false) {
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, file);
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 `projects:` plus the legacy `frontend:`/`backend:` keys,
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, file, fileDir, cli.quiet));
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, or pass --frontend and/or --backend.");
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);
@@ -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
- * simpler/looser than the scan code path.
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";
@@ -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, LEGACY_SKILLS, SKILLS, stopHookState, TOOL_SKILL_DIRS } from "./install.js";
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")
@@ -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. The only deletion: `update` replaces a renamed skill that flagrm
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 && !replaced.has(skill))
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 && !replaced.has(skill))
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]) {
@@ -137,7 +137,6 @@ const STEP_STYLE = {
137
137
  updated: pc.green,
138
138
  unchanged: pc.dim,
139
139
  skipped: pc.dim,
140
- deleted: pc.yellow,
141
140
  };
142
141
  /** One line per `init`/`update` step: `created .claude/skills/flagrm-remove`. */
143
142
  export function printSteps(steps) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "flagrm",
3
- "version": "0.5.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://github.com/dino-cosic/flagrm#readme",
11
+ "homepage": "https://flagrm.com/",
12
12
  "bugs": {
13
13
  "url": "https://github.com/dino-cosic/flagrm/issues"
14
14
  },