@rightkit/release 0.2.163 → 0.2.165

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/disk-command.mjs CHANGED
@@ -6,7 +6,7 @@ import { fileURLToPath, pathToFileURL } from "node:url";
6
6
  import { spawnSync } from "./background-process.mjs";
7
7
  import { compareVersions } from "./rightkit-sync.mjs";
8
8
  import {
9
- DISK_EXIT_WARNING, applySweep, checkDisk, ensureScratchRoots, formatBytes, listOpenPaths, logEvent, planSweep, platformKey, purgeHoldings, readDiskPolicy,
9
+ DISK_EXIT_WARNING, applySweep, checkDisk, ensureScratchRoots, formatBytes, listOpenPaths, logEvent, planSweep, platformKey, purgeHoldings, readDiskPolicy, setWorkspaceSweepAllowed,
10
10
  } from "./disk-policy.mjs";
11
11
  import { evaluateBinding, installStableRoot, readScheduleBinding, selfCheck } from "./disk-stable-root.mjs";
12
12
 
@@ -27,7 +27,7 @@ export function renderCheck(result) {
27
27
  return lines.join("\n");
28
28
  }
29
29
 
30
- export function renderSweep(plan, applied) {
30
+ export function renderSweep(plan, applied, { all = false } = {}) {
31
31
  const lines = [`${applied ? "Swept" : "Dry run: would move"} ${plan.candidates.length} item(s), ${formatBytes(plan.totalBytes)} (stale > ${plan.staleDays} days)`];
32
32
  const rows = applied ? applied.results : plan.candidates;
33
33
  const byWriter = new Map();
@@ -35,24 +35,27 @@ export function renderSweep(plan, applied) {
35
35
  for (const [writer, items] of byWriter) {
36
36
  const total = items.reduce((s, r) => s + r.bytes, 0);
37
37
  lines.push(` ${writer.padEnd(26)} ${String(items.length).padStart(4)} item(s) ${formatBytes(total).padStart(9)}`);
38
- for (const r of [...items].sort((a, b) => b.bytes - a.bytes).slice(0, 5)) lines.push(` ${formatBytes(r.bytes).padStart(9)} ${r.path}${r.destination ? ` -> ${r.destination}` : ""}${r.ok === false ? ` FAILED: ${r.error}` : ""}`);
39
- if (items.length > 5) lines.push(` ... ${items.length - 5} more`);
38
+ for (const r of [...items].sort((a, b) => b.bytes - a.bytes).slice(0, all ? Infinity : 5)) lines.push(` ${formatBytes(r.bytes).padStart(9)} ${r.path}${r.destination ? ` -> ${r.destination}` : ""}${r.ok === false ? ` FAILED: ${r.error}` : ""}`);
39
+ if (!all && items.length > 5) lines.push(` ... ${items.length - 5} more`);
40
40
  }
41
41
  const counts = {};
42
42
  for (const s of plan.skipped) counts[s.reason] = (counts[s.reason] ?? 0) + 1;
43
+ const reportOnly = plan.skipped.filter((s) => s.reason === "report-only");
44
+ if (reportOnly.length) lines.push(` workspace-unknown is report-only on this host: ${reportOnly.length} unknown entr${reportOnly.length === 1 ? "y" : "ies"} will not be moved (opt in with: right-release disk allow-workspace-sweep)`);
43
45
  if (plan.skipped.length) lines.push(` skipped: ${Object.entries(counts).map(([k, v]) => `${v} ${k}`).join(", ")}`);
44
46
  return lines.join("\n");
45
47
  }
46
48
 
47
49
  function parse(rest) {
48
- const known = new Set(["--json", "--dry-run", "--delete", "--days", "--policy"]);
49
- const opts = { json: false, dryRun: false, deleteMode: false, days: undefined, policy: undefined };
50
+ const known = new Set(["--json", "--dry-run", "--delete", "--all", "--days", "--policy"]);
51
+ const opts = { json: false, dryRun: false, deleteMode: false, all: false, days: undefined, policy: undefined };
50
52
  for (let i = 0; i < rest.length; i += 1) {
51
53
  const a = rest[i];
52
54
  if (!known.has(a)) throw new Error(`disk: unknown option ${a}`);
53
55
  if (a === "--json") opts.json = true;
54
56
  else if (a === "--dry-run") opts.dryRun = true;
55
57
  else if (a === "--delete") opts.deleteMode = true;
58
+ else if (a === "--all") opts.all = true;
56
59
  else { const v = rest[++i]; if (!v) throw new Error(`disk: ${a} needs a value`); if (a === "--days") opts.days = Number(v); else opts.policy = v; }
57
60
  }
58
61
  if (opts.days !== undefined && !(opts.days >= 0)) throw new Error("disk: --days must be a non-negative number");
@@ -62,6 +65,9 @@ function parse(rest) {
62
65
 
63
66
  // A path inside any git checkout (a `.git` entry on an ancestor) is development code, never an installed package.
64
67
  export function insideGitCheckout(file, exists = existsSync) {
68
+ // A pnpm store is an install location even when it sits inside a workspace that is itself a git checkout (the Mac keeps
69
+ // .pnpm-store under /Volumes/D/claude).
70
+ if (file.split(/[\\/]+/).some((seg) => seg === ".pnpm-store" || seg === ".pnpm")) return false;
65
71
  let dir = path.dirname(file);
66
72
  for (;;) {
67
73
  if (exists(path.join(dir, ".git"))) return true;
@@ -243,6 +249,12 @@ export async function runDiskCommand(args = process.argv.slice(2), { env = proce
243
249
  const exitCode = schedule(rest[0], { stdout, cli: at === -1 ? undefined : rest[at + 1], platform, env });
244
250
  return { exitCode };
245
251
  }
252
+ if (command === "allow-workspace-sweep") {
253
+ const off = rest.includes("--off");
254
+ const file = setWorkspaceSweepAllowed(!off, { platform, env, ...(deps.flagHome ? { home: deps.flagHome } : {}) });
255
+ stdout(off ? `workspace-unknown sweep revoked on this host (${file}.revoked)` : `workspace-unknown sweep allowed on this host (${file}); review \`disk sweep --dry-run --all\` first, revoke with --off`);
256
+ return { exitCode: 0 };
257
+ }
246
258
  const opts = parse(rest);
247
259
  const policy = readDiskPolicy(opts.policy);
248
260
  const key = platformKey(platform);
@@ -259,9 +271,9 @@ export async function runDiskCommand(args = process.argv.slice(2), { env = proce
259
271
  if (command === "sweep") {
260
272
  if (!opts.dryRun) ensureScratchRoots({ policy, platform, env });
261
273
  const p = plan();
262
- if (opts.dryRun) { out(p, renderSweep(p)); return { exitCode: 0, plan: p }; }
274
+ if (opts.dryRun) { out(p, renderSweep(p, undefined, { all: opts.all })); return { exitCode: 0, plan: p }; }
263
275
  const applied = applySweep({ plan: p, policy, platform, deleteMode: opts.deleteMode, ...deps.apply });
264
- out({ ...p, applied }, renderSweep(p, applied));
276
+ out({ ...p, applied }, renderSweep(p, applied, { all: opts.all }));
265
277
  return { exitCode: applied.results.some((r) => !r.ok) ? 1 : 0, plan: p, applied };
266
278
  }
267
279
  if (command === "purge") {
@@ -291,7 +303,7 @@ export async function runDiskCommand(args = process.argv.slice(2), { env = proce
291
303
  if (warn) stdout(`WARNING: ${after.warnings.join("; ")}`);
292
304
  return { exitCode: !self.ok ? 2 : warn ? DISK_EXIT_WARNING : 0, before, after, applied, purged, selfCheck: self };
293
305
  }
294
- throw new Error("disk usage: check|sweep [--dry-run|--delete] [--days N]|purge [--dry-run] [--days N]|run|schedule install|uninstall|status [--json]");
306
+ throw new Error("disk usage: check|sweep [--dry-run|--delete] [--all]|allow-workspace-sweep [--off] [--days N]|purge [--dry-run] [--days N]|run|schedule install|uninstall|status [--json]");
295
307
  }
296
308
 
297
309
  if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
package/disk-policy.mjs CHANGED
@@ -160,9 +160,33 @@ export function ensureScratchRoots({ policy, platform = process.platform, env =
160
160
 
161
161
  /** Top-level entries of each workspace root that are neither tracked by the workspace repo, nested git checkouts,
162
162
  * nor on the allowlist. Symlinks are listed (`symlink: true`) but sweep never touches them. */
163
+ // Names that are always known at a workspace root, whatever the policy file says: agent instructions and agent state.
164
+ export const WORKSPACE_ALWAYS_ALLOWED = ["CLAUDE.md", "AGENTS.md", "CLAUDE.local.md", ".claude", ".codex", ".agent", ".agents", ".membrane", "cockpit", "contributions"];
165
+ const PROJECT_MARKERS = [".git", "package.json", "Cargo.toml"];
166
+
167
+ /** Host-local opt-in for moving workspace-unknown entries. Without it the target is report-only on every host. */
168
+ export function workspaceSweepFlagPath({ platform = process.platform, env = process.env, home = os.homedir() } = {}) {
169
+ if (platform === "win32" || platform === "win") {
170
+ if (!env.LOCALAPPDATA) return null;
171
+ return path.win32.join(env.LOCALAPPDATA, "Orthic Labs", "rightkit-release", "workspace-sweep-allowed");
172
+ }
173
+ return path.posix.join(home, "Library", "Application Support", "Orthic Labs", "rightkit-release", "workspace-sweep-allowed");
174
+ }
175
+ export function workspaceSweepAllowed(options = {}) {
176
+ const file = workspaceSweepFlagPath(options);
177
+ return Boolean(file) && existsSync(file);
178
+ }
179
+ export function setWorkspaceSweepAllowed(on, options = {}) {
180
+ const file = workspaceSweepFlagPath(options);
181
+ if (!file) throw new Error("disk: cannot place the host-local flag (LOCALAPPDATA is not set)");
182
+ if (on) { mkdirSync(path.dirname(file), { recursive: true }); writeFileSync(file, `workspace-unknown sweep allowed on this host ${new Date().toISOString()}\n`); }
183
+ else renameSync(file, `${file}.revoked`);
184
+ return file;
185
+ }
186
+
163
187
  export function scanWorkspaceRoots({ policy, platform = process.platform, env = process.env, home = os.homedir(), tracked = (r) => workspaceTracked(r, { platform }) } = {}) {
164
188
  const key = platformKey(platform);
165
- const allow = new Set((policy.workspaceAllowlist ?? []).map((n) => fold(n, key)));
189
+ const allow = new Set([...WORKSPACE_ALWAYS_ALLOWED, ...(policy.workspaceAllowlist ?? [])].map((n) => fold(n, key)));
166
190
  const unknown = [];
167
191
  const skipped = [];
168
192
  for (const entry of policy.workspaceRoots?.[key] ?? []) {
@@ -177,10 +201,12 @@ export function scanWorkspaceRoots({ policy, platform = process.platform, env =
177
201
  for (const name of names) {
178
202
  const folded = fold(name, key);
179
203
  if (allow.has(folded) || extra.has(folded) || trackedNames.has(folded)) continue;
204
+ if (/\.md$/i.test(name)) continue; // any markdown file at the root is documentation
180
205
  const full = path.join(root, name);
181
206
  let st;
182
207
  try { st = lstatSync(full); } catch { continue; }
183
- if (!st.isSymbolicLink() && existsSync(path.join(full, ".git"))) continue; // nested git checkout
208
+ // a nested git checkout or any project directory (own package.json / Cargo.toml) is known
209
+ if (!st.isSymbolicLink() && st.isDirectory() && PROJECT_MARKERS.some((m) => existsSync(path.join(full, m)))) continue;
184
210
  unknown.push({ root, name, path: full, symlink: st.isSymbolicLink(), mtimeMs: st.mtimeMs });
185
211
  }
186
212
  }
@@ -206,7 +232,7 @@ export function checkDisk({ policy, platform = process.platform, env = process.e
206
232
  for (const w of writers) if (w.over) warnings.push(`${w.id} holds ${formatBytes(w.bytes)} over its ${formatBytes(w.capBytes)} cap`);
207
233
  const scanned = scan({ policy, platform, env, home });
208
234
  const unknown = scanned.unknown.map((u) => ({ root: u.root, name: u.name, path: u.path, symlink: u.symlink, bytes: u.symlink ? 0 : size(u.path), ageDays: Math.max(0, Math.floor((now - u.mtimeMs) / 86400_000)) }));
209
- for (const u of unknown) warnings.push(`unknown top-level entry ${u.path} (${u.symlink ? "symlink" : formatBytes(u.bytes)}, ${u.ageDays}d old): track it, add it to workspaceAllowlist, or let sweep move it`);
235
+ for (const u of unknown) warnings.push(`unknown top-level entry ${u.path} (${u.symlink ? "symlink" : formatBytes(u.bytes)}, ${u.ageDays}d old): track it or add it to workspaceAllowlist (sweep only moves it on a host that ran \`disk allow-workspace-sweep\`)`);
210
236
  for (const s of scanned.skipped) warnings.push(`workspace root ${s.path}: tracked paths cannot be read from git, so unknown entries are not evaluated or swept`);
211
237
  return { schema: 1, platform: key, floor, writers, unknown, warnings, warn: warnings.length > 0 };
212
238
  }
@@ -272,7 +298,7 @@ export function gitTracksPath(target) {
272
298
  return String(listed.stdout).length > 0;
273
299
  }
274
300
 
275
- export function planSweep({ policy, platform = process.platform, env = process.env, home = os.homedir(), now = Date.now(), staleDays, openPaths, inUse, tracked = gitTracksPath, size = (p) => pathSize(p, { platform }), scan = scanWorkspaceRoots } = {}) {
301
+ export function planSweep({ policy, platform = process.platform, env = process.env, home = os.homedir(), now = Date.now(), staleDays, openPaths, inUse, tracked = gitTracksPath, size = (p) => pathSize(p, { platform }), scan = scanWorkspaceRoots, workspaceSweep = workspaceSweepAllowed({ platform, env, home }) } = {}) {
276
302
  const key = platformKey(platform);
277
303
  const effectiveDays = staleDays ?? policy.staleDays;
278
304
  const candidates = [];
@@ -326,7 +352,11 @@ export function planSweep({ policy, platform = process.platform, env = process.e
326
352
  if (lockCheck(unit)) { skipped.push({ ...base, reason: "in-use" }); return; }
327
353
  candidates.push({ ...base, root, bytes: size(unit), mtime: new Date(st.mtimeMs).toISOString() });
328
354
  };
329
- for (const u of scanned.unknown) consider("workspace-unknown", u.path, u.root, unknownCutoff);
355
+ // Report-only unless this host opted in (`disk allow-workspace-sweep`): unknown entries are warned about, never moved.
356
+ for (const u of scanned.unknown) {
357
+ if (!workspaceSweep) { skipped.push({ writer: "workspace-unknown", path: u.path, reason: "report-only" }); continue; }
358
+ consider("workspace-unknown", u.path, u.root, unknownCutoff);
359
+ }
330
360
  // Scratch roots: every child older than 3 days and idle (the same window).
331
361
  for (const root of scratchRoots(policy, { platform, env, home })) {
332
362
  let rst;
@@ -429,7 +459,7 @@ export function applySweep({ plan, policy, platform = process.platform, deleteMo
429
459
  try {
430
460
  // Re-verify at apply time: still the planned unit, not a symlink, still inside its root.
431
461
  if (!c.root || lstatSync(c.path).isSymbolicLink() || !isInsideRoot(c.path, c.root)) throw new Error("refused: path is not a sweepable unit inside its target root");
432
- if (c.writer === "workspace-unknown" && existsSync(path.join(c.path, ".git"))) throw new Error("refused: a nested git checkout is never swept");
462
+ if (c.writer === "workspace-unknown" && PROJECT_MARKERS.some((m) => existsSync(path.join(c.path, m)))) throw new Error("refused: a nested git checkout or project directory is never swept");
433
463
  if (deleteMode) { remove(c.path); results.push({ ...c, action: "delete", ok: true }); logEvent(logFile, { event: "disk.sweep.delete", ...c }); continue; }
434
464
  // An item stays on its own volume: moving it elsewhere frees nothing on the volume that is short of space.
435
465
  const holding = pick(c.path, { policy, platform, env, home, date });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rightkit/release",
3
- "version": "0.2.163",
3
+ "version": "0.2.165",
4
4
  "description": "Portable release CLI/SDK with fail-closed public GitHub Releases and private R2 + RightApps distribution.",
5
5
  "license": "MIT OR Apache-2.0",
6
6
  "type": "module",
package/release.mjs CHANGED
@@ -51,7 +51,8 @@ const SIGN_MACOS = path.resolve(TOOL_ROOT, "sign-macos.mjs");
51
51
  const NOTARIZE_MACOS = path.resolve(TOOL_ROOT, "notarize-macos.mjs");
52
52
  const SIGN_UPDATER = path.resolve(TOOL_ROOT, "sign-updater.mjs");
53
53
  const RIGHTKIT_VERSIONS = JSON.parse(readFileSync(path.resolve(TOOL_ROOT, "rightkit-versions.json"), "utf8"));
54
- const VERSION = RIGHTKIT_VERSIONS.stagedNpm?.["@rightkit/release"] ?? RIGHTKIT_VERSIONS.npm["@rightkit/release"];
54
+ // The running package's own version: the bundled feed lags by one release (it is written after the publish).
55
+ const VERSION = JSON.parse(readFileSync(path.resolve(TOOL_ROOT, "package.json"), "utf8")).version;
55
56
  const TIERS = new Set(["patch", "update"]);
56
57
  const RIGHTKIT_ALLOWED = buildAllowedVersions(
57
58
  RIGHTKIT_VERSIONS.npm,
@@ -31,7 +31,7 @@
31
31
  "@rightkit/qa": "0.4.8",
32
32
  "@rightkit/qa-chrome": "0.1.6",
33
33
  "@rightkit/react-shell": "0.6.4",
34
- "@rightkit/release": "0.2.162",
34
+ "@rightkit/release": "0.2.164",
35
35
  "@rightkit/settings-ui": "0.4.4",
36
36
  "@rightkit/tauri": "0.2.1",
37
37
  "@rightkit/tauri-shell": "0.1.1",
@@ -203,7 +203,9 @@
203
203
  "0.2.158",
204
204
  "0.2.159",
205
205
  "0.2.160",
206
- "0.2.161"
206
+ "0.2.161",
207
+ "0.2.162",
208
+ "0.2.163"
207
209
  ],
208
210
  "@rightkit/qa": [
209
211
  "0.1.0",
@@ -393,6 +395,7 @@
393
395
  "rightkit-playback": "0.2.1",
394
396
  "rightkit-qa": "0.4.0",
395
397
  "rightkit-qa-driver": "0.4.0",
398
+ "rightkit-record": "0.3.0",
396
399
  "rightkit-review": "0.2.0",
397
400
  "rightkit-sandbox": "0.4.0",
398
401
  "rightkit-search": "0.2.1",
@@ -471,6 +474,7 @@
471
474
  "rightkit-qts": "0.2.6",
472
475
  "rightkit-qts-ggml": "0.1.2",
473
476
  "rightkit-qts-ggml-sys": "0.1.2",
477
+ "rightkit-record": "0.3.0",
474
478
  "rightkit-review": "0.2.0",
475
479
  "rightkit-sandbox": "0.4.0",
476
480
  "rightkit-search": "0.2.1",
@@ -513,7 +517,7 @@
513
517
  "@rightkit/qa": "0.4.8",
514
518
  "@rightkit/qa-chrome": "0.1.6",
515
519
  "@rightkit/react-shell": "0.6.4",
516
- "@rightkit/release": "0.2.162",
520
+ "@rightkit/release": "0.2.164",
517
521
  "@rightkit/settings-ui": "0.4.4",
518
522
  "@rightkit/shell": "0.1.1",
519
523
  "@rightkit/tauri": "0.2.1",