martty 0.2.34 → 0.2.35

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.
@@ -0,0 +1,88 @@
1
+ /** Removal is intentionally limited to saved recipes and exact private binary installations. */
2
+ import { existsSync, lstatSync, readdirSync, realpathSync, renameSync, rmSync } from 'node:fs'
3
+ import { randomUUID } from 'node:crypto'
4
+ import path from 'node:path'
5
+ import { savedHarnesses, removeHarnessConfiguration } from './harnesses.js'
6
+
7
+ const inside = (root, value) => value === root || value.startsWith(root + path.sep)
8
+ const references = entry => [entry.command, ...(entry.args ?? []), ...Object.entries(entry.env ?? {})
9
+ .flatMap(([key, value]) => key.toUpperCase() === 'PATH' ? value.split(path.delimiter) : [value])]
10
+ .filter(value => typeof value === 'string')
11
+ .map(value => !path.isAbsolute(value) && value.startsWith('--') && value.includes('=') ? value.slice(value.indexOf('=') + 1) : value)
12
+ .filter(value => path.isAbsolute(value))
13
+
14
+ function hasLink(directory) {
15
+ if (lstatSync(directory).isSymbolicLink()) return true
16
+ if (!lstatSync(directory).isDirectory()) return false
17
+ return readdirSync(directory).some(name => hasLink(path.join(directory, name)))
18
+ }
19
+
20
+ function privateInstallation(settingsPath, entry) {
21
+ const root = path.resolve(path.dirname(settingsPath), 'bin')
22
+ const canonicalRoot = existsSync(root) ? realpathSync(root) : root
23
+ const candidate = references(entry).find(value => inside(root, path.resolve(value)) || inside(canonicalRoot, path.resolve(value)))
24
+ if (!candidate) return { resources: [], cleanupReason: 'No private binary installation. External programs and shared npx/uvx caches are kept.' }
25
+ const parts = path.relative(inside(root, path.resolve(candidate)) ? root : canonicalRoot, path.resolve(candidate)).split(path.sep)
26
+ // The installer owns bin/<registry-id>/<version>/<platform>, never the whole bin or id directory.
27
+ if (parts.length < 4 || parts.slice(0, 3).some(part => !/^[a-zA-Z0-9][a-zA-Z0-9._-]*$/.test(part))) {
28
+ return { resources: [], cleanupReason: 'The path is not a recognized private binary installation.' }
29
+ }
30
+ const directory = path.join(root, ...parts.slice(0, 3))
31
+ let cursor = directory
32
+ while (true) {
33
+ try {
34
+ if (lstatSync(cursor).isSymbolicLink()) return { resources: [], cleanupReason: 'A symbolic link is present; resource cleanup is disabled.' }
35
+ } catch (error) { if (error.code !== 'ENOENT') throw error }
36
+ const parent = path.dirname(cursor)
37
+ if (cursor === path.dirname(root)) break
38
+ cursor = parent
39
+ }
40
+ if (!existsSync(directory)) return { resources: [], cleanupReason: 'Private installation is already absent.' }
41
+ if (hasLink(directory)) return { resources: [], cleanupReason: 'The installation contains a symbolic link; resource cleanup is disabled.' }
42
+ return { resources: [directory] }
43
+ }
44
+
45
+ export function planHarnessRemoval(settingsPath, id, options = {}) {
46
+ const entries = savedHarnesses(settingsPath)
47
+ const entry = entries.find(entry => entry.id === id)
48
+ if (!entry) throw new Error('Only a saved Harness configuration can be removed')
49
+ if (options.isCurrent?.(entry)) throw new Error('Switch to another Harness before removing the current Harness')
50
+ const forced = [options.forcedHarness, ...(options.defaults ?? []).filter(entry => entry.source === 'forced')].filter(Boolean)
51
+ if (forced.some(value => value.id === id || value.command === entry.command)) throw new Error('This Harness is product-forced and cannot be removed here')
52
+ const installation = privateInstallation(settingsPath, entry)
53
+ if (installation.resources.length) {
54
+ const directory = installation.resources[0]
55
+ const shared = [...entries.filter(value => value.id !== id), ...(options.defaults ?? []), ...forced]
56
+ .some(value => references(value).some(ref => {
57
+ const resolved = existsSync(ref) ? realpathSync(ref) : path.resolve(ref)
58
+ return inside(directory, path.resolve(ref)) || inside(realpathSync(directory), resolved)
59
+ }))
60
+ if (shared) return { entry, resources: [], cleanupReason: 'The installation is shared by another Harness configuration; its resources must be kept.' }
61
+ }
62
+ return { entry, ...installation }
63
+ }
64
+
65
+ export function removeHarness(settingsPath, expected, options = {}) {
66
+ const plan = planHarnessRemoval(settingsPath, expected.entry.id, options)
67
+ if (JSON.stringify(plan.entry) !== JSON.stringify(expected.entry)) throw new Error('Harness configuration changed; review removal again')
68
+ if (options.cleanup && plan.cleanupReason) throw new Error(plan.cleanupReason)
69
+ if (options.cleanup && JSON.stringify(plan.resources) !== JSON.stringify(expected.resources)) throw new Error('Installation paths changed; review removal again')
70
+ // Move the exact validated installation aside before changing settings. No external cache or ancestor is deleted.
71
+ const moved = []
72
+ try {
73
+ if (options.cleanup) for (const resource of plan.resources) {
74
+ const quarantine = path.join(path.dirname(resource), `.removed-${randomUUID()}`)
75
+ renameSync(resource, quarantine)
76
+ moved.push({ resource, quarantine })
77
+ }
78
+ removeHarnessConfiguration(settingsPath, plan.entry)
79
+ } catch (error) {
80
+ for (const { resource, quarantine } of moved.reverse()) renameSync(quarantine, resource)
81
+ throw error
82
+ }
83
+ for (const { quarantine } of moved) {
84
+ try { rmSync(quarantine, { recursive: true }) }
85
+ catch (error) { throw new Error(`Configuration removed, but cleanup failed at ${quarantine}: ${error.message}`) }
86
+ }
87
+ return { entry: plan.entry, removed: moved.map(({ resource }) => resource) }
88
+ }