apps relink: after a machine move, missing app folders are pointed at the same app here (last path parts, else the config's app name), ids kept; apps folders hints it; apps add names an app after its config app name
This commit is contained in:
parent
9a907d87e5
commit
cd0110527c
5 changed files with 134 additions and 5 deletions
|
|
@ -12,7 +12,7 @@
|
|||
// Written atomically (temp file, rename); a missing or unreadable file reads as empty (an unreadable one is kept).
|
||||
import { consoleLink } from './console-links';
|
||||
import { createHash } from 'node:crypto';
|
||||
import { copyFileSync, existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from 'node:fs';
|
||||
import { copyFileSync, existsSync, mkdirSync, readdirSync, readFileSync, renameSync, statSync, writeFileSync } from 'node:fs';
|
||||
import { homedir } from 'node:os';
|
||||
import { basename, dirname, join, resolve } from 'node:path';
|
||||
|
||||
|
|
@ -87,7 +87,7 @@ export function withFolder(r: Registry, path: string, name: string | undefined,
|
|||
const abs = resolve(path);
|
||||
const have = r.projects.find(p => p.path === abs);
|
||||
if (have) return { registry: r, project: have, added: false };
|
||||
const project = { id: folderId(abs), path: abs, name: name?.trim() || basename(abs), added: now };
|
||||
const project = { id: folderId(abs), path: abs, name: name?.trim() || configAppName(abs) || basename(abs), added: now };
|
||||
return { registry: { ...r, version: 1, projects: [...r.projects, project] }, project, added: true };
|
||||
}
|
||||
|
||||
|
|
@ -144,3 +144,64 @@ export function removeFolder(pathOrId: string, file = registryFile()): RegistryP
|
|||
if (removed) writeRegistry(registry, file);
|
||||
return removed;
|
||||
}
|
||||
|
||||
/** The app name a storebound config gives (`app: { name: '...' }`), read as text (no config load), else null. */
|
||||
export function configAppName(dir: string): string | null {
|
||||
for (const n of ['storebound.config.ts', 'storebound.config.mts', 'storebound.config.js', 'storebound.config.mjs']) {
|
||||
try {
|
||||
const text = readFileSync(join(dir, n), 'utf8');
|
||||
const m = /\bapp\s*:\s*\{[^}]*?\bname\s*:\s*(['"`])([^'"`]+)\1/s.exec(text);
|
||||
if (m) return m[2]!.trim();
|
||||
} catch { /* not this one */ }
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/** Folders with a storebound config under the roots (depth 4; node_modules, .git and _arsiv skipped). */
|
||||
export function configFolders(roots: string[], depth = 4): string[] {
|
||||
const out: string[] = [];
|
||||
const walk = (d: string, left: number) => {
|
||||
let names: string[] = [];
|
||||
try { names = readdirSync(d); } catch { return; }
|
||||
if (names.some(n => /^storebound\.config\.(ts|mts|js|mjs)$/.test(n))) out.push(d);
|
||||
if (left <= 0) return;
|
||||
for (const n of names) {
|
||||
if (n === 'node_modules' || n.startsWith('.') || n === '_arsiv') continue;
|
||||
const p = join(d, n);
|
||||
try { if (statSync(p).isDirectory()) walk(p, left - 1); } catch { /* unreadable */ }
|
||||
}
|
||||
};
|
||||
for (const r of roots) walk(r, depth);
|
||||
return [...new Set(out)];
|
||||
}
|
||||
|
||||
/**
|
||||
* After a machine move the list keeps the old computer's paths ("folder not found"). For each missing folder: a
|
||||
* config folder under the roots whose last path parts match (some-game, Acme/frontend), else one whose app name is
|
||||
* the same as the entry's name. The id stays, so the desktop's per-app accounts and settings stay linked. Pure.
|
||||
*/
|
||||
export function relinkPlan(r: Registry, candidates: string[], exists: (p: string) => boolean, nameOf: (dir: string) => string | null): { id: string; name: string; from: string; to: string }[] {
|
||||
const parts = (p: string) => p.split(/[\\/]+/).filter(Boolean).map(x => x.toLowerCase());
|
||||
const taken = new Set(r.projects.filter(p => exists(p.path)).map(p => resolve(p.path).toLowerCase()));
|
||||
const plan: { id: string; name: string; from: string; to: string }[] = [];
|
||||
for (const p of r.projects) {
|
||||
if (exists(p.path)) continue;
|
||||
const want = parts(p.path);
|
||||
const free = candidates.filter(c => !taken.has(resolve(c).toLowerCase()));
|
||||
let best: string | undefined;
|
||||
for (const k of [3, 2, 1]) {
|
||||
const tail = want.slice(-k).join('/');
|
||||
best = free.find(c => parts(c).slice(-k).join('/') === tail);
|
||||
if (best) break;
|
||||
}
|
||||
best ??= free.find(c => nameOf(c)?.toLowerCase() === p.name.toLowerCase());
|
||||
if (best) { plan.push({ id: p.id, name: p.name, from: p.path, to: resolve(best) }); taken.add(resolve(best).toLowerCase()); }
|
||||
}
|
||||
return plan;
|
||||
}
|
||||
|
||||
export function applyRelink(plan: { id: string; to: string }[], file = registryFile()): void {
|
||||
const r = readRegistry(file);
|
||||
for (const s of plan) { const p = r.projects.find(x => x.id === s.id); if (p) p.path = s.to; }
|
||||
writeRegistry(r, file);
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue