portable-agent-layer 0.85.8 → 0.85.9
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/package.json
CHANGED
package/src/cli/migrate.ts
CHANGED
|
@@ -12,7 +12,6 @@
|
|
|
12
12
|
|
|
13
13
|
import {
|
|
14
14
|
existsSync,
|
|
15
|
-
mkdirSync,
|
|
16
15
|
readdirSync,
|
|
17
16
|
readFileSync,
|
|
18
17
|
renameSync,
|
|
@@ -22,7 +21,7 @@ import {
|
|
|
22
21
|
} from "node:fs";
|
|
23
22
|
import { basename, dirname, resolve } from "node:path";
|
|
24
23
|
import { readBindings } from "../hooks/lib/bindings";
|
|
25
|
-
import {
|
|
24
|
+
import { foldHistoryInto } from "../hooks/lib/parked-history";
|
|
26
25
|
import { palHome, paths } from "../hooks/lib/paths";
|
|
27
26
|
import {
|
|
28
27
|
legacyJsonToProgress,
|
|
@@ -55,6 +54,8 @@ interface Migration {
|
|
|
55
54
|
description: string;
|
|
56
55
|
check(): { pending: boolean; detail?: string };
|
|
57
56
|
run(dryRun?: boolean): MigrationResult;
|
|
57
|
+
/** Moves its sources rather than copying them, so there is nothing left to delete. */
|
|
58
|
+
movesSources?: true;
|
|
58
59
|
}
|
|
59
60
|
|
|
60
61
|
// ── v1-projects: JSON progress files → ISA.md ─────────────────────
|
|
@@ -588,15 +589,6 @@ function destinationFor(item: StrandedHistory): string {
|
|
|
588
589
|
: resolve(paths.unboundHistory(), `${item.slug}.jsonl`);
|
|
589
590
|
}
|
|
590
591
|
|
|
591
|
-
/** Union both sides so a destination that already has history keeps it. */
|
|
592
|
-
function foldHistoryInto(target: string, source: string): number {
|
|
593
|
-
const local = existsSync(target) ? readFileSync(target, "utf-8") : "";
|
|
594
|
-
const { text, added } = mergeJsonlLines(local, readFileSync(source, "utf-8"));
|
|
595
|
-
mkdirSync(dirname(target), { recursive: true });
|
|
596
|
-
writeFileSync(target, text, "utf-8");
|
|
597
|
-
return added;
|
|
598
|
-
}
|
|
599
|
-
|
|
600
592
|
function discardEmptyFolder(dir: string): void {
|
|
601
593
|
try {
|
|
602
594
|
if (readdirSync(dir).length === 0) rmdirSync(dir);
|
|
@@ -608,6 +600,7 @@ function discardEmptyFolder(dir: string): void {
|
|
|
608
600
|
const v6HistorySlugs: Migration = {
|
|
609
601
|
id: "v6-history-slugs",
|
|
610
602
|
description: "Re-file session history keyed on a cwd name onto its owning project",
|
|
603
|
+
movesSources: true,
|
|
611
604
|
|
|
612
605
|
check() {
|
|
613
606
|
const stranded = strandedHistory();
|
|
@@ -728,7 +721,7 @@ export function runMigrate(args: string[]): void {
|
|
|
728
721
|
console.log(
|
|
729
722
|
` ${dryRun ? "Would migrate" : "Migrated"}: ${totalMigrated} | Skipped: ${totalSkipped}`
|
|
730
723
|
);
|
|
731
|
-
if (!dryRun && totalMigrated > 0) {
|
|
724
|
+
if (!dryRun && totalMigrated > 0 && pending.some((m) => !m.movesSources)) {
|
|
732
725
|
console.log(" Source files preserved — delete manually once verified.");
|
|
733
726
|
}
|
|
734
727
|
console.log("");
|
|
@@ -20,6 +20,7 @@
|
|
|
20
20
|
import { copyFileSync, existsSync, readFileSync, writeFileSync } from "node:fs";
|
|
21
21
|
import { homedir } from "node:os";
|
|
22
22
|
import { resolve } from "node:path";
|
|
23
|
+
import { adoptParkedHistory } from "./parked-history";
|
|
23
24
|
import { palHome, toPath } from "./paths";
|
|
24
25
|
|
|
25
26
|
/** Project name → absolute path on this machine. */
|
|
@@ -91,6 +92,7 @@ export function writeBindings(bindings: Bindings, home: string = palHome()): voi
|
|
|
91
92
|
const sorted: Bindings = {};
|
|
92
93
|
for (const key of Object.keys(bindings).sort()) sorted[key] = bindings[key];
|
|
93
94
|
writeFileSync(bindingsFilePath(home), `${JSON.stringify(sorted, null, 2)}\n`);
|
|
95
|
+
adoptParkedHistory(sorted, home);
|
|
94
96
|
}
|
|
95
97
|
|
|
96
98
|
/** The absolute path this machine has for `project`, or null when unbound. */
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* History from a directory no project claimed is parked under the directory's
|
|
3
|
+
* name. Once exactly one project is bound to a directory of that name, the
|
|
4
|
+
* parked history is its own and moves in.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import {
|
|
8
|
+
existsSync,
|
|
9
|
+
mkdirSync,
|
|
10
|
+
readdirSync,
|
|
11
|
+
readFileSync,
|
|
12
|
+
unlinkSync,
|
|
13
|
+
writeFileSync,
|
|
14
|
+
} from "node:fs";
|
|
15
|
+
import { basename, dirname, resolve } from "node:path";
|
|
16
|
+
import { mergeJsonlLines } from "./import-merge";
|
|
17
|
+
import { logError } from "./log";
|
|
18
|
+
|
|
19
|
+
type Bindings = Record<string, string>;
|
|
20
|
+
|
|
21
|
+
/** Union both sides so a destination that already has history keeps it. */
|
|
22
|
+
export function foldHistoryInto(target: string, source: string): number {
|
|
23
|
+
const local = existsSync(target) ? readFileSync(target, "utf-8") : "";
|
|
24
|
+
const { text, added } = mergeJsonlLines(local, readFileSync(source, "utf-8"));
|
|
25
|
+
mkdirSync(dirname(target), { recursive: true });
|
|
26
|
+
writeFileSync(target, text, "utf-8");
|
|
27
|
+
return added;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** The project bound to a directory named `slug`, when exactly one is. */
|
|
31
|
+
function projectBoundUnder(slug: string, bindings: Bindings): string | null {
|
|
32
|
+
const owners = Object.entries(bindings).filter(([, path]) => basename(path) === slug);
|
|
33
|
+
return owners.length === 1 ? owners[0][0] : null;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function parkedDir(home: string): string {
|
|
37
|
+
return resolve(home, "memory", "state", "unbound-history");
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function recordedProjectDir(home: string, project: string): string | null {
|
|
41
|
+
const dir = resolve(home, "memory", "projects", project);
|
|
42
|
+
return existsSync(resolve(dir, "ISA.md")) ? dir : null;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function adoptParkedHistory(bindings: Bindings, home: string): void {
|
|
46
|
+
const dir = parkedDir(home);
|
|
47
|
+
if (!existsSync(dir)) return;
|
|
48
|
+
for (const file of readdirSync(dir).filter((f) => f.endsWith(".jsonl"))) {
|
|
49
|
+
const owner = projectBoundUnder(file.slice(0, -".jsonl".length), bindings);
|
|
50
|
+
const projectDir = owner && recordedProjectDir(home, owner);
|
|
51
|
+
if (!projectDir) continue;
|
|
52
|
+
try {
|
|
53
|
+
foldHistoryInto(resolve(projectDir, "history.jsonl"), resolve(dir, file));
|
|
54
|
+
unlinkSync(resolve(dir, file));
|
|
55
|
+
} catch (err) {
|
|
56
|
+
logError("parked-history:adopt", err);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
@@ -267,11 +267,6 @@ export function writeProject(p: ProjectProgress): void {
|
|
|
267
267
|
const detected = detectRemote(p.path);
|
|
268
268
|
if (detected) p.remote = detected;
|
|
269
269
|
}
|
|
270
|
-
// Only a path that exists here may be bound. Saving a record is not a claim
|
|
271
|
-
// about this machine's disk — `path` may have arrived from an imported record
|
|
272
|
-
// written elsewhere, and binding it would recreate the very leak bindings exist
|
|
273
|
-
// to prevent. An explicit `writeBinding` from the user stays unguarded.
|
|
274
|
-
if (p.path && existsSync(p.path)) writeBinding(p.name, p.path);
|
|
275
270
|
const meta: Record<string, unknown> = {
|
|
276
271
|
name: p.name,
|
|
277
272
|
status: p.status,
|
|
@@ -290,6 +285,11 @@ export function writeProject(p: ProjectProgress): void {
|
|
|
290
285
|
if (p.placed) meta.placed = p.placed;
|
|
291
286
|
if (p.placed_by) meta.placed_by = p.placed_by;
|
|
292
287
|
writeFileSync(ensureAndGetIsaFile(p.name), stringify(meta, buildBody(p)), "utf-8");
|
|
288
|
+
// Only a path that exists here may be bound. Saving a record is not a claim
|
|
289
|
+
// about this machine's disk — `path` may have arrived from an imported record
|
|
290
|
+
// written elsewhere, and binding it would recreate the very leak bindings exist
|
|
291
|
+
// to prevent. An explicit `writeBinding` from the user stays unguarded.
|
|
292
|
+
if (p.path && existsSync(p.path)) writeBinding(p.name, p.path);
|
|
293
293
|
}
|
|
294
294
|
|
|
295
295
|
export function deleteProject(name: string): boolean {
|