@sleepy_panda_srl/portulan 0.3.0 → 0.4.1
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/README.md +18 -6
- package/cli/README.md +56 -4
- package/cli/advisory.mjs +167 -53
- package/cli/cache-risk.mjs +69 -0
- package/cli/comments.mjs +8 -5
- package/cli/compile.mjs +492 -146
- package/cli/context.mjs +204 -43
- package/cli/doctor.mjs +211 -33
- package/cli/drills.mjs +32 -7
- package/cli/finish.md +34 -0
- package/cli/finish.mjs +9 -67
- package/cli/form.mjs +35 -19
- package/cli/frontmatter.mjs +39 -0
- package/cli/gate.mjs +14 -12
- package/cli/guidance-files.mjs +335 -0
- package/cli/handoff-policy.mjs +76 -0
- package/cli/html-entities.mjs +2130 -0
- package/cli/import-files.mjs +15 -0
- package/cli/index.mjs +77 -24
- package/cli/init-preview.mjs +553 -0
- package/cli/init.mjs +261 -46
- package/cli/instructions-journal.mjs +1185 -0
- package/cli/instructions.mjs +332 -57
- package/cli/ledger.mjs +123 -37
- package/cli/librarian.mjs +103 -19
- package/cli/persona-routing.mjs +209 -0
- package/cli/plugin-lint.mjs +77 -56
- package/cli/portulan.mjs +7 -2
- package/cli/recipe-output.mjs +91 -0
- package/cli/recipe-runner.md +68 -0
- package/cli/recipe-runner.mjs +131 -0
- package/cli/recipe-set.mjs +12 -3
- package/cli/ritual-contexts.mjs +103 -0
- package/cli/sessions.mjs +2 -1
- package/cli/spend-analysis.mjs +111 -0
- package/cli/spend-offer.md +53 -0
- package/cli/spend-offer.mjs +93 -0
- package/cli/stop-diagnostics.mjs +114 -0
- package/cli/stop-gate.mjs +133 -90
- package/cli/stop-policy.mjs +14 -0
- package/cli/symbols.mjs +55 -8
- package/cli/telemetry.mjs +19 -12
- package/cli/upgrade.mjs +62 -44
- package/cli/vendor.mjs +210 -126
- package/core/operating/context.md +368 -60
- package/core/operating/evolution.md +3 -2
- package/core/operating/loop.md +9 -4
- package/core/operating/sessions.md +6 -15
- package/core/personas/README.md +52 -10
- package/core/personas/implementer.md +3 -0
- package/core/personas/librarian.md +3 -0
- package/core/personas/reviewer.md +3 -0
- package/core/templates/handoff.md +6 -2
- package/evals/ab/personas/README.md +111 -0
- package/evals/ab/personas/acquisition.json +330 -0
- package/evals/ab/personas/bindings/implementer.txt +43 -0
- package/evals/ab/personas/bindings/librarian.txt +40 -0
- package/evals/ab/personas/bindings/reviewer.txt +41 -0
- package/evals/ab/personas/corpus.json +69 -0
- package/evals/ab/personas/grader.mjs +171 -0
- package/evals/ab/personas/implementer.json +774 -0
- package/evals/ab/personas/implementer.md +22 -0
- package/evals/ab/personas/librarian.json +758 -0
- package/evals/ab/personas/librarian.md +22 -0
- package/evals/ab/personas/protocol/continuation.mjs.txt +284 -0
- package/evals/ab/personas/protocol/first-outcome.mjs.txt +227 -0
- package/evals/ab/personas/protocol/first-probe.mjs.txt +172 -0
- package/evals/ab/personas/protocol/publisher.mjs.txt +126 -0
- package/evals/ab/personas/protocol/second-probe.mjs.txt +193 -0
- package/evals/ab/personas/repairs/first-control-answer.txt +23 -0
- package/evals/ab/personas/repairs/grader-before-decode-repair.mjs.txt +143 -0
- package/evals/ab/personas/reviewer.json +758 -0
- package/evals/ab/personas/reviewer.md +22 -0
- package/evals/releases/0.4.0.json +154 -0
- package/evals/releases/0.4.0.md +69 -0
- package/evals/releases/0.4.1.json +154 -0
- package/evals/releases/0.4.1.md +69 -0
- package/package.json +6 -2
- package/packs/rituals/checkpoints/README.md +35 -0
- package/packs/rituals/checkpoints/pack.json +3 -2
- package/packs/tools/local-llm/README.md +46 -6
- package/packs/tools/local-llm/runner/local-llm.mjs +127 -13
- package/packs/tools/local-llm/skills/delegate-local/SKILL.md +26 -2
- package/spec/README.md +37 -4
- package/spec/migrations/0009-instruction-sections-on-read.mjs +60 -11
- package/spec/migrations/0011-persona-bindings-compiled.mjs +23 -0
- package/spec/migrations/README.md +6 -0
- package/spec/pack.schema.json +12 -3
- package/spec/slots.md +262 -117
- package/spec/workspace.schema.json +67 -11
package/cli/vendor.mjs
CHANGED
|
@@ -5,10 +5,14 @@
|
|
|
5
5
|
// Exit 0 done · 1 `doctor` red at an end · 2 could not run.
|
|
6
6
|
|
|
7
7
|
import fs from "node:fs";
|
|
8
|
+
import {createHash,randomUUID} from "node:crypto";
|
|
9
|
+
import {isInside} from "./inside.mjs";
|
|
10
|
+
import {atomicWrite,guardedRemove} from "./guidance-files.mjs";
|
|
11
|
+
import {assertNoSourceJournal,SOURCE_JOURNAL} from "./instructions-journal.mjs";
|
|
8
12
|
import path from "node:path";
|
|
9
13
|
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
10
14
|
|
|
11
|
-
import { agentsMdGuidance, BOOT_CARD_UNIT, CompileError, compileGuidance, GENERATED_DIRS, guidanceUnits } from "./compile.mjs";
|
|
15
|
+
import { agentsMdGuidance, BOOT_CARD_UNIT, CompileError, compileGuidance, GENERATED_DIRS, guidanceUnits, ownedNestedFiles } from "./compile.mjs";
|
|
12
16
|
import { changesReadme, CHANGES_README, handoffIndexIgnore, withIgnoreLines } from "./form.mjs";
|
|
13
17
|
import { inspect } from "./doctor.mjs";
|
|
14
18
|
import { AUTO, namedWithAuto } from "./discover.mjs";
|
|
@@ -536,7 +540,9 @@ export function agentsMd(manifest, host, kernel = null, guidance = null) {
|
|
|
536
540
|
"- **A resolved pointer.** Nothing here fetches anything.",
|
|
537
541
|
...(cardText?.includes("<plugin root>")
|
|
538
542
|
? ["- **The Portulan package.** The card's `<plugin root>` is where it is installed, which this copy is not."]
|
|
539
|
-
:
|
|
543
|
+
: cardText?.includes("portulan finish")
|
|
544
|
+
? ["- **The Portulan package.** The card's `portulan finish` runs from it, and this copy does not carry it."]
|
|
545
|
+
: []),
|
|
540
546
|
"",
|
|
541
547
|
"Run `portulan doctor .portulan` to see where this stands.",
|
|
542
548
|
"",
|
|
@@ -692,9 +698,10 @@ export async function run(argv, options = {}) {
|
|
|
692
698
|
if (sourceStat.isSymbolicLink()) throw new VendorError(`\`${parsed.source}\` is a symlink, and this refuses to read a workspace through one — a manifest reached that way describes some other directory`);
|
|
693
699
|
if (!sourceStat.isDirectory()) throw new VendorError(`\`${parsed.source}\` is not a directory — the source is a workspace directory, the same shape \`doctor\` takes`);
|
|
694
700
|
|
|
695
|
-
let manifest;
|
|
701
|
+
let manifest,manifestBytes;
|
|
696
702
|
try {
|
|
697
|
-
|
|
703
|
+
manifestBytes=fs.readFileSync(path.join(source,"workspace.json"));
|
|
704
|
+
manifest = JSON.parse(manifestBytes);
|
|
698
705
|
} catch (cause) {
|
|
699
706
|
throw new VendorError(
|
|
700
707
|
cause.code === "ENOENT"
|
|
@@ -714,6 +721,8 @@ export async function run(argv, options = {}) {
|
|
|
714
721
|
}
|
|
715
722
|
|
|
716
723
|
const sourceResidence = residenceOf(manifest);
|
|
724
|
+
const checkSourceRecovery=()=>{try{assertNoSourceJournal(source);if(typeof manifest.tree==="string")assertNoSourceJournal(path.resolve(source,manifest.tree));}catch(error){throw new VendorError(error.message);}};
|
|
725
|
+
checkSourceRecovery();
|
|
717
726
|
const dest = path.resolve(parsed.into);
|
|
718
727
|
|
|
719
728
|
if (parsed.residence === "in-repo" && path.basename(dest) !== ".portulan") {
|
|
@@ -736,13 +745,11 @@ export async function run(argv, options = {}) {
|
|
|
736
745
|
}
|
|
737
746
|
|
|
738
747
|
let guidance = null;
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
throw error;
|
|
745
|
-
}
|
|
748
|
+
try {
|
|
749
|
+
guidance = guidanceUnits(source, ".");
|
|
750
|
+
} catch (error) {
|
|
751
|
+
if (error instanceof CompileError) throw new VendorError(error.message);
|
|
752
|
+
throw error;
|
|
746
753
|
}
|
|
747
754
|
|
|
748
755
|
// ---- the scope bound, refused in both its shapes
|
|
@@ -779,10 +786,28 @@ export async function run(argv, options = {}) {
|
|
|
779
786
|
// ---- the destination
|
|
780
787
|
|
|
781
788
|
const carve = parsed.switching ? carveOut(dest, manifest.name) : { allow: new Set(), pointer: null };
|
|
789
|
+
const preserved=new Map(),priorStamps=new Map();
|
|
790
|
+
const priorRoot=fs.existsSync(dest)?{real:fs.realpathSync(dest),stat:fs.statSync(dest)}:null;
|
|
791
|
+
for(const rel of carve.allow){const stamp=fileStamp(dest,rel,priorRoot.real);if(!stamp)throw new VendorError(`${rel} changed during destination snapshot, or is a symlink or nonregular file`);const bytes=readStamped(dest,rel,stamp);priorStamps.set(rel,stamp);preserved.set(rel,bytes);}
|
|
792
|
+
if(carve.pointer&&JSON.stringify(JSON.parse(preserved.get("workspace.json").toString("utf8")))!==JSON.stringify(carve.pointer))throw new VendorError("the destination pointer changed while its replacement was being authorized");
|
|
782
793
|
const walked = walk(source);
|
|
783
|
-
const sourceDirs = directories(source);
|
|
784
|
-
const
|
|
785
|
-
|
|
794
|
+
const sourceDirs = directories(source).map(entry=>{const at=fs.lstatSync(path.join(source,entry.rel));return {...entry,identity:{dev:at.dev,ino:at.ino}};});
|
|
795
|
+
const journals=[...walked,...sourceDirs].filter(file=>file.rel.split("/").some(part=>part.toLowerCase()===SOURCE_JOURNAL));
|
|
796
|
+
if(journals.length)throw new VendorError(`reserved source journal was inventoried: ${journals.map(file=>file.rel).join(", ")}; resume or review the source migration before vendoring`);
|
|
797
|
+
checkSourceRecovery();
|
|
798
|
+
const sourceReal=fs.realpathSync(source),sourceDirectory=fs.statSync(source),observed=new Map();
|
|
799
|
+
if(parsed.switching)for(const file of walked) {
|
|
800
|
+
const stamp=fileStamp(source,file.rel,sourceReal);
|
|
801
|
+
if(stamp===null)throw new VendorError(`${file.rel} could not be inventoried safely before switching`);
|
|
802
|
+
observed.set(file.rel,stamp);
|
|
803
|
+
}
|
|
804
|
+
if(parsed.switching && observed.get("workspace.json")?.at(-1)!==createHash("sha256").update(manifestBytes).digest("hex"))throw new VendorError("the source manifest changed while its inventory was being prepared");
|
|
805
|
+
let nestedOwned;
|
|
806
|
+
try { nestedOwned=ownedNestedFiles(source,"."); } catch(error) { if(error instanceof CompileError) throw new VendorError(error.message); throw error; }
|
|
807
|
+
const files = walked.filter((f) => !isGenerated(f.rel) && !nestedOwned.has(f.rel));
|
|
808
|
+
const unfinished=files.filter(f=>/\.portulan-(?:guidance(?:-claim)?|upgrade|retire)\./.test(path.basename(f.rel)));
|
|
809
|
+
if(unfinished.length)throw new VendorError(`source recovery artifacts were retained: ${unfinished.map(f=>f.rel).join(", ")}; finish or review the source migration before vendoring`);
|
|
810
|
+
const generated = walked.filter((f) => isGenerated(f.rel) || nestedOwned.has(f.rel)).map((f) => f.rel);
|
|
786
811
|
const rels = files.map((f) => f.rel);
|
|
787
812
|
const hostFile = parsed.host === null ? null : path.join(path.dirname(dest), "AGENTS.md");
|
|
788
813
|
|
|
@@ -816,7 +841,8 @@ export async function run(argv, options = {}) {
|
|
|
816
841
|
|
|
817
842
|
const retargeted = retarget(manifest, parsed.residence, parsed.kindOf);
|
|
818
843
|
const oldResidence = source;
|
|
819
|
-
const
|
|
844
|
+
const oldTree=typeof manifest.tree==="string" && manifest.tree.trim() ? path.resolve(source,manifest.tree) : source;
|
|
845
|
+
const repoDir = parsed.residence === "in-repo" ? path.dirname(dest) : oldTree;
|
|
820
846
|
|
|
821
847
|
if (parsed.dryRun) {
|
|
822
848
|
say(`vendor: ${parsed.switching ? "switch" : "vendor"} \`${manifest.name}\` — ${sourceResidence} → ${parsed.residence}`);
|
|
@@ -852,26 +878,34 @@ export async function run(argv, options = {}) {
|
|
|
852
878
|
}
|
|
853
879
|
}
|
|
854
880
|
fs.mkdirSync(staging, { recursive: true });
|
|
855
|
-
|
|
881
|
+
const stagingRoot={real:fs.realpathSync(staging),stat:fs.statSync(staging)},stagedOwned=new Map(),stagedDirs=new Map();
|
|
882
|
+
undo.push(()=>cleanupPublications(staging,stagedOwned,new Map(),stagingRoot,true,stagedDirs));
|
|
883
|
+
const stageWrite=(rel,bytes,mode=null)=>{
|
|
884
|
+
const full=path.join(staging,rel);makeOwnedDirectories(staging,path.dirname(rel),stagedDirs);const fd=fs.openSync(full,"wx"),wanted=Buffer.from(bytes),at=fs.fstatSync(fd);
|
|
885
|
+
stagedOwned.set(rel,[at.dev,at.ino,at.mode,createHash("sha256").update(Buffer.alloc(0)).digest("hex")]);
|
|
886
|
+
try{fs.writeFileSync(fd,bytes);if(mode!==null)fs.fchmodSync(fd,mode);const done=fs.fstatSync(fd);stagedOwned.set(rel,[done.dev,done.ino,done.mode,createHash("sha256").update(wanted).digest("hex")]);}
|
|
887
|
+
catch(error){const partial=fs.fstatSync(fd);stagedOwned.set(rel,[partial.dev,partial.ino,partial.mode,createHash("sha256").update(wanted.subarray(0,partial.size)).digest("hex")]);throw error;}
|
|
888
|
+
finally{fs.closeSync(fd);}
|
|
889
|
+
};
|
|
856
890
|
|
|
857
891
|
for (const entry of sourceDirs) {
|
|
858
892
|
if (isGenerated(entry.rel)) continue;
|
|
859
893
|
const full = path.join(staging, entry.rel);
|
|
860
|
-
|
|
861
|
-
fs.chmodSync(full, entry.mode);
|
|
894
|
+
makeOwnedDirectories(staging,entry.rel,stagedDirs);
|
|
895
|
+
fs.chmodSync(full, entry.mode|0o700);
|
|
862
896
|
}
|
|
863
897
|
// Copied byte for byte: `# portulan:bundle-fallback` lines are not rewritten here; `portulan upgrade` re-derives them.
|
|
864
898
|
for (const file of files) {
|
|
865
899
|
if (file.rel === "workspace.json") continue;
|
|
866
900
|
const full = path.join(staging, file.rel);
|
|
867
|
-
fs.
|
|
868
|
-
fs.writeFileSync(full, fs.readFileSync(path.join(source, file.rel)));
|
|
869
|
-
fs.chmodSync(full, file.mode);
|
|
901
|
+
stageWrite(file.rel,fs.readFileSync(path.join(source,file.rel)),file.mode);
|
|
870
902
|
}
|
|
871
903
|
if (parsed.switching && carve.pointer && !rels.includes("README.md")) {
|
|
872
|
-
|
|
904
|
+
stageWrite("README.md",arrivedReadme(manifest.name));
|
|
873
905
|
}
|
|
874
|
-
|
|
906
|
+
stageWrite("workspace.json",`${JSON.stringify(retargeted,null,2)}\n`);
|
|
907
|
+
const inputTimes=new Map([...stagedOwned.keys()].map(rel=>[rel,fs.lstatSync(path.join(staging,rel),{bigint:true}).ctimeNs]));
|
|
908
|
+
try{guidance=guidanceUnits(staging,".");}catch(error){if(error instanceof CompileError)throw new VendorError(error.message);throw error;}
|
|
875
909
|
if (hostFile !== null) {
|
|
876
910
|
// Not required: a missing kernel is said in the file rather than refusing the run.
|
|
877
911
|
let kernel = null;
|
|
@@ -880,9 +914,13 @@ export async function run(argv, options = {}) {
|
|
|
880
914
|
} catch {
|
|
881
915
|
kernel = null;
|
|
882
916
|
}
|
|
883
|
-
|
|
917
|
+
stageWrite("..AGENTS.md.vendoring",agentsMd(retargeted,parsed.host,kernel,guidance));
|
|
884
918
|
}
|
|
919
|
+
for(const entry of [...sourceDirs].reverse())if(!isGenerated(entry.rel))fs.chmodSync(path.join(staging,entry.rel),entry.mode);
|
|
885
920
|
fault("materialise:files");
|
|
921
|
+
const stagedNow=walk(staging);
|
|
922
|
+
if(stagedNow.length!==stagedOwned.size||stagedNow.some(file=>!sameStamp(fileStamp(staging,file.rel,stagingRoot.real,stagedOwned.get(file.rel)),stagedOwned.get(file.rel))))throw new VendorError("staged material changed before publication; retained files need review");
|
|
923
|
+
if([...inputTimes].some(([rel,time])=>fs.lstatSync(path.join(staging,rel),{bigint:true}).ctimeNs!==time))throw new VendorError("staged source changed while guidance was being validated");
|
|
886
924
|
|
|
887
925
|
// ---- doctor the new residence, BEFORE it is one
|
|
888
926
|
|
|
@@ -898,9 +936,12 @@ export async function run(argv, options = {}) {
|
|
|
898
936
|
say(`vendor: RED at the new residence — nothing was moved, and \`${manifest.name}\` still resides ${sourceResidence === "in-repo" ? "in its repository" : "feed-side"}.`);
|
|
899
937
|
for (const f of staged) say(`vendor: ${f.check} ${f.message}`);
|
|
900
938
|
say("vendor: the switch was refused before it began, which is the point of validating a copy before it is a residence.");
|
|
901
|
-
await unwind(undo);
|
|
939
|
+
const incomplete=await unwind(undo);if(incomplete.length)warn(`vendor: rollback incomplete; retained ${incomplete.join(", ")}`);
|
|
902
940
|
return 1;
|
|
903
941
|
}
|
|
942
|
+
if([...inputTimes].some(([rel,time])=>fs.lstatSync(path.join(staging,rel),{bigint:true}).ctimeNs!==time))throw new VendorError("staged source changed after guidance validation");
|
|
943
|
+
|
|
944
|
+
checkSourceRecovery();
|
|
904
945
|
|
|
905
946
|
// ---- the one rename, and the sentence that precedes it
|
|
906
947
|
|
|
@@ -915,48 +956,42 @@ export async function run(argv, options = {}) {
|
|
|
915
956
|
say("vendor: wrote — and the window closes. If it reports none, the window never opened: delete nothing.");
|
|
916
957
|
}
|
|
917
958
|
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
const
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
if (destWasAbsent) {
|
|
929
|
-
fs.rmSync(dest, { recursive: true, force: true });
|
|
930
|
-
} else {
|
|
931
|
-
for (const rel of written) if (!preserved.has(rel)) fs.rmSync(path.join(dest, rel), { force: true });
|
|
932
|
-
for (const [rel, bytes] of preserved) fs.writeFileSync(path.join(dest, rel), bytes);
|
|
933
|
-
// Emptied directories too: one left behind makes the retry's `carveOut` refuse the destination.
|
|
934
|
-
pruneEmpty(dest);
|
|
935
|
-
}
|
|
959
|
+
if(priorRoot && (fs.statSync(dest).dev!==priorRoot.stat.dev||fs.statSync(dest).ino!==priorRoot.stat.ino||[...priorStamps].some(([rel,stamp])=>!sameStamp(fileStamp(dest,rel,priorRoot.real,stamp),stamp))))throw new VendorError("the destination pointer changed during staging");
|
|
960
|
+
const destWasAbsent=priorRoot===null,destOwned=new Map(),hostOwned=new Map(),hostPrevious=new Map(),destDirs=new Map(),hostDirs=new Map();
|
|
961
|
+
const hostRoot=hostFile===null?null:path.dirname(hostFile),hostIdentity=hostRoot===null?null:{real:fs.realpathSync(hostRoot),stat:fs.statSync(hostRoot)};
|
|
962
|
+
let destRoot=priorRoot;
|
|
963
|
+
const oldBytes=new Map([...preserved].map(([rel,bytes])=>[rel,{bytes,mode:priorStamps.get(rel)[2]}]));
|
|
964
|
+
undo.push(()=>{
|
|
965
|
+
const failures=[];
|
|
966
|
+
if(hostFile!==null && hostOwned.size)failures.push(...cleanupPublications(hostRoot,hostOwned,hostPrevious,hostIdentity,false,hostDirs));
|
|
967
|
+
if(destRoot)failures.push(...cleanupPublications(dest,destOwned,oldBytes,destRoot,destWasAbsent,destDirs));
|
|
968
|
+
return failures;
|
|
936
969
|
});
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
970
|
+
const publishHost=()=>{
|
|
971
|
+
if(hostFile===null)return;
|
|
972
|
+
const stamp=stagedOwned.get("..AGENTS.md.vendoring"),bytes=readStamped(staging,"..AGENTS.md.vendoring",stamp);
|
|
973
|
+
atomicWrite(hostFile,bytes,{exclusive:true,mode:stamp[2],onPublish:id=>hostOwned.set(path.basename(hostFile),[id.dev,id.ino,stamp[2],stamp[3]])});
|
|
974
|
+
guardedRemove(path.join(staging,"..AGENTS.md.vendoring"),{beforeDigest:stamp[3],identity:{dev:stamp[0],ino:stamp[1],mode:stamp[2]}});
|
|
975
|
+
stagedOwned.delete("..AGENTS.md.vendoring");
|
|
976
|
+
};
|
|
977
|
+
if(destWasAbsent) {
|
|
978
|
+
fs.mkdirSync(path.dirname(dest),{recursive:true});publishHost();
|
|
979
|
+
destRoot={real:path.join(fs.realpathSync(path.dirname(dest)),path.basename(dest)),stat:stagingRoot.stat};
|
|
980
|
+
for(const [rel,stamp]of stagedOwned)destOwned.set(rel,stamp);
|
|
981
|
+
for(const [rel,stamp]of stagedDirs)destDirs.set(rel,stamp);
|
|
982
|
+
fs.renameSync(staging,dest);
|
|
942
983
|
} else {
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
}
|
|
948
|
-
for
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
fs.chmodSync(full, file.mode);
|
|
954
|
-
written.push(file.rel);
|
|
955
|
-
}
|
|
956
|
-
if (hostFile !== null) fs.renameSync(path.join(staging, "..AGENTS.md.vendoring"), hostFile);
|
|
957
|
-
fs.renameSync(path.join(staging, "workspace.json"), path.join(dest, "workspace.json"));
|
|
984
|
+
for(const entry of directories(staging)){makeOwnedDirectories(dest,entry.rel,destDirs);if(destDirs.has(entry.rel))fs.chmodSync(path.join(dest,entry.rel),entry.mode|0o700);}
|
|
985
|
+
const publish=rel=>{
|
|
986
|
+
const stamp=stagedOwned.get(rel),prior=priorStamps.get(rel),bytes=readStamped(staging,rel,stamp);
|
|
987
|
+
atomicWrite(path.join(dest,rel),bytes,{exclusive:!prior,beforeDigest:prior?.[3],identity:prior?{dev:prior[0],ino:prior[1],mode:prior[2]}:undefined,mode:stamp[2],onPublish:id=>destOwned.set(rel,[id.dev,id.ino,stamp[2],stamp[3]])});
|
|
988
|
+
};
|
|
989
|
+
for(const rel of stagedOwned.keys())if(rel!=="workspace.json"&&rel!=="..AGENTS.md.vendoring")publish(rel);
|
|
990
|
+
publishHost();publish("workspace.json");
|
|
991
|
+
for(const entry of directories(staging).reverse())if(destDirs.has(entry.rel))fs.chmodSync(path.join(dest,entry.rel),entry.mode);
|
|
992
|
+
const retained=cleanupPublications(staging,stagedOwned,new Map(),stagingRoot,true,stagedDirs);
|
|
993
|
+
if(retained.length)throw new VendorError(`staging cleanup incomplete: ${retained.join(", ")}`);
|
|
958
994
|
}
|
|
959
|
-
fs.rmSync(staging, { recursive: true, force: true });
|
|
960
995
|
fault("materialise:manifest");
|
|
961
996
|
|
|
962
997
|
const records = [];
|
|
@@ -968,12 +1003,9 @@ export async function run(argv, options = {}) {
|
|
|
968
1003
|
if (readmeBlocked.length) {
|
|
969
1004
|
recordsLeft.push(`\`${CHANGES_README}\` ${readmeBlocked[0].why === "already exists" ? "is the tree's own" : readmeBlocked[0].why}`);
|
|
970
1005
|
} else {
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
if (made !== undefined) fs.rmSync(made, { recursive: true, force: true });
|
|
975
|
-
});
|
|
976
|
-
fs.writeFileSync(readme, changesReadme(), { flag: "wx" });
|
|
1006
|
+
makeOwnedDirectories(hostRoot,path.dirname(CHANGES_README),hostDirs);
|
|
1007
|
+
const bytes=changesReadme();
|
|
1008
|
+
atomicWrite(readme,bytes,{exclusive:true,onPublish:id=>hostOwned.set(CHANGES_README,[id.dev,id.ino,id.mode,createHash("sha256").update(bytes).digest("hex")])});
|
|
977
1009
|
records.push(CHANGES_README);
|
|
978
1010
|
}
|
|
979
1011
|
const index = retargeted.handoffs?.index?.path;
|
|
@@ -982,21 +1014,20 @@ export async function run(argv, options = {}) {
|
|
|
982
1014
|
if (typeof index === "string" && blocked.length) recordsLeft.push(`\`.gitignore\` ${blocked[0].why}`);
|
|
983
1015
|
if (typeof index === "string" && blocked.length === 0) {
|
|
984
1016
|
const rel = path.relative(hostRoot, path.resolve(dest, index)).split(path.sep).join("/");
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
if (cause.code !== "ENOENT") throw cause;
|
|
990
|
-
}
|
|
1017
|
+
const prior=fileStamp(hostRoot,".gitignore",hostIdentity.real);
|
|
1018
|
+
let before=null;
|
|
1019
|
+
try {fs.lstatSync(ignore);if(!prior)throw new VendorError(".gitignore changed or is not an ordinary file");before=readStamped(hostRoot,".gitignore",prior);}
|
|
1020
|
+
catch(cause){if(cause.code!=="ENOENT")throw cause;}
|
|
991
1021
|
const next = withIgnoreLines(before, handoffIndexIgnore(rel, path.relative(hostRoot, dest).split(path.sep).join("/")));
|
|
992
1022
|
if (next !== (before ?? "")) {
|
|
993
|
-
|
|
994
|
-
|
|
1023
|
+
if(prior)hostPrevious.set(".gitignore",{bytes:before,mode:prior[2]});
|
|
1024
|
+
atomicWrite(ignore,next,{exclusive:!prior,beforeDigest:prior?.[3],identity:prior?{dev:prior[0],ino:prior[1],mode:prior[2]}:undefined,mode:prior?.[2],onPublish:id=>hostOwned.set(".gitignore",[id.dev,id.ino,id.mode,createHash("sha256").update(next).digest("hex")])});
|
|
995
1025
|
records.push(".gitignore");
|
|
996
1026
|
}
|
|
997
1027
|
}
|
|
998
1028
|
}
|
|
999
1029
|
|
|
1030
|
+
checkSourceRecovery();
|
|
1000
1031
|
if (!parsed.switching) {
|
|
1001
1032
|
say(`vendor: wrote ${files.length + (hostFile ? 1 : 0)} file(s) — the \`${manifest.name}\` workspace at ${display(dest)}${hostFile ? `, and ${display(hostFile)}` : ""}.`);
|
|
1002
1033
|
if (records.length) say(`vendor: and the records the new form keeps, beside it: ${records.map((r) => `\`${r}\``).join(", ")}.`);
|
|
@@ -1009,15 +1040,16 @@ export async function run(argv, options = {}) {
|
|
|
1009
1040
|
// ---- retire the old residence's MANIFEST, which is the act that transfers governance
|
|
1010
1041
|
|
|
1011
1042
|
// Before the material: retiring the files first would leave a governing manifest over a workspace with holes.
|
|
1043
|
+
checkSourceRecovery();
|
|
1012
1044
|
const oldManifest = path.join(oldResidence, "workspace.json");
|
|
1045
|
+
if(!sameStamp(fileStamp(source,"workspace.json",sourceReal,observed.get("workspace.json")),observed.get("workspace.json")))throw new VendorError("the source manifest changed during staging; the switch is refused");
|
|
1013
1046
|
if (leave === "pointer") {
|
|
1014
1047
|
const staged2 = `${oldManifest}.vendoring`;
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
fs.writeFileSync(staged2, `${JSON.stringify(pointerManifest(oldResidence, manifest.name, parsed.feed), null, 2)}\n`);
|
|
1018
|
-
fs.renameSync(staged2, oldManifest);
|
|
1048
|
+
const stamp=observed.get("workspace.json");
|
|
1049
|
+
atomicWrite(oldManifest,`${JSON.stringify(pointerManifest(oldResidence,manifest.name,parsed.feed),null,2)}\n`,{stagingPath:staged2,beforeDigest:stamp[3],identity:{dev:stamp[0],ino:stamp[1],mode:stamp[2]},mode:stamp[2],onPublish:()=>{pastTheFlip=true;}});
|
|
1019
1050
|
} else {
|
|
1020
|
-
|
|
1051
|
+
const stamp=observed.get("workspace.json");
|
|
1052
|
+
guardedRemove(oldManifest,{beforeDigest:stamp[3],identity:{dev:stamp[0],ino:stamp[1],mode:stamp[2]},onRemove:()=>{pastTheFlip=true;}});
|
|
1021
1053
|
}
|
|
1022
1054
|
pastTheFlip = true;
|
|
1023
1055
|
undo.length = 0;
|
|
@@ -1037,18 +1069,23 @@ export async function run(argv, options = {}) {
|
|
|
1037
1069
|
say(`vendor: governance has moved to ${display(dest)}, and \`doctor\` is RED at ${newEnd.length ? "the new" : "the old"} end.`);
|
|
1038
1070
|
for (const f of [...newEnd, ...oldEnd]) say(`vendor: ${f.check} ${f.message}`);
|
|
1039
1071
|
say(`vendor: the old residence's files were NOT removed — green at both ends comes before retirement, and it was not green.`);
|
|
1040
|
-
say(`vendor:
|
|
1072
|
+
say(`vendor: the transfer completed, but validation failed. Preserve both ends and re-run \`doctor\` after fixing what is named above.`);
|
|
1041
1073
|
return 1;
|
|
1042
1074
|
}
|
|
1043
1075
|
|
|
1044
1076
|
// ---- retire the old residence's material — only what this run can account for
|
|
1045
1077
|
|
|
1046
|
-
//
|
|
1078
|
+
// Retire only unchanged files from the initial inventory, including compiled outputs.
|
|
1047
1079
|
const moved = new Set([...rels, ...generated]);
|
|
1048
1080
|
const leftovers = [];
|
|
1081
|
+
const retiredGenerated=[],generatedSet=new Set(generated);
|
|
1082
|
+
const retain=rel=>{if(!leftovers.includes(rel))leftovers.push(rel);};
|
|
1083
|
+
let keptResidence=false,retiredCount=0;
|
|
1049
1084
|
let remaining = [];
|
|
1050
1085
|
let unscannable = null;
|
|
1086
|
+
const sameResidence=()=>{try{const at=fs.statSync(source);return fs.realpathSync(source)===sourceReal && at.dev===sourceDirectory.dev && at.ino===sourceDirectory.ino;}catch{return false;}};
|
|
1051
1087
|
try {
|
|
1088
|
+
if(!sameResidence())throw new VendorError("the old residence changed identity during switching");
|
|
1052
1089
|
remaining = walk(oldResidence);
|
|
1053
1090
|
} catch (cause) {
|
|
1054
1091
|
// A failed scan is not an empty residence: under `--leave nothing` it would delete files this run cannot account for.
|
|
@@ -1056,18 +1093,31 @@ export async function run(argv, options = {}) {
|
|
|
1056
1093
|
}
|
|
1057
1094
|
for (const file of remaining) {
|
|
1058
1095
|
if (file.rel === "workspace.json") continue;
|
|
1059
|
-
if (leave === "pointer" && file.rel === "README.md" && !moved.has("README.md")) continue;
|
|
1060
1096
|
if (!moved.has(file.rel)) {
|
|
1061
|
-
|
|
1097
|
+
retain(file.rel);
|
|
1062
1098
|
continue;
|
|
1063
1099
|
}
|
|
1064
|
-
|
|
1100
|
+
if(!retireObserved(oldResidence,file.rel,observed.get(file.rel),sourceReal)) {retain(file.rel);continue;}
|
|
1101
|
+
retiredCount++;if(generatedSet.has(file.rel))retiredGenerated.push(file.rel);
|
|
1065
1102
|
}
|
|
1066
|
-
if
|
|
1103
|
+
if(unscannable===null) {
|
|
1104
|
+
try {for(const file of walk(oldResidence))if(file.rel!=="workspace.json")retain(file.rel);}
|
|
1105
|
+
catch(cause){unscannable=cause;}
|
|
1106
|
+
}
|
|
1107
|
+
if(unscannable===null && !sameResidence())unscannable=new VendorError("the old residence changed identity during retirement");
|
|
1108
|
+
if (unscannable === null) pruneObservedDirectories(oldResidence,sourceDirs);
|
|
1067
1109
|
if (leave === "pointer") {
|
|
1068
|
-
fs.writeFileSync(path.join(oldResidence,
|
|
1110
|
+
try {fs.writeFileSync(path.join(oldResidence,"README.md"),pointerReadme(manifest.name,parsed.feed,display(dest)),{flag:"wx"});}
|
|
1111
|
+
catch(cause){if(cause.code!=="EEXIST")throw cause;retain("README.md");}
|
|
1069
1112
|
} else if (leftovers.length === 0 && unscannable === null) {
|
|
1070
|
-
fs.
|
|
1113
|
+
try {fs.rmdirSync(oldResidence);}
|
|
1114
|
+
catch(cause) {
|
|
1115
|
+
if(cause.code!=="ENOENT") {
|
|
1116
|
+
keptResidence=true;
|
|
1117
|
+
try {for(const file of walk(oldResidence))retain(file.rel);}
|
|
1118
|
+
catch(error){unscannable=error;}
|
|
1119
|
+
}
|
|
1120
|
+
}
|
|
1071
1121
|
}
|
|
1072
1122
|
fault("retire:material");
|
|
1073
1123
|
|
|
@@ -1075,12 +1125,12 @@ export async function run(argv, options = {}) {
|
|
|
1075
1125
|
say(`vendor: resides at ${display(dest)}, green.`);
|
|
1076
1126
|
if (unscannable !== null) {
|
|
1077
1127
|
say(`vendor: the old residence at ${display(oldResidence)} could NOT be scanned — ${unscannable.message}`);
|
|
1078
|
-
say("vendor: so nothing there was removed.
|
|
1128
|
+
say(retiredCount===0 ? "vendor: so nothing there was removed. Its original manifest is retired or is a pointer," : "vendor: cleanup stopped after some accounted-for files were retired; retained content needs review,");
|
|
1079
1129
|
say("vendor: and nothing here deletes files it could not account for. Clear it by hand when you can read it.");
|
|
1080
1130
|
} else if (leave === "pointer") {
|
|
1081
1131
|
say(`vendor: a pointer at ${display(oldResidence)}, green.`);
|
|
1082
|
-
} else if (leftovers.length) {
|
|
1083
|
-
say(`vendor:
|
|
1132
|
+
} else if (leftovers.length || keptResidence) {
|
|
1133
|
+
say(`vendor: the original manifest at ${display(oldResidence)} is retired; remaining content was preserved for review.`);
|
|
1084
1134
|
} else {
|
|
1085
1135
|
say(`vendor: nothing left at ${display(oldResidence)}.`);
|
|
1086
1136
|
}
|
|
@@ -1088,12 +1138,12 @@ export async function run(argv, options = {}) {
|
|
|
1088
1138
|
say(`vendor: ${leftovers.length} file(s) at the old residence were not moved by this run and were left alone: ${leftovers.join(", ")}.`);
|
|
1089
1139
|
say("vendor: nothing here deletes a file it cannot account for — they are yours to keep or remove.");
|
|
1090
1140
|
}
|
|
1091
|
-
if (
|
|
1092
|
-
say(`vendor: ${
|
|
1141
|
+
if (retiredGenerated.length) {
|
|
1142
|
+
say(`vendor: ${retiredGenerated.length} compiled artifact(s) did not travel and were retired with the old residence: ${retiredGenerated.join(", ")}.`);
|
|
1093
1143
|
say("vendor: enforcement is keyed to the residence — where a settings file lands differs between the two — so a copy would name paths for the residence it left.");
|
|
1094
1144
|
}
|
|
1095
|
-
if (sourceResidence === "in-repo") {
|
|
1096
|
-
say(`vendor: \`${display(path.join(
|
|
1145
|
+
if (sourceResidence === "in-repo" && !isInside(source,oldTree)) {
|
|
1146
|
+
say(`vendor: \`${display(path.join(oldTree,".claude","settings.json"))}\` and \`${display(path.join(oldTree,".claude","rules","portulan"))}\` — where they exist — lie outside the retired workspace. This switch left those paths and their recorded nested targets for you to review.`);
|
|
1097
1147
|
}
|
|
1098
1148
|
if (!parsed.repoRoots.length && cards.length) {
|
|
1099
1149
|
say(`vendor: no --repo-root was given, so the cross-repository half of "green at both ends" reported rather than checked.`);
|
|
@@ -1117,17 +1167,72 @@ export async function run(argv, options = {}) {
|
|
|
1117
1167
|
}
|
|
1118
1168
|
return 0;
|
|
1119
1169
|
} catch (error) {
|
|
1120
|
-
if (!pastTheFlip) await unwind(undo);
|
|
1170
|
+
if (!pastTheFlip) {const incomplete=await unwind(undo);if(incomplete.length)warn(`vendor: rollback incomplete; retained ${incomplete.join(", ")}`);}
|
|
1121
1171
|
const message = error instanceof VendorError ? error.message : `could not run — ${error.code === "EIO" ? error.message : (error.stack ?? error)}`;
|
|
1122
1172
|
warn(`vendor: ${message}`);
|
|
1123
1173
|
if (pastTheFlip) {
|
|
1124
1174
|
warn("vendor: governance had already moved when this failed, so nothing was rolled back — undoing it would re-open the window in the other direction.");
|
|
1125
|
-
warn("vendor:
|
|
1175
|
+
warn("vendor: run `doctor` at both ends to check the current governance and retained files.");
|
|
1126
1176
|
}
|
|
1127
1177
|
return 2;
|
|
1128
1178
|
}
|
|
1129
1179
|
}
|
|
1130
1180
|
|
|
1181
|
+
function makeOwnedDirectories(root,rel,created) {
|
|
1182
|
+
if(rel==="."||rel==="")return;
|
|
1183
|
+
let current="";
|
|
1184
|
+
for(const part of rel.split(path.sep)){current=path.join(current,part);const full=path.join(root,current);try{fs.mkdirSync(full);const at=fs.lstatSync(full);created.set(current,{dev:at.dev,ino:at.ino});}catch(error){if(error.code!=="EEXIST"||!fs.lstatSync(full).isDirectory()||fs.lstatSync(full).isSymbolicLink())throw error;}}
|
|
1185
|
+
}
|
|
1186
|
+
function cleanupPublications(root,published,previous,expectedRoot,removeRoot,created=new Map()) {
|
|
1187
|
+
if(!fs.existsSync(root))return [];
|
|
1188
|
+
if(expectedRoot){const at=fs.statSync(root);if(at.dev!==expectedRoot.stat.dev||at.ino!==expectedRoot.stat.ino||fs.realpathSync(root)!==expectedRoot.real)return [root];}
|
|
1189
|
+
const real=fs.realpathSync(root),failed=[],directoryModes=[];
|
|
1190
|
+
for(const [rel,id]of created){const full=path.join(root,rel);try{const at=fs.lstatSync(full);if(at.isDirectory()&&!at.isSymbolicLink()&&at.dev===id.dev&&at.ino===id.ino){directoryModes.push({full,id,mode:at.mode});if((at.mode&0o700)!==0o700)fs.chmodSync(full,at.mode|0o700);}}catch(error){if(error.code!=="ENOENT")failed.push(full);}}
|
|
1191
|
+
const order=[...published.keys()].sort((a,b)=>a==="workspace.json"?-1:b==="workspace.json"?1:0);
|
|
1192
|
+
for(const rel of order) {
|
|
1193
|
+
const stamp=published.get(rel),full=path.join(root,rel),prior=previous.get(rel);
|
|
1194
|
+
try {
|
|
1195
|
+
if(!sameStamp(fileStamp(root,rel,real,stamp),stamp)){failed.push(full);continue;}
|
|
1196
|
+
const options={beforeDigest:stamp[3],identity:{dev:stamp[0],ino:stamp[1],mode:stamp[2]}};
|
|
1197
|
+
if(prior)atomicWrite(full,prior.bytes,{...options,mode:prior.mode});else guardedRemove(full,options);
|
|
1198
|
+
}catch{failed.push(full);}
|
|
1199
|
+
}
|
|
1200
|
+
for(const [rel,id]of [...created].reverse()){const full=path.join(root,rel);try{const at=fs.lstatSync(full);if(!at.isDirectory()||at.isSymbolicLink()||at.dev!==id.dev||at.ino!==id.ino){failed.push(full);continue;}fs.rmdirSync(full);}catch(error){if(error.code!=="ENOENT")failed.push(full);}}
|
|
1201
|
+
for(const {full,id,mode}of directoryModes)try{const at=fs.lstatSync(full);if(at.isDirectory()&&!at.isSymbolicLink()&&at.dev===id.dev&&at.ino===id.ino)fs.chmodSync(full,mode);}catch(error){if(error.code!=="ENOENT")failed.push(full);}
|
|
1202
|
+
if(removeRoot){try{fs.rmdirSync(root);}catch(error){if(error.code!=="ENOENT")failed.push(root);}}
|
|
1203
|
+
return failed;
|
|
1204
|
+
}
|
|
1205
|
+
|
|
1206
|
+
function fileStamp(root,rel,realRoot,expected=null) {
|
|
1207
|
+
try {
|
|
1208
|
+
const file=path.join(root,rel),at=fs.lstatSync(file);
|
|
1209
|
+
if(!at.isFile() || (expected && (at.dev!==expected[0] || at.ino!==expected[1] || at.mode!==expected[2])) || fs.realpathSync(file)!==path.join(realRoot,rel))return null;
|
|
1210
|
+
return [at.dev,at.ino,at.mode,createHash("sha256").update(fs.readFileSync(file)).digest("hex")];
|
|
1211
|
+
} catch {return null;}
|
|
1212
|
+
}
|
|
1213
|
+
function readStamped(root,rel,stamp) {
|
|
1214
|
+
const full=path.join(root,rel),fd=fs.openSync(full,fs.constants.O_RDONLY|(fs.constants.O_NOFOLLOW??0)|fs.constants.O_NONBLOCK);
|
|
1215
|
+
try {const at=fs.fstatSync(fd),bytes=fs.readFileSync(fd);if(!at.isFile()||!sameStamp([at.dev,at.ino,at.mode,createHash("sha256").update(bytes).digest("hex")],stamp))throw new VendorError(`${rel} changed before publication`);return bytes;}
|
|
1216
|
+
finally{fs.closeSync(fd);}
|
|
1217
|
+
}
|
|
1218
|
+
const sameStamp=(left,right)=>Boolean(left && right && left.every((value,index)=>value===right[index]));
|
|
1219
|
+
function retireObserved(root,rel,expected,realRoot) {
|
|
1220
|
+
if(!sameStamp(fileStamp(root,rel,realRoot,expected),expected))return false;
|
|
1221
|
+
const file=path.join(root,rel),claim=path.join(path.dirname(file),`.${path.basename(file)}.portulan-retire.${process.pid}.${randomUUID()}`);
|
|
1222
|
+
let made=false,moved=false,placeholder;
|
|
1223
|
+
try {
|
|
1224
|
+
const fd=fs.openSync(claim,"wx");made=true;const at=fs.fstatSync(fd);placeholder={dev:at.dev,ino:at.ino};fs.closeSync(fd);
|
|
1225
|
+
try{fs.renameSync(file,claim);moved=true;}catch(error){const held=fs.lstatSync(claim,{throwIfNoEntry:false});if(held&&(held.dev!==placeholder.dev||held.ino!==placeholder.ino)){moved=true;try{fs.linkSync(claim,file);fs.unlinkSync(claim);moved=false;made=false;}catch{throw new VendorError(`${rel} had an uncertain retirement; its data is preserved at ${claim}. Review both paths before cleanup`);}}throw error;}
|
|
1226
|
+
if(!sameStamp(fileStamp(root,path.relative(root,claim),realRoot,expected),expected)) {
|
|
1227
|
+
try {fs.linkSync(claim,file);fs.unlinkSync(claim);moved=false;made=false;}
|
|
1228
|
+
catch {throw new VendorError(`${rel} changed during retirement; its data is preserved at ${claim}. Review both paths before cleanup`);}
|
|
1229
|
+
return false;
|
|
1230
|
+
}
|
|
1231
|
+
fs.unlinkSync(claim);moved=false;made=false;return true;
|
|
1232
|
+
} catch(error){if(!moved&&["EACCES","EPERM","EROFS"].includes(error.code))return false;throw error;}
|
|
1233
|
+
finally {if(made&&!moved){const at=fs.lstatSync(claim,{throwIfNoEntry:false});if(at?.isFile()&&at.dev===placeholder.dev&&at.ino===placeholder.ino&&at.size===0)guardedRemove(claim,{before:Buffer.alloc(0),identity:placeholder});else if(at)throw new VendorError(`${rel} retirement staging changed; retained ${claim} for review`);}}
|
|
1234
|
+
}
|
|
1235
|
+
|
|
1131
1236
|
async function verdict(dir, roots) {
|
|
1132
1237
|
try {
|
|
1133
1238
|
const { findings } = await inspect(dir, roots);
|
|
@@ -1138,34 +1243,13 @@ async function verdict(dir, roots) {
|
|
|
1138
1243
|
}
|
|
1139
1244
|
|
|
1140
1245
|
async function unwind(undo) {
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
step();
|
|
1145
|
-
} catch {
|
|
1146
|
-
/* best effort: the message about the original failure is worth more than this one */
|
|
1147
|
-
}
|
|
1148
|
-
}
|
|
1246
|
+
const failed=[];
|
|
1247
|
+
while(undo.length){try{const result=await undo.pop()();if(Array.isArray(result))failed.push(...result);}catch(error){failed.push(error.message);}}
|
|
1248
|
+
return failed;
|
|
1149
1249
|
}
|
|
1150
1250
|
|
|
1151
|
-
function
|
|
1152
|
-
const
|
|
1153
|
-
let entries;
|
|
1154
|
-
try {
|
|
1155
|
-
entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
1156
|
-
} catch {
|
|
1157
|
-
return;
|
|
1158
|
-
}
|
|
1159
|
-
for (const entry of entries) if (entry.isDirectory()) walkDown(path.join(dir, entry.name));
|
|
1160
|
-
if (dir !== root) {
|
|
1161
|
-
try {
|
|
1162
|
-
if (fs.readdirSync(dir).length === 0) fs.rmdirSync(dir);
|
|
1163
|
-
} catch {
|
|
1164
|
-
/* a directory that will not go is one somebody still has something in */
|
|
1165
|
-
}
|
|
1166
|
-
}
|
|
1167
|
-
};
|
|
1168
|
-
walkDown(path.resolve(root));
|
|
1251
|
+
function pruneObservedDirectories(root,observed) {
|
|
1252
|
+
for(const entry of [...observed].reverse())try{const full=path.join(root,entry.rel),at=fs.lstatSync(full);if(at.isDirectory()&&!at.isSymbolicLink()&&at.dev===entry.identity.dev&&at.ino===entry.identity.ino)fs.rmdirSync(full);}catch{}
|
|
1169
1253
|
}
|
|
1170
1254
|
|
|
1171
1255
|
// `?? ""`: `process.argv[1]` is absent when a non-script imports this, and `pathToFileURL(undefined)` throws.
|