@osdlc/repo-cleaner 0.9.2 → 0.10.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.
@@ -1,2 +1,2 @@
1
1
  /** This package's version, from its package.json when it was built. */
2
- export declare const VERSION = "0.9.2";
2
+ export declare const VERSION = "0.10.1";
@@ -1,3 +1,3 @@
1
1
  // Generated by cli-tool-stamp-version from package.json. Do not edit.
2
2
  /** This package's version, from its package.json when it was built. */
3
- export const VERSION = '0.9.2';
3
+ export const VERSION = '0.10.1';
@@ -26,7 +26,7 @@ export declare function hasBlockers(item: Pick<SweepItem, 'facts'>): boolean;
26
26
  * The branch set is returned because a local branch checked out in a worktree
27
27
  * must not be judged on its own: the worktree is what a person would act on.
28
28
  */
29
- export declare function gatherWorktrees(repo: RepoInfo, prs: Map<string, PRInfo>, cwd: string, git: Git, only: string | undefined, probe: ProcessProbe): [SweepItem[], Set<string>];
29
+ export declare function gatherWorktrees(repo: RepoInfo, prs: Map<string, PRInfo>, cwd: string, git: Git, only: string | undefined, probe: ProcessProbe, tick?: (done: number, total: number) => void): [SweepItem[], Set<string>];
30
30
  export declare function classifyWorktree(opts: {
31
31
  path: string;
32
32
  branch: string;
package/dist/worktrees.js CHANGED
@@ -9,7 +9,7 @@ import { existsSync, statSync } from 'node:fs';
9
9
  import { isAbsolute, join, sep } from 'node:path';
10
10
  import { shellJoin } from '@osdlc/command-seam';
11
11
  import { nodeGuard } from '@osdlc/path-guard/node';
12
- import { gapSentence, recordPr } from "./branches.js";
12
+ import { gapSentence, mergedFact, recordPr } from "./branches.js";
13
13
  import { confirmationNote, judgeLanding, landingConfirmed, recordLanding, unmatchedSentence, } from "./landing.js";
14
14
  import { busyHolders, isIdleHolder, parseLockOwner } from "./processes.js";
15
15
  import { authorizationsFor, landedRef, makeItem, } from "./types.js";
@@ -141,7 +141,7 @@ function readBlockers(opts) {
141
141
  * The branch set is returned because a local branch checked out in a worktree
142
142
  * must not be judged on its own: the worktree is what a person would act on.
143
143
  */
144
- export function gatherWorktrees(repo, prs, cwd, git, only, probe) {
144
+ export function gatherWorktrees(repo, prs, cwd, git, only, probe, tick) {
145
145
  const all = git.at(repo.root).worktrees();
146
146
  const pathsByBranch = new Map();
147
147
  for (const entry of all) {
@@ -151,7 +151,8 @@ export function gatherWorktrees(repo, prs, cwd, git, only, probe) {
151
151
  }
152
152
  const entries = only === undefined ? all : all.filter((entry) => entry.branch === only);
153
153
  const items = [];
154
- for (const entry of entries) {
154
+ for (const [i, entry] of entries.entries()) {
155
+ tick?.(i, entries.length);
155
156
  const branch = entry.branch ?? '';
156
157
  items.push(classifyWorktree({
157
158
  path: entry.path,
@@ -202,8 +203,13 @@ export function classifyWorktree(opts) {
202
203
  if (nodeGuard.contains(path, absCwd)) {
203
204
  return pinned("current session's worktree");
204
205
  }
205
- const onDisk = isDir(path);
206
+ // A directory whose `.git` is gone is no longer a checkout: git lists it as
207
+ // prunable and `git status` inside it fails. Treat it as gone; the prune
208
+ // that drops its registration leaves the directory alone.
209
+ const dirPresent = isDir(path);
210
+ const onDisk = dirPresent && existsSync(join(path, '.git'));
206
211
  facts['on_disk'] = onDisk;
212
+ facts['directory_left'] = dirPresent && !onDisk;
207
213
  let dirtyFiles = [];
208
214
  let unpushed = [];
209
215
  if (onDisk) {
@@ -244,6 +250,14 @@ export function classifyWorktree(opts) {
244
250
  recordLanding(facts, patch);
245
251
  const patchLanded = patch !== null && patch.check !== null;
246
252
  const unlanded = unpushed.length > 0 && !landed && !landingConfirmed(patch);
253
+ if (onDisk && head !== null) {
254
+ // `soft`: a missing default ref reads as not merged.
255
+ facts['merged'] = mergedFact({
256
+ landed,
257
+ patchLanded,
258
+ ancestor: git.at(repo.root).soft.isAncestor(head, landedRef(repo)),
259
+ });
260
+ }
247
261
  const requires = authorizationsFor({
248
262
  'abandon-unmerged': unlanded,
249
263
  'discard-changes': dirtyFiles.length > 0,
@@ -280,7 +294,9 @@ export function classifyWorktree(opts) {
280
294
  id: path,
281
295
  classification: 'safe-to-delete',
282
296
  action: 'remove',
283
- reason: 'worktree directory no longer on disk; prune the stale link',
297
+ reason: dirPresent
298
+ ? 'directory is no longer a checkout (no .git); prune the stale link, the directory stays'
299
+ : 'worktree directory no longer on disk; prune the stale link',
284
300
  facts,
285
301
  commands: pruneOnly,
286
302
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@osdlc/repo-cleaner",
3
- "version": "0.9.2",
3
+ "version": "0.10.1",
4
4
  "description": "What has been merged into the default branch, and what can go away. Inventories local branches, worktrees and remote branches across one repository or many, classifying each with the evidence behind the verdict and the command that would act on it. Plans and applies approved deletes, compare-and-swap.",
5
5
  "license": "MIT OR Apache-2.0",
6
6
  "author": "sksizer",
@@ -50,11 +50,11 @@
50
50
  "dependencies": {
51
51
  "zod": "4.4.3",
52
52
  "@osdlc/cli-tool": "^0.12.0",
53
- "@osdlc/command-seam": "^0.14.0",
54
- "@osdlc/config-layers": "^0.8.0",
55
53
  "@osdlc/path-guard": "^0.3.1",
56
- "@osdlc/typed-gh": "^0.14.0",
57
- "@osdlc/typed-git": "^0.11.1"
54
+ "@osdlc/command-seam": "^0.14.0",
55
+ "@osdlc/typed-git": "^0.12.0",
56
+ "@osdlc/typed-gh": "^0.14.1",
57
+ "@osdlc/ux": "^0.1.1"
58
58
  },
59
59
  "devDependencies": {
60
60
  "@types/node": "^26.1.0",
@@ -63,8 +63,8 @@
63
63
  "prettier": "3.9.6",
64
64
  "typescript": "~6.0.3",
65
65
  "vitest": "^5.0.5",
66
- "@osdlc/prettier-config": "^0.3.1",
67
- "@osdlc/tsconfig": "^0.4.1"
66
+ "@osdlc/tsconfig": "^0.4.1",
67
+ "@osdlc/prettier-config": "^0.3.1"
68
68
  },
69
69
  "scripts": {
70
70
  "build": "cli-tool-stamp-version && tsc -p tsconfig.build.json",
package/dist/config.d.ts DELETED
@@ -1,53 +0,0 @@
1
- /**
2
- * The standalone tool's own configuration file.
3
- *
4
- * Only the CLI reads it. The library takes repositories as an argument, so a
5
- * host that already knows where its repositories are passes them in and never touches this file.
6
- *
7
- * Shape, location and loading are `@osdlc/config-layers`'s machine file:
8
- * `<config-home>/repo-cleaner/config.yaml` over `@osdlc/tiered-dirs`'
9
- * standalone tier, Zod as the single source of truth, never throws, degrades
10
- * to the defaults, memoised. Standalone, not suite or vendor, because a
11
- * general tool must not write a company namespace onto someone else's
12
- * machine.
13
- */
14
- import { z } from 'zod';
15
- /** The tool's name, and so its config directory segment. */
16
- export declare const TOOL = "repo-cleaner";
17
- /** Env var that relocates the config file, for tests and one-off runs. */
18
- export declare const CONFIG_ENV = "REPO_CLEANER_MACHINE_CONFIG";
19
- export declare const RepoCleanerConfigSchema: z.ZodObject<{
20
- paths: z.ZodDefault<z.ZodRecord<z.ZodString, z.ZodString>>;
21
- repos: z.ZodDefault<z.ZodArray<z.ZodString>>;
22
- }, z.core.$strict>;
23
- export type RepoCleanerConfig = z.infer<typeof RepoCleanerConfigSchema>;
24
- /**
25
- * Whose directories the config file, plans and receipts live under: vendor
26
- * `repo-cleaner`, moved by the `REPO_CLEANER_*` override variables.
27
- */
28
- export declare const VENDOR_DIRS: {
29
- vendor: string;
30
- envVars: import("@osdlc/tiered-dirs").EnvVars;
31
- };
32
- export declare const DEFAULT_CONFIG: RepoCleanerConfig;
33
- /** `<config-home>/repo-cleaner/config.yaml`, unless {@link CONFIG_ENV} says otherwise. */
34
- export declare const configPath: () => string;
35
- /**
36
- * The config, defaults filled.
37
- *
38
- * Never throws. A missing file, unreadable file, malformed YAML or wrong
39
- * shape all degrade to the defaults: a person running the tool in a
40
- * repository should not be stopped by a config file they never wrote.
41
- */
42
- export declare const loadConfig: () => RepoCleanerConfig;
43
- /** Drop the memoised value. Call from tests between runs. */
44
- export declare const clearConfigCache: () => void;
45
- /**
46
- * The configured entries with their `{name}` path names substituted against
47
- * `paths:`, relative entries resolved against `baseDir`.
48
- *
49
- * A name nothing defines is an error rather than an empty string — a
50
- * `repos:` entry that quietly becomes `/dev` would sweep whatever happens to
51
- * be there.
52
- */
53
- export declare function configuredRepos(config?: RepoCleanerConfig, baseDir?: string): string[];
package/dist/config.js DELETED
@@ -1,67 +0,0 @@
1
- /**
2
- * The standalone tool's own configuration file.
3
- *
4
- * Only the CLI reads it. The library takes repositories as an argument, so a
5
- * host that already knows where its repositories are passes them in and never touches this file.
6
- *
7
- * Shape, location and loading are `@osdlc/config-layers`'s machine file:
8
- * `<config-home>/repo-cleaner/config.yaml` over `@osdlc/tiered-dirs`'
9
- * standalone tier, Zod as the single source of truth, never throws, degrades
10
- * to the defaults, memoised. Standalone, not suite or vendor, because a
11
- * general tool must not write a company namespace onto someone else's
12
- * machine.
13
- */
14
- import { envVars } from '@osdlc/cli-tool';
15
- import { machineConfigFile, pathNames, pathsSchema, substitutePathNames, } from '@osdlc/config-layers';
16
- import { z } from 'zod';
17
- /** The tool's name, and so its config directory segment. */
18
- export const TOOL = 'repo-cleaner';
19
- /** Env var that relocates the config file, for tests and one-off runs. */
20
- export const CONFIG_ENV = 'REPO_CLEANER_MACHINE_CONFIG';
21
- export const RepoCleanerConfigSchema = z
22
- .object({
23
- paths: pathsSchema()
24
- .default({})
25
- .describe('Named directories; a `repos:` entry refers to one as `{name}`.'),
26
- repos: z
27
- .array(z.string().trim().min(1))
28
- .default([])
29
- .describe('Checkouts, or directories containing checkouts at any depth.'),
30
- })
31
- .strict();
32
- /**
33
- * Whose directories the config file, plans and receipts live under: vendor
34
- * `repo-cleaner`, moved by the `REPO_CLEANER_*` override variables.
35
- */
36
- export const VENDOR_DIRS = { vendor: TOOL, envVars: envVars('REPO_CLEANER') };
37
- const file = machineConfigFile({
38
- tool: TOOL,
39
- schema: RepoCleanerConfigSchema,
40
- vendorDirs: VENDOR_DIRS,
41
- env: CONFIG_ENV,
42
- });
43
- export const DEFAULT_CONFIG = RepoCleanerConfigSchema.parse({});
44
- /** `<config-home>/repo-cleaner/config.yaml`, unless {@link CONFIG_ENV} says otherwise. */
45
- export const configPath = () => file.path();
46
- /**
47
- * The config, defaults filled.
48
- *
49
- * Never throws. A missing file, unreadable file, malformed YAML or wrong
50
- * shape all degrade to the defaults: a person running the tool in a
51
- * repository should not be stopped by a config file they never wrote.
52
- */
53
- export const loadConfig = () => file.load();
54
- /** Drop the memoised value. Call from tests between runs. */
55
- export const clearConfigCache = () => file.clearCache();
56
- /**
57
- * The configured entries with their `{name}` path names substituted against
58
- * `paths:`, relative entries resolved against `baseDir`.
59
- *
60
- * A name nothing defines is an error rather than an empty string — a
61
- * `repos:` entry that quietly becomes `/dev` would sweep whatever happens to
62
- * be there.
63
- */
64
- export function configuredRepos(config = loadConfig(), baseDir = process.cwd()) {
65
- const names = pathNames(baseDir, config.paths, {});
66
- return config.repos.map((entry) => substitutePathNames(entry, names, 'repos:'));
67
- }