@rightkit/release 0.2.164 → 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");
@@ -246,6 +249,12 @@ export async function runDiskCommand(args = process.argv.slice(2), { env = proce
246
249
  const exitCode = schedule(rest[0], { stdout, cli: at === -1 ? undefined : rest[at + 1], platform, env });
247
250
  return { exitCode };
248
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
+ }
249
258
  const opts = parse(rest);
250
259
  const policy = readDiskPolicy(opts.policy);
251
260
  const key = platformKey(platform);
@@ -262,9 +271,9 @@ export async function runDiskCommand(args = process.argv.slice(2), { env = proce
262
271
  if (command === "sweep") {
263
272
  if (!opts.dryRun) ensureScratchRoots({ policy, platform, env });
264
273
  const p = plan();
265
- 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 }; }
266
275
  const applied = applySweep({ plan: p, policy, platform, deleteMode: opts.deleteMode, ...deps.apply });
267
- out({ ...p, applied }, renderSweep(p, applied));
276
+ out({ ...p, applied }, renderSweep(p, applied, { all: opts.all }));
268
277
  return { exitCode: applied.results.some((r) => !r.ok) ? 1 : 0, plan: p, applied };
269
278
  }
270
279
  if (command === "purge") {
@@ -294,7 +303,7 @@ export async function runDiskCommand(args = process.argv.slice(2), { env = proce
294
303
  if (warn) stdout(`WARNING: ${after.warnings.join("; ")}`);
295
304
  return { exitCode: !self.ok ? 2 : warn ? DISK_EXIT_WARNING : 0, before, after, applied, purged, selfCheck: self };
296
305
  }
297
- 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]");
298
307
  }
299
308
 
300
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.164",
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.163",
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",
@@ -204,7 +204,8 @@
204
204
  "0.2.159",
205
205
  "0.2.160",
206
206
  "0.2.161",
207
- "0.2.162"
207
+ "0.2.162",
208
+ "0.2.163"
208
209
  ],
209
210
  "@rightkit/qa": [
210
211
  "0.1.0",
@@ -516,7 +517,7 @@
516
517
  "@rightkit/qa": "0.4.8",
517
518
  "@rightkit/qa-chrome": "0.1.6",
518
519
  "@rightkit/react-shell": "0.6.4",
519
- "@rightkit/release": "0.2.163",
520
+ "@rightkit/release": "0.2.164",
520
521
  "@rightkit/settings-ui": "0.4.4",
521
522
  "@rightkit/shell": "0.1.1",
522
523
  "@rightkit/tauri": "0.2.1",