@rightkit/release 0.2.162 → 0.2.164
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/cli/right-release.mjs +2 -2
- package/disk-command.mjs +73 -24
- package/disk-policy.json +10 -0
- package/disk-policy.mjs +108 -4
- package/disk-stable-root.mjs +168 -0
- package/package.json +1 -1
- package/rightkit-versions.json +15 -8
- package/runtime-budget-classes.json +4 -0
package/cli/right-release.mjs
CHANGED
|
@@ -247,8 +247,8 @@ Commands:
|
|
|
247
247
|
disk check [--json] Per-writer sizes vs caps and free-space floor (exit 3 on warning)
|
|
248
248
|
disk sweep [--dry-run] [--days N] [--delete] [--json]
|
|
249
249
|
Move stale idle heavy data to the heavy volume (delete only with --delete)
|
|
250
|
-
disk run check, dry-run sweep, move sweep, warn (the scheduled job)
|
|
251
|
-
disk schedule install|uninstall|status Daily launchd job (Mac) / hidden Task Scheduler entry (Windows)
|
|
250
|
+
disk run self-check, check, dry-run sweep, move sweep, warn (the scheduled job)
|
|
251
|
+
disk schedule install|uninstall|status Daily launchd job (Mac) / hidden Task Scheduler entry (Windows), bound to the stable per-user copy
|
|
252
252
|
deps --check|--audit|--update Shared dependency lane
|
|
253
253
|
hardening <artifact...> Run the Right Suite hardening scan
|
|
254
254
|
lsclean <AppName.app> Clear macOS LaunchServices duplicates
|
package/disk-command.mjs
CHANGED
|
@@ -6,8 +6,9 @@ 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, formatBytes, listOpenPaths, logEvent, planSweep, platformKey, purgeHoldings, readDiskPolicy,
|
|
9
|
+
DISK_EXIT_WARNING, applySweep, checkDisk, ensureScratchRoots, formatBytes, listOpenPaths, logEvent, planSweep, platformKey, purgeHoldings, readDiskPolicy,
|
|
10
10
|
} from "./disk-policy.mjs";
|
|
11
|
+
import { evaluateBinding, installStableRoot, readScheduleBinding, selfCheck } from "./disk-stable-root.mjs";
|
|
11
12
|
|
|
12
13
|
export const LAUNCHD_LABEL = "dev.rightkit.disk-hygiene";
|
|
13
14
|
export const WINDOWS_TASK = "RightKit Disk Hygiene";
|
|
@@ -18,6 +19,10 @@ export function renderCheck(result) {
|
|
|
18
19
|
const size = w.error ? `error: ${w.error}` : w.matches === 0 ? "absent" : `${formatBytes(w.bytes)} / cap ${formatBytes(w.capBytes)}`;
|
|
19
20
|
lines.push(` ${w.id.padEnd(28)} ${w.action.padEnd(6)} ${size}${w.over ? " OVER CAP" : ""}`);
|
|
20
21
|
}
|
|
22
|
+
if (result.unknown?.length) {
|
|
23
|
+
lines.push(`Unknown top-level entries (${result.unknown.length}):`);
|
|
24
|
+
for (const u of result.unknown) lines.push(` ${(u.symlink ? "symlink" : formatBytes(u.bytes)).padStart(9)} ${String(u.ageDays).padStart(4)}d ${u.path}`);
|
|
25
|
+
}
|
|
21
26
|
for (const w of result.warnings) lines.push(`WARNING: ${w}`);
|
|
22
27
|
return lines.join("\n");
|
|
23
28
|
}
|
|
@@ -57,6 +62,9 @@ function parse(rest) {
|
|
|
57
62
|
|
|
58
63
|
// A path inside any git checkout (a `.git` entry on an ancestor) is development code, never an installed package.
|
|
59
64
|
export function insideGitCheckout(file, exists = existsSync) {
|
|
65
|
+
// A pnpm store is an install location even when it sits inside a workspace that is itself a git checkout (the Mac keeps
|
|
66
|
+
// .pnpm-store under /Volumes/D/claude).
|
|
67
|
+
if (file.split(/[\\/]+/).some((seg) => seg === ".pnpm-store" || seg === ".pnpm")) return false;
|
|
60
68
|
let dir = path.dirname(file);
|
|
61
69
|
for (;;) {
|
|
62
70
|
if (exists(path.join(dir, ".git"))) return true;
|
|
@@ -162,48 +170,81 @@ Register-ScheduledTask -TaskName '${WINDOWS_TASK}' -Action $a -Trigger $t -Setti
|
|
|
162
170
|
return { platform: "win", script };
|
|
163
171
|
}
|
|
164
172
|
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
173
|
+
const here = path.dirname(fileURLToPath(import.meta.url));
|
|
174
|
+
|
|
175
|
+
/** CLI to copy into the stable root: --cli / RIGHT_RELEASE_INSTALLED_CLI, else this very package when it is not a checkout
|
|
176
|
+
* (the usual `right-release disk schedule install` from the installed package), else the discovered installed package. */
|
|
177
|
+
export function sourceCli({ explicit, env = process.env, self = path.join(here, "cli", "right-release.mjs"), resolve = resolveInstalledCli } = {}) {
|
|
178
|
+
if (explicit || env.RIGHT_RELEASE_INSTALLED_CLI) return resolve({ explicit, env });
|
|
179
|
+
let real = self;
|
|
180
|
+
try { real = realpathSync(self); } catch { /* keep lexical */ }
|
|
181
|
+
if (existsSync(self) && !insideGitCheckout(real)) return self;
|
|
182
|
+
return resolve({ explicit, env });
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
function scheduleStatus({ platform, stdout, env = process.env, home = os.homedir() }) {
|
|
186
|
+
let binding = null;
|
|
187
|
+
let loaded = false;
|
|
188
|
+
try { binding = readScheduleBinding({ platform, home }); } catch (error) { stdout(`status: cannot read the schedule (${error.message})`); return 1; }
|
|
189
|
+
if (platform === "darwin") {
|
|
190
|
+
const r = spawnSync("launchctl", ["print", `gui/${process.getuid()}/${LAUNCHD_LABEL}`], { encoding: "utf8" });
|
|
191
|
+
loaded = r.status === 0;
|
|
192
|
+
} else loaded = Boolean(binding);
|
|
193
|
+
if (!binding) { stdout("not installed"); return 0; }
|
|
194
|
+
const verdict = evaluateBinding(binding, { platform, env, home });
|
|
195
|
+
stdout(`${loaded ? "installed" : "plist present but not loaded"} (${binding.source})`);
|
|
196
|
+
stdout(` target: ${binding.cli ?? "(none)"}`);
|
|
197
|
+
if (verdict.ok) { stdout(" binding ok: stable root, target present"); return loaded ? 0 : 1; }
|
|
198
|
+
for (const p of verdict.problems) stdout(` PROBLEM: ${p}`);
|
|
199
|
+
return 1;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
function schedule(sub, { stdout, cli, platform = process.platform, env = process.env }) {
|
|
203
|
+
if (!["install", "uninstall", "status"].includes(sub)) throw new Error("disk schedule install|uninstall|status");
|
|
204
|
+
if (sub === "status") return scheduleStatus({ platform, stdout, env });
|
|
205
|
+
const spec = scheduleSpec({ platform });
|
|
168
206
|
if (platform === "darwin") {
|
|
169
207
|
const uid = String(process.getuid());
|
|
170
208
|
if (sub === "install") {
|
|
171
|
-
|
|
209
|
+
const installed = installStableRoot({ cli: sourceCli({ explicit: cli, env }), platform, env });
|
|
210
|
+
stdout(`${installed.unchanged ? "kept" : "installed"} @rightkit/release ${installed.version ?? "?"} in ${installed.stable}`);
|
|
211
|
+
const bound = scheduleSpec({ platform, cli: installed.cli });
|
|
212
|
+
mkdirSync(path.dirname(bound.file), { recursive: true });
|
|
172
213
|
mkdirSync("/Volumes/D/claude/tmp/swept", { recursive: true });
|
|
173
|
-
|
|
214
|
+
ensureScratchRoots({ policy: readDiskPolicy(), platform, env });
|
|
215
|
+
writeFileSync(bound.file, bound.plist);
|
|
174
216
|
spawnSync("launchctl", ["bootout", `gui/${uid}/${LAUNCHD_LABEL}`], { encoding: "utf8" });
|
|
175
|
-
const r = spawnSync("launchctl", ["bootstrap", `gui/${uid}`,
|
|
217
|
+
const r = spawnSync("launchctl", ["bootstrap", `gui/${uid}`, bound.file], { encoding: "utf8" });
|
|
176
218
|
if (r.status !== 0) throw new Error(`launchctl bootstrap failed: ${r.stderr}`);
|
|
177
|
-
stdout(`installed ${
|
|
178
|
-
} else
|
|
219
|
+
stdout(`installed ${bound.file} (daily 03:30), bound to ${installed.cli}`);
|
|
220
|
+
} else {
|
|
179
221
|
spawnSync("launchctl", ["bootout", `gui/${uid}/${LAUNCHD_LABEL}`], { encoding: "utf8" });
|
|
180
222
|
if (existsSync(spec.file)) unlinkSync(spec.file);
|
|
181
223
|
stdout("uninstalled");
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
stdout(r.status === 0 ? `installed (${spec.file})` : "not installed");
|
|
185
|
-
} else throw new Error("disk schedule install|uninstall|status");
|
|
186
|
-
return;
|
|
224
|
+
}
|
|
225
|
+
return 0;
|
|
187
226
|
}
|
|
188
227
|
if (sub === "install") {
|
|
189
|
-
const
|
|
228
|
+
const installed = installStableRoot({ cli: sourceCli({ explicit: cli, env }), platform, env });
|
|
229
|
+
stdout(`${installed.unchanged ? "kept" : "installed"} @rightkit/release ${installed.version ?? "?"} in ${installed.stable}`);
|
|
230
|
+
ensureScratchRoots({ policy: readDiskPolicy(), platform, env });
|
|
231
|
+
const bound = scheduleSpec({ platform, cli: installed.cli });
|
|
232
|
+
const r = spawnSync("powershell.exe", ["-NoProfile", "-NonInteractive", "-WindowStyle", "Hidden", "-Command", bound.script], { encoding: "utf8" });
|
|
190
233
|
if (r.status !== 0) throw new Error(`Register-ScheduledTask failed: ${r.stderr || r.stdout}`);
|
|
191
|
-
stdout(`installed scheduled task "${WINDOWS_TASK}" (daily 03:30, hidden)`);
|
|
192
|
-
} else
|
|
234
|
+
stdout(`installed scheduled task "${WINDOWS_TASK}" (daily 03:30, hidden), bound to ${installed.cli}`);
|
|
235
|
+
} else {
|
|
193
236
|
spawnSync("powershell.exe", ["-NoProfile", "-NonInteractive", "-Command", `Unregister-ScheduledTask -TaskName '${WINDOWS_TASK}' -Confirm:$false`], { encoding: "utf8" });
|
|
194
237
|
stdout("uninstalled");
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
stdout(r.status === 0 ? `installed (${String(r.stdout).trim()})` : "not installed");
|
|
198
|
-
} else throw new Error("disk schedule install|uninstall|status");
|
|
238
|
+
}
|
|
239
|
+
return 0;
|
|
199
240
|
}
|
|
200
241
|
|
|
201
242
|
export async function runDiskCommand(args = process.argv.slice(2), { env = process.env, stdout = console.log, platform = process.platform, deps = {} } = {}) {
|
|
202
243
|
const [command, ...rest] = args;
|
|
203
244
|
if (command === "schedule") {
|
|
204
245
|
const at = rest.indexOf("--cli");
|
|
205
|
-
schedule(rest[0], { stdout, cli: at === -1 ? undefined : rest[at + 1] });
|
|
206
|
-
return { exitCode
|
|
246
|
+
const exitCode = schedule(rest[0], { stdout, cli: at === -1 ? undefined : rest[at + 1], platform, env });
|
|
247
|
+
return { exitCode };
|
|
207
248
|
}
|
|
208
249
|
const opts = parse(rest);
|
|
209
250
|
const policy = readDiskPolicy(opts.policy);
|
|
@@ -219,6 +260,7 @@ export async function runDiskCommand(args = process.argv.slice(2), { env = proce
|
|
|
219
260
|
return { exitCode: result.warn ? DISK_EXIT_WARNING : 0, result };
|
|
220
261
|
}
|
|
221
262
|
if (command === "sweep") {
|
|
263
|
+
if (!opts.dryRun) ensureScratchRoots({ policy, platform, env });
|
|
222
264
|
const p = plan();
|
|
223
265
|
if (opts.dryRun) { out(p, renderSweep(p)); return { exitCode: 0, plan: p }; }
|
|
224
266
|
const applied = applySweep({ plan: p, policy, platform, deleteMode: opts.deleteMode, ...deps.apply });
|
|
@@ -231,6 +273,13 @@ export async function runDiskCommand(args = process.argv.slice(2), { env = proce
|
|
|
231
273
|
return { exitCode: purged.results.some((r) => !r.ok) ? 1 : 0, purged };
|
|
232
274
|
}
|
|
233
275
|
if (command === "run") {
|
|
276
|
+
// Self-check first: a broken binding (non-stable target, missing package or dependency) is logged and fails the run.
|
|
277
|
+
const self = (deps.selfCheck ?? selfCheck)({ moduleDir: path.resolve(here, "."), platform, env, ...(deps.selfCheckOptions ?? {}) });
|
|
278
|
+
if (!self.ok) {
|
|
279
|
+
for (const problem of self.problems) stdout(`ERROR: disk run self-check: ${problem}`);
|
|
280
|
+
logEvent(logFile, { event: "disk.run.selfcheck.fail", problems: self.problems, version: self.version });
|
|
281
|
+
}
|
|
282
|
+
ensureScratchRoots({ policy, platform, env });
|
|
234
283
|
const before = check();
|
|
235
284
|
stdout(renderCheck(before));
|
|
236
285
|
const p = plan();
|
|
@@ -243,7 +292,7 @@ export async function runDiskCommand(args = process.argv.slice(2), { env = proce
|
|
|
243
292
|
const warn = after.warn;
|
|
244
293
|
logEvent(logFile, { event: warn ? "disk.floor.warn" : "disk.run.ok", warnings: after.warnings, freeBytes: after.floor.freeBytes, movedBytes: applied.movedBytes });
|
|
245
294
|
if (warn) stdout(`WARNING: ${after.warnings.join("; ")}`);
|
|
246
|
-
return { exitCode: warn ? DISK_EXIT_WARNING : 0, before, after, applied, purged };
|
|
295
|
+
return { exitCode: !self.ok ? 2 : warn ? DISK_EXIT_WARNING : 0, before, after, applied, purged, selfCheck: self };
|
|
247
296
|
}
|
|
248
297
|
throw new Error("disk usage: check|sweep [--dry-run|--delete] [--days N]|purge [--dry-run] [--days N]|run|schedule install|uninstall|status [--json]");
|
|
249
298
|
}
|
package/disk-policy.json
CHANGED
|
@@ -14,6 +14,16 @@
|
|
|
14
14
|
"mac": "/Volumes/D/claude/tmp/swept",
|
|
15
15
|
"win": "D:\\claude\\tmp\\swept"
|
|
16
16
|
},
|
|
17
|
+
"unknownStaleDays": 3,
|
|
18
|
+
"workspaceRoots": {
|
|
19
|
+
"mac": [{"path": "/Volumes/D/claude"}],
|
|
20
|
+
"win": [{"path": "D:\\claude"}]
|
|
21
|
+
},
|
|
22
|
+
"workspaceAllowlist": [".DS_Store", ".git", "node_modules", "tmp", "reports", "tasks", ".cache", "bin", "repos", "packages", "tests", "schemas", "assets", "migration", "archive", "catalog.db", "catalog.db-shm", "catalog.db-wal", "apple-skill-sources", "foundation-runs", "SEO", ".audit", ".commandcode", ".fastembed_cache", ".legion", ".model-bench", ".pnpm-store", ".pytest_cache", ".research", ".right-release", ".rightkit-managed", ".tether", ".venv-tools", ".vite"],
|
|
23
|
+
"scratchRoots": {
|
|
24
|
+
"mac": ["/Volumes/D/scratch"],
|
|
25
|
+
"win": ["D:\\scratch"]
|
|
26
|
+
},
|
|
17
27
|
"writers": [
|
|
18
28
|
{"id": "claude-session-dirs", "platform": "mac", "path": "/private/tmp/claude-501/*/*", "capBytes": 2147483648, "action": "sweep", "unit": "self"},
|
|
19
29
|
{"id": "rightkit-qa-tmp", "platform": "mac", "path": "/private/tmp/rightkit-qa", "capBytes": 2147483648, "action": "sweep", "unit": "children"},
|
package/disk-policy.mjs
CHANGED
|
@@ -56,7 +56,9 @@ export function walkSize(root) {
|
|
|
56
56
|
const dir = stack.pop();
|
|
57
57
|
let st;
|
|
58
58
|
try { st = lstatSync(dir); } catch { continue; }
|
|
59
|
-
|
|
59
|
+
// Allocated bytes; where the filesystem reports none for a non-empty file (NTFS keeps small files resident
|
|
60
|
+
// in the MFT, so Windows reports 0 blocks) the logical size is the best estimate.
|
|
61
|
+
total += st.blocks ? st.blocks * 512 : st.size;
|
|
60
62
|
if (!st.isDirectory()) continue;
|
|
61
63
|
let handle;
|
|
62
64
|
try { handle = opendirSync(dir); } catch { continue; }
|
|
@@ -115,7 +117,77 @@ export function writerMatches(writer, ctx) {
|
|
|
115
117
|
return expandGlob(pattern);
|
|
116
118
|
}
|
|
117
119
|
|
|
118
|
-
|
|
120
|
+
|
|
121
|
+
// ---- Workspace roots (allowlist) and scratch roots ------------------------------------------------------------------
|
|
122
|
+
|
|
123
|
+
const isWin = (key) => key === "win";
|
|
124
|
+
const fold = (value, key) => (isWin(key) ? value.toLowerCase() : value);
|
|
125
|
+
|
|
126
|
+
/** Top-level names git tracks in the repository rooted exactly at `root`; null when that cannot be proven
|
|
127
|
+
* (not a repository root, git missing or failing). Callers must then leave the root alone. */
|
|
128
|
+
export function workspaceTracked(root, { platform = process.platform, git = spawnSync } = {}) {
|
|
129
|
+
const key = platformKey(platform);
|
|
130
|
+
const top = git("git", ["-C", root, "rev-parse", "--show-toplevel"], { encoding: "utf8" });
|
|
131
|
+
if (top.status !== 0) return null;
|
|
132
|
+
try {
|
|
133
|
+
// The native form resolves Windows 8.3 short names (C:\Users\RUNNER~1) to the long names git prints.
|
|
134
|
+
if (fold(realpathSync.native(String(top.stdout).trim()), key) !== fold(realpathSync.native(root), key)) return null;
|
|
135
|
+
} catch { return null; }
|
|
136
|
+
const listed = git("git", ["-C", root, "ls-files", "-z"], { encoding: "utf8", maxBuffer: 1 << 28 });
|
|
137
|
+
if (listed.status !== 0) return null;
|
|
138
|
+
const names = new Set();
|
|
139
|
+
for (const file of String(listed.stdout).split("\0")) if (file) names.add(fold(file.split("/")[0], key));
|
|
140
|
+
return names;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
export function scratchRoots(policy, { platform = process.platform, env = process.env, home = os.homedir() } = {}) {
|
|
144
|
+
const out = [];
|
|
145
|
+
for (const p of policy.scratchRoots?.[platformKey(platform)] ?? []) { try { out.push(expandPolicyPath(p, { env, home })); } catch { /* unset variable */ } }
|
|
146
|
+
return out;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/** Create missing scratch roots (only when their parent volume exists). Returns the roots that exist afterwards. */
|
|
150
|
+
export function ensureScratchRoots({ policy, platform = process.platform, env = process.env, home = os.homedir() } = {}) {
|
|
151
|
+
const ready = [];
|
|
152
|
+
for (const root of scratchRoots(policy, { platform, env, home })) {
|
|
153
|
+
try {
|
|
154
|
+
if (!existsSync(root) && existsSync(path.dirname(root))) mkdirSync(root);
|
|
155
|
+
if (existsSync(root)) ready.push(root);
|
|
156
|
+
} catch { /* volume not writable: the next run retries */ }
|
|
157
|
+
}
|
|
158
|
+
return ready;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/** Top-level entries of each workspace root that are neither tracked by the workspace repo, nested git checkouts,
|
|
162
|
+
* nor on the allowlist. Symlinks are listed (`symlink: true`) but sweep never touches them. */
|
|
163
|
+
export function scanWorkspaceRoots({ policy, platform = process.platform, env = process.env, home = os.homedir(), tracked = (r) => workspaceTracked(r, { platform }) } = {}) {
|
|
164
|
+
const key = platformKey(platform);
|
|
165
|
+
const allow = new Set((policy.workspaceAllowlist ?? []).map((n) => fold(n, key)));
|
|
166
|
+
const unknown = [];
|
|
167
|
+
const skipped = [];
|
|
168
|
+
for (const entry of policy.workspaceRoots?.[key] ?? []) {
|
|
169
|
+
let root;
|
|
170
|
+
try { root = expandPolicyPath(entry.path, { env, home }); } catch { continue; }
|
|
171
|
+
if (!existsSync(root)) continue;
|
|
172
|
+
const trackedNames = tracked(root);
|
|
173
|
+
if (!trackedNames) { skipped.push({ writer: "workspace-unknown", path: root, reason: "git-unavailable" }); continue; }
|
|
174
|
+
let names = [];
|
|
175
|
+
try { names = readdirSync(root); } catch { continue; }
|
|
176
|
+
const extra = new Set((entry.allow ?? []).map((n) => fold(n, key)));
|
|
177
|
+
for (const name of names) {
|
|
178
|
+
const folded = fold(name, key);
|
|
179
|
+
if (allow.has(folded) || extra.has(folded) || trackedNames.has(folded)) continue;
|
|
180
|
+
const full = path.join(root, name);
|
|
181
|
+
let st;
|
|
182
|
+
try { st = lstatSync(full); } catch { continue; }
|
|
183
|
+
if (!st.isSymbolicLink() && existsSync(path.join(full, ".git"))) continue; // nested git checkout
|
|
184
|
+
unknown.push({ root, name, path: full, symlink: st.isSymbolicLink(), mtimeMs: st.mtimeMs });
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
return { unknown, skipped };
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
export function checkDisk({ policy, platform = process.platform, env = process.env, home = os.homedir(), size = (p) => pathSize(p, { platform }), space = freeSpace, now = Date.now(), scan = scanWorkspaceRoots } = {}) {
|
|
119
191
|
const key = platformKey(platform);
|
|
120
192
|
const floorPolicy = policy.floors[key];
|
|
121
193
|
const floor = evaluateFloor(floorPolicy, space(floorPolicy.path));
|
|
@@ -132,7 +204,11 @@ export function checkDisk({ policy, platform = process.platform, env = process.e
|
|
|
132
204
|
const warnings = [];
|
|
133
205
|
if (floor.warn) warnings.push(`${floor.label} free ${formatBytes(floor.freeBytes)} is below the ${formatBytes(floor.thresholdBytes)} floor`);
|
|
134
206
|
for (const w of writers) if (w.over) warnings.push(`${w.id} holds ${formatBytes(w.bytes)} over its ${formatBytes(w.capBytes)} cap`);
|
|
135
|
-
|
|
207
|
+
const scanned = scan({ policy, platform, env, home });
|
|
208
|
+
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`);
|
|
210
|
+
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
|
+
return { schema: 1, platform: key, floor, writers, unknown, warnings, warn: warnings.length > 0 };
|
|
136
212
|
}
|
|
137
213
|
|
|
138
214
|
export function formatBytes(n) {
|
|
@@ -196,7 +272,7 @@ export function gitTracksPath(target) {
|
|
|
196
272
|
return String(listed.stdout).length > 0;
|
|
197
273
|
}
|
|
198
274
|
|
|
199
|
-
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 }) } = {}) {
|
|
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 } = {}) {
|
|
200
276
|
const key = platformKey(platform);
|
|
201
277
|
const effectiveDays = staleDays ?? policy.staleDays;
|
|
202
278
|
const candidates = [];
|
|
@@ -233,6 +309,33 @@ export function planSweep({ policy, platform = process.platform, env = process.e
|
|
|
233
309
|
candidates.push({ ...base, root, bytes: size(unit), mtime: new Date(st.mtimeMs).toISOString() });
|
|
234
310
|
}
|
|
235
311
|
}
|
|
312
|
+
// Allowlisted workspace roots: unknown top-level entries older than 3 days and idle.
|
|
313
|
+
const scanned = scan({ policy, platform, env, home });
|
|
314
|
+
skipped.push(...scanned.skipped);
|
|
315
|
+
const unknownCutoff = now - (staleDays ?? policy.unknownStaleDays ?? 3) * 86400_000;
|
|
316
|
+
const consider = (writerId, unit, root, cutoff) => {
|
|
317
|
+
if (seen.has(unit)) return;
|
|
318
|
+
seen.add(unit);
|
|
319
|
+
const base = { writer: writerId, path: unit };
|
|
320
|
+
let st;
|
|
321
|
+
try { st = lstatSync(unit); } catch { return; }
|
|
322
|
+
if (st.isSymbolicLink()) { skipped.push({ ...base, reason: "symlink" }); return; }
|
|
323
|
+
if (!isInsideRoot(unit, root)) { skipped.push({ ...base, reason: "outside-root" }); return; }
|
|
324
|
+
if (isProtectedPath(unit, policy)) { skipped.push({ ...base, reason: "protected" }); return; }
|
|
325
|
+
if (hasFreshContent(unit, cutoff)) { skipped.push({ ...base, reason: "fresh" }); return; }
|
|
326
|
+
if (lockCheck(unit)) { skipped.push({ ...base, reason: "in-use" }); return; }
|
|
327
|
+
candidates.push({ ...base, root, bytes: size(unit), mtime: new Date(st.mtimeMs).toISOString() });
|
|
328
|
+
};
|
|
329
|
+
for (const u of scanned.unknown) consider("workspace-unknown", u.path, u.root, unknownCutoff);
|
|
330
|
+
// Scratch roots: every child older than 3 days and idle (the same window).
|
|
331
|
+
for (const root of scratchRoots(policy, { platform, env, home })) {
|
|
332
|
+
let rst;
|
|
333
|
+
try { rst = lstatSync(root); } catch { continue; }
|
|
334
|
+
if (rst.isSymbolicLink()) { skipped.push({ writer: "scratch-root", path: root, reason: "symlink-root" }); continue; }
|
|
335
|
+
let names = [];
|
|
336
|
+
try { names = readdirSync(root); } catch { continue; }
|
|
337
|
+
for (const n of names) consider("scratch-root", path.join(root, n), root, unknownCutoff);
|
|
338
|
+
}
|
|
236
339
|
return { schema: 1, platform: key, staleDays: effectiveDays, candidates, skipped, totalBytes: candidates.reduce((s, c) => s + c.bytes, 0) };
|
|
237
340
|
}
|
|
238
341
|
|
|
@@ -326,6 +429,7 @@ export function applySweep({ plan, policy, platform = process.platform, deleteMo
|
|
|
326
429
|
try {
|
|
327
430
|
// Re-verify at apply time: still the planned unit, not a symlink, still inside its root.
|
|
328
431
|
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");
|
|
329
433
|
if (deleteMode) { remove(c.path); results.push({ ...c, action: "delete", ok: true }); logEvent(logFile, { event: "disk.sweep.delete", ...c }); continue; }
|
|
330
434
|
// An item stays on its own volume: moving it elsewhere frees nothing on the volume that is short of space.
|
|
331
435
|
const holding = pick(c.path, { policy, platform, env, home, date });
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
import { cpSync, existsSync, lstatSync, mkdirSync, readFileSync, readdirSync, realpathSync, renameSync, rmSync, writeFileSync } from "node:fs";
|
|
2
|
+
import os from "node:os";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { spawnSync } from "./background-process.mjs";
|
|
5
|
+
|
|
6
|
+
// The scheduled disk job must never bind to a pnpm store, a pnpm global hash directory or a checkout: those move or vanish
|
|
7
|
+
// on upgrade. `disk schedule install` copies the installed package into this per-user root and binds the job to it.
|
|
8
|
+
// See docs/architecture/development-installed-product-boundary.md (installed bindings use a stable `current` root).
|
|
9
|
+
|
|
10
|
+
const pathFor = (platform) => (platform === "win32" || platform === "win" ? path.win32 : path.posix);
|
|
11
|
+
|
|
12
|
+
export function stableRootPath({ platform = process.platform, env = process.env, home = os.homedir() } = {}) {
|
|
13
|
+
if (platform === "win32" || platform === "win") {
|
|
14
|
+
if (!env.LOCALAPPDATA) throw new Error("disk: LOCALAPPDATA is not set");
|
|
15
|
+
return path.win32.join(env.LOCALAPPDATA, "Orthic Labs", "rightkit-release", "current");
|
|
16
|
+
}
|
|
17
|
+
return path.posix.join(home, "Library", "Application Support", "Orthic Labs", "rightkit-release", "current");
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function stableCliPath(stable, platform = process.platform) {
|
|
21
|
+
return pathFor(platform).join(stable, "node_modules", "@rightkit", "release", "cli", "right-release.mjs");
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function isUnderStable(file, stable, platform = process.platform) {
|
|
25
|
+
const win = platform === "win32" || platform === "win";
|
|
26
|
+
const n = (v) => (win ? v.toLowerCase().replace(/\//g, "\\") : v);
|
|
27
|
+
const prefix = n(stable).replace(/[\\/]+$/, "") + (win ? "\\" : "/");
|
|
28
|
+
return n(file).startsWith(prefix);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const readJson = (file) => { try { return JSON.parse(readFileSync(file, "utf8")); } catch { return null; } };
|
|
32
|
+
|
|
33
|
+
// Walk up from `from` looking for `node_modules/<name>` (or `<name>` directly inside a node_modules directory).
|
|
34
|
+
function findDependency(name, from) {
|
|
35
|
+
let dir = from;
|
|
36
|
+
for (;;) {
|
|
37
|
+
for (const candidate of [path.join(dir, "node_modules", name), path.basename(dir) === "node_modules" ? path.join(dir, name) : null]) {
|
|
38
|
+
if (candidate && existsSync(path.join(candidate, "package.json"))) return candidate;
|
|
39
|
+
}
|
|
40
|
+
const up = path.dirname(dir);
|
|
41
|
+
if (up === dir) return null;
|
|
42
|
+
dir = up;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** Runtime dependency directories of a package (transitive), as `{ name, dir }`, found beside the package (lexical path
|
|
47
|
+
* first, then its real path, so pnpm's symlinked layout works). */
|
|
48
|
+
export function collectDependencies(pkgRoot) {
|
|
49
|
+
const found = new Map();
|
|
50
|
+
const visit = (root) => {
|
|
51
|
+
const deps = Object.keys(readJson(path.join(root, "package.json"))?.dependencies ?? {});
|
|
52
|
+
for (const name of deps) {
|
|
53
|
+
if (found.has(name)) continue;
|
|
54
|
+
let real = root;
|
|
55
|
+
try { real = realpathSync(root); } catch { /* keep lexical */ }
|
|
56
|
+
const dir = findDependency(name, root) ?? findDependency(name, real);
|
|
57
|
+
if (!dir) throw new Error(`disk: dependency ${name} of ${root} was not found beside the installed package`);
|
|
58
|
+
found.set(name, dir);
|
|
59
|
+
visit(dir);
|
|
60
|
+
}
|
|
61
|
+
};
|
|
62
|
+
visit(pkgRoot);
|
|
63
|
+
return [...found].map(([name, dir]) => ({ name, dir }));
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** Copy the installed @rightkit/release package (and its dependencies) into the stable root. The new tree is built and
|
|
67
|
+
* smoke-tested in a staging directory next to `current`, then swapped in by rename; a failed swap restores the previous
|
|
68
|
+
* `current`. Returns `{ stable, cli, version, replaced, unchanged }`. */
|
|
69
|
+
export function installStableRoot({ cli, platform = process.platform, env = process.env, home = os.homedir(), now = Date.now(), nodePath = process.execPath, smoke = true } = {}) {
|
|
70
|
+
const stable = stableRootPath({ platform, env, home });
|
|
71
|
+
const stableCli = stableCliPath(stable, platform);
|
|
72
|
+
let realCli = cli;
|
|
73
|
+
try { realCli = realpathSync(cli); } catch { /* keep lexical */ }
|
|
74
|
+
if (isUnderStable(cli, stable, platform) || isUnderStable(realCli, stable, platform)) {
|
|
75
|
+
return { stable, cli: stableCli, version: readJson(path.join(stable, "node_modules", "@rightkit", "release", "package.json"))?.version, replaced: false, unchanged: true };
|
|
76
|
+
}
|
|
77
|
+
const pkgRoot = path.resolve(path.dirname(cli), "..");
|
|
78
|
+
const pkg = readJson(path.join(pkgRoot, "package.json"));
|
|
79
|
+
if (pkg?.name !== "@rightkit/release") throw new Error(`disk: ${cli} is not inside an @rightkit/release package`);
|
|
80
|
+
const deps = collectDependencies(pkgRoot);
|
|
81
|
+
|
|
82
|
+
const parent = path.dirname(stable);
|
|
83
|
+
mkdirSync(parent, { recursive: true });
|
|
84
|
+
for (const name of readdirSync(parent)) {
|
|
85
|
+
if (/^current\.(staging|prev)-/.test(name)) try { rmSync(path.join(parent, name), { recursive: true, force: true }); } catch { /* in use: leave it */ }
|
|
86
|
+
}
|
|
87
|
+
const staging = path.join(parent, `current.staging-${process.pid}-${now}`);
|
|
88
|
+
const copy = (from, to, filter) => cpSync(from, to, { recursive: true, dereference: true, force: true, filter });
|
|
89
|
+
try {
|
|
90
|
+
const target = path.join(staging, "node_modules", "@rightkit", "release");
|
|
91
|
+
mkdirSync(path.dirname(target), { recursive: true });
|
|
92
|
+
copy(pkgRoot, target, (src) => path.basename(src) !== "node_modules" && !src.endsWith(".test.mjs"));
|
|
93
|
+
for (const dep of deps) {
|
|
94
|
+
const to = path.join(staging, "node_modules", ...dep.name.split("/"));
|
|
95
|
+
mkdirSync(path.dirname(to), { recursive: true });
|
|
96
|
+
copy(dep.dir, to, (src) => path.basename(src) !== "node_modules");
|
|
97
|
+
}
|
|
98
|
+
writeFileSync(path.join(staging, "install.json"), `${JSON.stringify({ package: pkg.name, version: pkg.version, source: pkgRoot, installedAt: new Date(now).toISOString(), node: nodePath }, null, 2)}\n`);
|
|
99
|
+
const stagedCli = stableCliPath(staging, platform);
|
|
100
|
+
if (!existsSync(stagedCli)) throw new Error("disk: the copied package has no cli/right-release.mjs");
|
|
101
|
+
if (smoke) {
|
|
102
|
+
const r = spawnSync(nodePath, [stagedCli, "--version"], { encoding: "utf8" });
|
|
103
|
+
if (r.status !== 0) throw new Error(`disk: the staged copy does not run: ${r.stderr || r.stdout || r.error?.message}`);
|
|
104
|
+
}
|
|
105
|
+
} catch (error) {
|
|
106
|
+
rmSync(staging, { recursive: true, force: true });
|
|
107
|
+
throw error;
|
|
108
|
+
}
|
|
109
|
+
let replaced = false;
|
|
110
|
+
let prev;
|
|
111
|
+
try {
|
|
112
|
+
if (existsSync(stable) || isLink(stable)) { prev = path.join(parent, `current.prev-${process.pid}-${now}`); renameSync(stable, prev); replaced = true; }
|
|
113
|
+
renameSync(staging, stable);
|
|
114
|
+
} catch (error) {
|
|
115
|
+
if (prev && !existsSync(stable)) try { renameSync(prev, stable); } catch { /* reported below */ }
|
|
116
|
+
rmSync(staging, { recursive: true, force: true });
|
|
117
|
+
throw new Error(`disk: could not replace ${stable}: ${error.message}`);
|
|
118
|
+
}
|
|
119
|
+
if (prev) try { rmSync(prev, { recursive: true, force: true }); } catch { /* a later install removes it */ }
|
|
120
|
+
return { stable, cli: stableCli, version: pkg.version, replaced, unchanged: false };
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function isLink(p) { try { return lstatSync(p).isSymbolicLink(); } catch { return false; } }
|
|
124
|
+
|
|
125
|
+
/** Registered schedule target: the CLI path the launchd job or Windows task runs, or null when none is registered. */
|
|
126
|
+
export function readScheduleBinding({ platform = process.platform, home = os.homedir(), file, run = spawnSync, label = "RightKit Disk Hygiene", plistFile } = {}) {
|
|
127
|
+
if (platform === "darwin") {
|
|
128
|
+
const plist = plistFile ?? file ?? path.join(home, "Library", "LaunchAgents", "dev.rightkit.disk-hygiene.plist");
|
|
129
|
+
if (!existsSync(plist)) return null;
|
|
130
|
+
const m = /<key>ProgramArguments<\/key><array>\s*<string>([^<]*)<\/string>\s*<string>([^<]*)<\/string>/.exec(readFileSync(plist, "utf8"));
|
|
131
|
+
return m ? { node: m[1], cli: m[2], source: plist } : { node: null, cli: null, source: plist };
|
|
132
|
+
}
|
|
133
|
+
const r = run("powershell.exe", ["-NoProfile", "-NonInteractive", "-Command", `$t = Get-ScheduledTask -TaskName '${label}' -ErrorAction SilentlyContinue; if (-not $t) { exit 3 }; $a = $t.Actions[0]; Write-Output $a.Execute; Write-Output $a.Arguments`], { encoding: "utf8" });
|
|
134
|
+
if (r.status !== 0) return null;
|
|
135
|
+
const [exe, ...rest] = String(r.stdout).split(/\r?\n/).map((l) => l.trim()).filter(Boolean);
|
|
136
|
+
const args = rest.join(" ");
|
|
137
|
+
const quoted = /"([^"]+)"/.exec(args);
|
|
138
|
+
return { node: exe ?? null, cli: quoted ? quoted[1] : (args.split(/\s+/)[0] || null), source: label };
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/** Evaluate a binding against the stable root. */
|
|
142
|
+
export function evaluateBinding(binding, { platform = process.platform, env = process.env, home = os.homedir(), exists = existsSync } = {}) {
|
|
143
|
+
if (!binding) return { registered: false, ok: true, problems: [] };
|
|
144
|
+
const stable = stableRootPath({ platform, env, home });
|
|
145
|
+
const problems = [];
|
|
146
|
+
if (!binding.cli) problems.push("the schedule names no right-release CLI");
|
|
147
|
+
else {
|
|
148
|
+
if (!isUnderStable(binding.cli, stable, platform)) problems.push(`target ${binding.cli} is not under the stable root ${stable} (reinstall: right-release disk schedule install)`);
|
|
149
|
+
if (!exists(binding.cli)) problems.push(`target ${binding.cli} is MISSING`);
|
|
150
|
+
}
|
|
151
|
+
if (binding.node && !exists(binding.node)) problems.push(`node ${binding.node} is MISSING`);
|
|
152
|
+
return { registered: true, ok: problems.length === 0, problems, cli: binding.cli, node: binding.node, stable };
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/** `disk run` self-check: this process's own package must be intact and a registered schedule must be bound to the stable root. */
|
|
156
|
+
export function selfCheck({ moduleDir, platform = process.platform, env = process.env, home = os.homedir(), binding = () => readScheduleBinding({ platform, home }), exists = existsSync } = {}) {
|
|
157
|
+
const problems = [];
|
|
158
|
+
const pkg = readJson(path.join(moduleDir, "package.json"));
|
|
159
|
+
if (!pkg) problems.push(`own package.json is unreadable at ${moduleDir}`);
|
|
160
|
+
else {
|
|
161
|
+
if (!exists(path.join(moduleDir, "cli", "right-release.mjs"))) problems.push("own cli/right-release.mjs is missing");
|
|
162
|
+
for (const dep of Object.keys(pkg.dependencies ?? {})) if (!findDependency(dep, moduleDir)) problems.push(`dependency ${dep} is not resolvable from ${moduleDir}`);
|
|
163
|
+
}
|
|
164
|
+
let bound;
|
|
165
|
+
try { bound = evaluateBinding(binding(), { platform, env, home, exists }); } catch (error) { bound = { registered: false, ok: false, problems: [`cannot read the schedule binding: ${error.message}`] }; }
|
|
166
|
+
problems.push(...bound.problems);
|
|
167
|
+
return { ok: problems.length === 0, problems, registered: bound.registered, version: pkg?.version };
|
|
168
|
+
}
|
package/package.json
CHANGED
package/rightkit-versions.json
CHANGED
|
@@ -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.
|
|
34
|
+
"@rightkit/release": "0.2.163",
|
|
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,8 @@
|
|
|
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"
|
|
207
208
|
],
|
|
208
209
|
"@rightkit/qa": [
|
|
209
210
|
"0.1.0",
|
|
@@ -354,7 +355,7 @@
|
|
|
354
355
|
"rightkit-tauri": "0.1.0"
|
|
355
356
|
},
|
|
356
357
|
"stagedCargo": {
|
|
357
|
-
"rightkit-agent-tools": "0.1.
|
|
358
|
+
"rightkit-agent-tools": "0.1.3",
|
|
358
359
|
"rightkit-agents": "0.1.0",
|
|
359
360
|
"rightkit-app-settings": "0.2.0",
|
|
360
361
|
"rightkit-asr": "0.2.1",
|
|
@@ -383,7 +384,7 @@
|
|
|
383
384
|
"rightkit-llm": "0.2.1",
|
|
384
385
|
"rightkit-logs": "0.3.0",
|
|
385
386
|
"rightkit-mcp": "0.5.3",
|
|
386
|
-
"rightkit-models": "0.3.
|
|
387
|
+
"rightkit-models": "0.3.2",
|
|
387
388
|
"rightkit-oauth": "0.5.0",
|
|
388
389
|
"rightkit-ocr": "0.2.2",
|
|
389
390
|
"rightkit-onnx": "0.2.3",
|
|
@@ -393,6 +394,7 @@
|
|
|
393
394
|
"rightkit-playback": "0.2.1",
|
|
394
395
|
"rightkit-qa": "0.4.0",
|
|
395
396
|
"rightkit-qa-driver": "0.4.0",
|
|
397
|
+
"rightkit-record": "0.3.0",
|
|
396
398
|
"rightkit-review": "0.2.0",
|
|
397
399
|
"rightkit-sandbox": "0.4.0",
|
|
398
400
|
"rightkit-search": "0.2.1",
|
|
@@ -423,7 +425,7 @@
|
|
|
423
425
|
},
|
|
424
426
|
"registry": {
|
|
425
427
|
"cargo": {
|
|
426
|
-
"rightkit-agent-tools": "0.1.
|
|
428
|
+
"rightkit-agent-tools": "0.1.3",
|
|
427
429
|
"rightkit-agents": "0.1.0",
|
|
428
430
|
"rightkit-app-settings": "0.2.0",
|
|
429
431
|
"rightkit-asr": "0.2.1",
|
|
@@ -456,7 +458,7 @@
|
|
|
456
458
|
"rightkit-logs": "0.3.0",
|
|
457
459
|
"rightkit-mcp": "0.5.3",
|
|
458
460
|
"rightkit-media": "0.2.6",
|
|
459
|
-
"rightkit-models": "0.3.
|
|
461
|
+
"rightkit-models": "0.3.2",
|
|
460
462
|
"rightkit-oauth": "0.5.0",
|
|
461
463
|
"rightkit-ocr": "0.2.2",
|
|
462
464
|
"rightkit-onnx": "0.2.3",
|
|
@@ -471,6 +473,7 @@
|
|
|
471
473
|
"rightkit-qts": "0.2.6",
|
|
472
474
|
"rightkit-qts-ggml": "0.1.2",
|
|
473
475
|
"rightkit-qts-ggml-sys": "0.1.2",
|
|
476
|
+
"rightkit-record": "0.3.0",
|
|
474
477
|
"rightkit-review": "0.2.0",
|
|
475
478
|
"rightkit-sandbox": "0.4.0",
|
|
476
479
|
"rightkit-search": "0.2.1",
|
|
@@ -513,7 +516,7 @@
|
|
|
513
516
|
"@rightkit/qa": "0.4.8",
|
|
514
517
|
"@rightkit/qa-chrome": "0.1.6",
|
|
515
518
|
"@rightkit/react-shell": "0.6.4",
|
|
516
|
-
"@rightkit/release": "0.2.
|
|
519
|
+
"@rightkit/release": "0.2.163",
|
|
517
520
|
"@rightkit/settings-ui": "0.4.4",
|
|
518
521
|
"@rightkit/shell": "0.1.1",
|
|
519
522
|
"@rightkit/tauri": "0.2.1",
|
|
@@ -540,7 +543,8 @@
|
|
|
540
543
|
"0.2.5",
|
|
541
544
|
"0.2.6",
|
|
542
545
|
"0.2.7",
|
|
543
|
-
"0.3.0"
|
|
546
|
+
"0.3.0",
|
|
547
|
+
"0.3.1"
|
|
544
548
|
],
|
|
545
549
|
"rightkit-audio": [
|
|
546
550
|
"0.1.0",
|
|
@@ -756,6 +760,9 @@
|
|
|
756
760
|
"rightkit-qa-driver": [
|
|
757
761
|
"0.3.0",
|
|
758
762
|
"0.3.1"
|
|
763
|
+
],
|
|
764
|
+
"rightkit-agent-tools": [
|
|
765
|
+
"0.1.2"
|
|
759
766
|
]
|
|
760
767
|
}
|
|
761
768
|
}
|