@sleepy_panda_srl/portulan 0.3.0 → 0.4.0
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 +15 -4
- package/cli/README.md +55 -3
- 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 +8 -67
- package/cli/form.mjs +34 -18
- 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/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 +207 -125
- 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/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";
|
|
@@ -692,9 +696,10 @@ export async function run(argv, options = {}) {
|
|
|
692
696
|
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
697
|
if (!sourceStat.isDirectory()) throw new VendorError(`\`${parsed.source}\` is not a directory — the source is a workspace directory, the same shape \`doctor\` takes`);
|
|
694
698
|
|
|
695
|
-
let manifest;
|
|
699
|
+
let manifest,manifestBytes;
|
|
696
700
|
try {
|
|
697
|
-
|
|
701
|
+
manifestBytes=fs.readFileSync(path.join(source,"workspace.json"));
|
|
702
|
+
manifest = JSON.parse(manifestBytes);
|
|
698
703
|
} catch (cause) {
|
|
699
704
|
throw new VendorError(
|
|
700
705
|
cause.code === "ENOENT"
|
|
@@ -714,6 +719,8 @@ export async function run(argv, options = {}) {
|
|
|
714
719
|
}
|
|
715
720
|
|
|
716
721
|
const sourceResidence = residenceOf(manifest);
|
|
722
|
+
const checkSourceRecovery=()=>{try{assertNoSourceJournal(source);if(typeof manifest.tree==="string")assertNoSourceJournal(path.resolve(source,manifest.tree));}catch(error){throw new VendorError(error.message);}};
|
|
723
|
+
checkSourceRecovery();
|
|
717
724
|
const dest = path.resolve(parsed.into);
|
|
718
725
|
|
|
719
726
|
if (parsed.residence === "in-repo" && path.basename(dest) !== ".portulan") {
|
|
@@ -736,13 +743,11 @@ export async function run(argv, options = {}) {
|
|
|
736
743
|
}
|
|
737
744
|
|
|
738
745
|
let guidance = null;
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
throw error;
|
|
745
|
-
}
|
|
746
|
+
try {
|
|
747
|
+
guidance = guidanceUnits(source, ".");
|
|
748
|
+
} catch (error) {
|
|
749
|
+
if (error instanceof CompileError) throw new VendorError(error.message);
|
|
750
|
+
throw error;
|
|
746
751
|
}
|
|
747
752
|
|
|
748
753
|
// ---- the scope bound, refused in both its shapes
|
|
@@ -779,10 +784,28 @@ export async function run(argv, options = {}) {
|
|
|
779
784
|
// ---- the destination
|
|
780
785
|
|
|
781
786
|
const carve = parsed.switching ? carveOut(dest, manifest.name) : { allow: new Set(), pointer: null };
|
|
787
|
+
const preserved=new Map(),priorStamps=new Map();
|
|
788
|
+
const priorRoot=fs.existsSync(dest)?{real:fs.realpathSync(dest),stat:fs.statSync(dest)}:null;
|
|
789
|
+
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);}
|
|
790
|
+
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
791
|
const walked = walk(source);
|
|
783
|
-
const sourceDirs = directories(source);
|
|
784
|
-
const
|
|
785
|
-
|
|
792
|
+
const sourceDirs = directories(source).map(entry=>{const at=fs.lstatSync(path.join(source,entry.rel));return {...entry,identity:{dev:at.dev,ino:at.ino}};});
|
|
793
|
+
const journals=[...walked,...sourceDirs].filter(file=>file.rel.split("/").some(part=>part.toLowerCase()===SOURCE_JOURNAL));
|
|
794
|
+
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`);
|
|
795
|
+
checkSourceRecovery();
|
|
796
|
+
const sourceReal=fs.realpathSync(source),sourceDirectory=fs.statSync(source),observed=new Map();
|
|
797
|
+
if(parsed.switching)for(const file of walked) {
|
|
798
|
+
const stamp=fileStamp(source,file.rel,sourceReal);
|
|
799
|
+
if(stamp===null)throw new VendorError(`${file.rel} could not be inventoried safely before switching`);
|
|
800
|
+
observed.set(file.rel,stamp);
|
|
801
|
+
}
|
|
802
|
+
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");
|
|
803
|
+
let nestedOwned;
|
|
804
|
+
try { nestedOwned=ownedNestedFiles(source,"."); } catch(error) { if(error instanceof CompileError) throw new VendorError(error.message); throw error; }
|
|
805
|
+
const files = walked.filter((f) => !isGenerated(f.rel) && !nestedOwned.has(f.rel));
|
|
806
|
+
const unfinished=files.filter(f=>/\.portulan-(?:guidance(?:-claim)?|upgrade|retire)\./.test(path.basename(f.rel)));
|
|
807
|
+
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`);
|
|
808
|
+
const generated = walked.filter((f) => isGenerated(f.rel) || nestedOwned.has(f.rel)).map((f) => f.rel);
|
|
786
809
|
const rels = files.map((f) => f.rel);
|
|
787
810
|
const hostFile = parsed.host === null ? null : path.join(path.dirname(dest), "AGENTS.md");
|
|
788
811
|
|
|
@@ -816,7 +839,8 @@ export async function run(argv, options = {}) {
|
|
|
816
839
|
|
|
817
840
|
const retargeted = retarget(manifest, parsed.residence, parsed.kindOf);
|
|
818
841
|
const oldResidence = source;
|
|
819
|
-
const
|
|
842
|
+
const oldTree=typeof manifest.tree==="string" && manifest.tree.trim() ? path.resolve(source,manifest.tree) : source;
|
|
843
|
+
const repoDir = parsed.residence === "in-repo" ? path.dirname(dest) : oldTree;
|
|
820
844
|
|
|
821
845
|
if (parsed.dryRun) {
|
|
822
846
|
say(`vendor: ${parsed.switching ? "switch" : "vendor"} \`${manifest.name}\` — ${sourceResidence} → ${parsed.residence}`);
|
|
@@ -852,26 +876,34 @@ export async function run(argv, options = {}) {
|
|
|
852
876
|
}
|
|
853
877
|
}
|
|
854
878
|
fs.mkdirSync(staging, { recursive: true });
|
|
855
|
-
|
|
879
|
+
const stagingRoot={real:fs.realpathSync(staging),stat:fs.statSync(staging)},stagedOwned=new Map(),stagedDirs=new Map();
|
|
880
|
+
undo.push(()=>cleanupPublications(staging,stagedOwned,new Map(),stagingRoot,true,stagedDirs));
|
|
881
|
+
const stageWrite=(rel,bytes,mode=null)=>{
|
|
882
|
+
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);
|
|
883
|
+
stagedOwned.set(rel,[at.dev,at.ino,at.mode,createHash("sha256").update(Buffer.alloc(0)).digest("hex")]);
|
|
884
|
+
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")]);}
|
|
885
|
+
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;}
|
|
886
|
+
finally{fs.closeSync(fd);}
|
|
887
|
+
};
|
|
856
888
|
|
|
857
889
|
for (const entry of sourceDirs) {
|
|
858
890
|
if (isGenerated(entry.rel)) continue;
|
|
859
891
|
const full = path.join(staging, entry.rel);
|
|
860
|
-
|
|
861
|
-
fs.chmodSync(full, entry.mode);
|
|
892
|
+
makeOwnedDirectories(staging,entry.rel,stagedDirs);
|
|
893
|
+
fs.chmodSync(full, entry.mode|0o700);
|
|
862
894
|
}
|
|
863
895
|
// Copied byte for byte: `# portulan:bundle-fallback` lines are not rewritten here; `portulan upgrade` re-derives them.
|
|
864
896
|
for (const file of files) {
|
|
865
897
|
if (file.rel === "workspace.json") continue;
|
|
866
898
|
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);
|
|
899
|
+
stageWrite(file.rel,fs.readFileSync(path.join(source,file.rel)),file.mode);
|
|
870
900
|
}
|
|
871
901
|
if (parsed.switching && carve.pointer && !rels.includes("README.md")) {
|
|
872
|
-
|
|
902
|
+
stageWrite("README.md",arrivedReadme(manifest.name));
|
|
873
903
|
}
|
|
874
|
-
|
|
904
|
+
stageWrite("workspace.json",`${JSON.stringify(retargeted,null,2)}\n`);
|
|
905
|
+
const inputTimes=new Map([...stagedOwned.keys()].map(rel=>[rel,fs.lstatSync(path.join(staging,rel),{bigint:true}).ctimeNs]));
|
|
906
|
+
try{guidance=guidanceUnits(staging,".");}catch(error){if(error instanceof CompileError)throw new VendorError(error.message);throw error;}
|
|
875
907
|
if (hostFile !== null) {
|
|
876
908
|
// Not required: a missing kernel is said in the file rather than refusing the run.
|
|
877
909
|
let kernel = null;
|
|
@@ -880,9 +912,13 @@ export async function run(argv, options = {}) {
|
|
|
880
912
|
} catch {
|
|
881
913
|
kernel = null;
|
|
882
914
|
}
|
|
883
|
-
|
|
915
|
+
stageWrite("..AGENTS.md.vendoring",agentsMd(retargeted,parsed.host,kernel,guidance));
|
|
884
916
|
}
|
|
917
|
+
for(const entry of [...sourceDirs].reverse())if(!isGenerated(entry.rel))fs.chmodSync(path.join(staging,entry.rel),entry.mode);
|
|
885
918
|
fault("materialise:files");
|
|
919
|
+
const stagedNow=walk(staging);
|
|
920
|
+
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");
|
|
921
|
+
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
922
|
|
|
887
923
|
// ---- doctor the new residence, BEFORE it is one
|
|
888
924
|
|
|
@@ -898,9 +934,12 @@ export async function run(argv, options = {}) {
|
|
|
898
934
|
say(`vendor: RED at the new residence — nothing was moved, and \`${manifest.name}\` still resides ${sourceResidence === "in-repo" ? "in its repository" : "feed-side"}.`);
|
|
899
935
|
for (const f of staged) say(`vendor: ${f.check} ${f.message}`);
|
|
900
936
|
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);
|
|
937
|
+
const incomplete=await unwind(undo);if(incomplete.length)warn(`vendor: rollback incomplete; retained ${incomplete.join(", ")}`);
|
|
902
938
|
return 1;
|
|
903
939
|
}
|
|
940
|
+
if([...inputTimes].some(([rel,time])=>fs.lstatSync(path.join(staging,rel),{bigint:true}).ctimeNs!==time))throw new VendorError("staged source changed after guidance validation");
|
|
941
|
+
|
|
942
|
+
checkSourceRecovery();
|
|
904
943
|
|
|
905
944
|
// ---- the one rename, and the sentence that precedes it
|
|
906
945
|
|
|
@@ -915,48 +954,42 @@ export async function run(argv, options = {}) {
|
|
|
915
954
|
say("vendor: wrote — and the window closes. If it reports none, the window never opened: delete nothing.");
|
|
916
955
|
}
|
|
917
956
|
|
|
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
|
-
}
|
|
957
|
+
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");
|
|
958
|
+
const destWasAbsent=priorRoot===null,destOwned=new Map(),hostOwned=new Map(),hostPrevious=new Map(),destDirs=new Map(),hostDirs=new Map();
|
|
959
|
+
const hostRoot=hostFile===null?null:path.dirname(hostFile),hostIdentity=hostRoot===null?null:{real:fs.realpathSync(hostRoot),stat:fs.statSync(hostRoot)};
|
|
960
|
+
let destRoot=priorRoot;
|
|
961
|
+
const oldBytes=new Map([...preserved].map(([rel,bytes])=>[rel,{bytes,mode:priorStamps.get(rel)[2]}]));
|
|
962
|
+
undo.push(()=>{
|
|
963
|
+
const failures=[];
|
|
964
|
+
if(hostFile!==null && hostOwned.size)failures.push(...cleanupPublications(hostRoot,hostOwned,hostPrevious,hostIdentity,false,hostDirs));
|
|
965
|
+
if(destRoot)failures.push(...cleanupPublications(dest,destOwned,oldBytes,destRoot,destWasAbsent,destDirs));
|
|
966
|
+
return failures;
|
|
936
967
|
});
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
968
|
+
const publishHost=()=>{
|
|
969
|
+
if(hostFile===null)return;
|
|
970
|
+
const stamp=stagedOwned.get("..AGENTS.md.vendoring"),bytes=readStamped(staging,"..AGENTS.md.vendoring",stamp);
|
|
971
|
+
atomicWrite(hostFile,bytes,{exclusive:true,mode:stamp[2],onPublish:id=>hostOwned.set(path.basename(hostFile),[id.dev,id.ino,stamp[2],stamp[3]])});
|
|
972
|
+
guardedRemove(path.join(staging,"..AGENTS.md.vendoring"),{beforeDigest:stamp[3],identity:{dev:stamp[0],ino:stamp[1],mode:stamp[2]}});
|
|
973
|
+
stagedOwned.delete("..AGENTS.md.vendoring");
|
|
974
|
+
};
|
|
975
|
+
if(destWasAbsent) {
|
|
976
|
+
fs.mkdirSync(path.dirname(dest),{recursive:true});publishHost();
|
|
977
|
+
destRoot={real:path.join(fs.realpathSync(path.dirname(dest)),path.basename(dest)),stat:stagingRoot.stat};
|
|
978
|
+
for(const [rel,stamp]of stagedOwned)destOwned.set(rel,stamp);
|
|
979
|
+
for(const [rel,stamp]of stagedDirs)destDirs.set(rel,stamp);
|
|
980
|
+
fs.renameSync(staging,dest);
|
|
942
981
|
} 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"));
|
|
982
|
+
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);}
|
|
983
|
+
const publish=rel=>{
|
|
984
|
+
const stamp=stagedOwned.get(rel),prior=priorStamps.get(rel),bytes=readStamped(staging,rel,stamp);
|
|
985
|
+
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]])});
|
|
986
|
+
};
|
|
987
|
+
for(const rel of stagedOwned.keys())if(rel!=="workspace.json"&&rel!=="..AGENTS.md.vendoring")publish(rel);
|
|
988
|
+
publishHost();publish("workspace.json");
|
|
989
|
+
for(const entry of directories(staging).reverse())if(destDirs.has(entry.rel))fs.chmodSync(path.join(dest,entry.rel),entry.mode);
|
|
990
|
+
const retained=cleanupPublications(staging,stagedOwned,new Map(),stagingRoot,true,stagedDirs);
|
|
991
|
+
if(retained.length)throw new VendorError(`staging cleanup incomplete: ${retained.join(", ")}`);
|
|
958
992
|
}
|
|
959
|
-
fs.rmSync(staging, { recursive: true, force: true });
|
|
960
993
|
fault("materialise:manifest");
|
|
961
994
|
|
|
962
995
|
const records = [];
|
|
@@ -968,12 +1001,9 @@ export async function run(argv, options = {}) {
|
|
|
968
1001
|
if (readmeBlocked.length) {
|
|
969
1002
|
recordsLeft.push(`\`${CHANGES_README}\` ${readmeBlocked[0].why === "already exists" ? "is the tree's own" : readmeBlocked[0].why}`);
|
|
970
1003
|
} else {
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
if (made !== undefined) fs.rmSync(made, { recursive: true, force: true });
|
|
975
|
-
});
|
|
976
|
-
fs.writeFileSync(readme, changesReadme(), { flag: "wx" });
|
|
1004
|
+
makeOwnedDirectories(hostRoot,path.dirname(CHANGES_README),hostDirs);
|
|
1005
|
+
const bytes=changesReadme();
|
|
1006
|
+
atomicWrite(readme,bytes,{exclusive:true,onPublish:id=>hostOwned.set(CHANGES_README,[id.dev,id.ino,id.mode,createHash("sha256").update(bytes).digest("hex")])});
|
|
977
1007
|
records.push(CHANGES_README);
|
|
978
1008
|
}
|
|
979
1009
|
const index = retargeted.handoffs?.index?.path;
|
|
@@ -982,21 +1012,20 @@ export async function run(argv, options = {}) {
|
|
|
982
1012
|
if (typeof index === "string" && blocked.length) recordsLeft.push(`\`.gitignore\` ${blocked[0].why}`);
|
|
983
1013
|
if (typeof index === "string" && blocked.length === 0) {
|
|
984
1014
|
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
|
-
}
|
|
1015
|
+
const prior=fileStamp(hostRoot,".gitignore",hostIdentity.real);
|
|
1016
|
+
let before=null;
|
|
1017
|
+
try {fs.lstatSync(ignore);if(!prior)throw new VendorError(".gitignore changed or is not an ordinary file");before=readStamped(hostRoot,".gitignore",prior);}
|
|
1018
|
+
catch(cause){if(cause.code!=="ENOENT")throw cause;}
|
|
991
1019
|
const next = withIgnoreLines(before, handoffIndexIgnore(rel, path.relative(hostRoot, dest).split(path.sep).join("/")));
|
|
992
1020
|
if (next !== (before ?? "")) {
|
|
993
|
-
|
|
994
|
-
|
|
1021
|
+
if(prior)hostPrevious.set(".gitignore",{bytes:before,mode:prior[2]});
|
|
1022
|
+
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
1023
|
records.push(".gitignore");
|
|
996
1024
|
}
|
|
997
1025
|
}
|
|
998
1026
|
}
|
|
999
1027
|
|
|
1028
|
+
checkSourceRecovery();
|
|
1000
1029
|
if (!parsed.switching) {
|
|
1001
1030
|
say(`vendor: wrote ${files.length + (hostFile ? 1 : 0)} file(s) — the \`${manifest.name}\` workspace at ${display(dest)}${hostFile ? `, and ${display(hostFile)}` : ""}.`);
|
|
1002
1031
|
if (records.length) say(`vendor: and the records the new form keeps, beside it: ${records.map((r) => `\`${r}\``).join(", ")}.`);
|
|
@@ -1009,15 +1038,16 @@ export async function run(argv, options = {}) {
|
|
|
1009
1038
|
// ---- retire the old residence's MANIFEST, which is the act that transfers governance
|
|
1010
1039
|
|
|
1011
1040
|
// Before the material: retiring the files first would leave a governing manifest over a workspace with holes.
|
|
1041
|
+
checkSourceRecovery();
|
|
1012
1042
|
const oldManifest = path.join(oldResidence, "workspace.json");
|
|
1043
|
+
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
1044
|
if (leave === "pointer") {
|
|
1014
1045
|
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);
|
|
1046
|
+
const stamp=observed.get("workspace.json");
|
|
1047
|
+
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
1048
|
} else {
|
|
1020
|
-
|
|
1049
|
+
const stamp=observed.get("workspace.json");
|
|
1050
|
+
guardedRemove(oldManifest,{beforeDigest:stamp[3],identity:{dev:stamp[0],ino:stamp[1],mode:stamp[2]},onRemove:()=>{pastTheFlip=true;}});
|
|
1021
1051
|
}
|
|
1022
1052
|
pastTheFlip = true;
|
|
1023
1053
|
undo.length = 0;
|
|
@@ -1037,18 +1067,23 @@ export async function run(argv, options = {}) {
|
|
|
1037
1067
|
say(`vendor: governance has moved to ${display(dest)}, and \`doctor\` is RED at ${newEnd.length ? "the new" : "the old"} end.`);
|
|
1038
1068
|
for (const f of [...newEnd, ...oldEnd]) say(`vendor: ${f.check} ${f.message}`);
|
|
1039
1069
|
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:
|
|
1070
|
+
say(`vendor: the transfer completed, but validation failed. Preserve both ends and re-run \`doctor\` after fixing what is named above.`);
|
|
1041
1071
|
return 1;
|
|
1042
1072
|
}
|
|
1043
1073
|
|
|
1044
1074
|
// ---- retire the old residence's material — only what this run can account for
|
|
1045
1075
|
|
|
1046
|
-
//
|
|
1076
|
+
// Retire only unchanged files from the initial inventory, including compiled outputs.
|
|
1047
1077
|
const moved = new Set([...rels, ...generated]);
|
|
1048
1078
|
const leftovers = [];
|
|
1079
|
+
const retiredGenerated=[],generatedSet=new Set(generated);
|
|
1080
|
+
const retain=rel=>{if(!leftovers.includes(rel))leftovers.push(rel);};
|
|
1081
|
+
let keptResidence=false,retiredCount=0;
|
|
1049
1082
|
let remaining = [];
|
|
1050
1083
|
let unscannable = null;
|
|
1084
|
+
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
1085
|
try {
|
|
1086
|
+
if(!sameResidence())throw new VendorError("the old residence changed identity during switching");
|
|
1052
1087
|
remaining = walk(oldResidence);
|
|
1053
1088
|
} catch (cause) {
|
|
1054
1089
|
// A failed scan is not an empty residence: under `--leave nothing` it would delete files this run cannot account for.
|
|
@@ -1056,18 +1091,31 @@ export async function run(argv, options = {}) {
|
|
|
1056
1091
|
}
|
|
1057
1092
|
for (const file of remaining) {
|
|
1058
1093
|
if (file.rel === "workspace.json") continue;
|
|
1059
|
-
if (leave === "pointer" && file.rel === "README.md" && !moved.has("README.md")) continue;
|
|
1060
1094
|
if (!moved.has(file.rel)) {
|
|
1061
|
-
|
|
1095
|
+
retain(file.rel);
|
|
1062
1096
|
continue;
|
|
1063
1097
|
}
|
|
1064
|
-
|
|
1098
|
+
if(!retireObserved(oldResidence,file.rel,observed.get(file.rel),sourceReal)) {retain(file.rel);continue;}
|
|
1099
|
+
retiredCount++;if(generatedSet.has(file.rel))retiredGenerated.push(file.rel);
|
|
1065
1100
|
}
|
|
1066
|
-
if
|
|
1101
|
+
if(unscannable===null) {
|
|
1102
|
+
try {for(const file of walk(oldResidence))if(file.rel!=="workspace.json")retain(file.rel);}
|
|
1103
|
+
catch(cause){unscannable=cause;}
|
|
1104
|
+
}
|
|
1105
|
+
if(unscannable===null && !sameResidence())unscannable=new VendorError("the old residence changed identity during retirement");
|
|
1106
|
+
if (unscannable === null) pruneObservedDirectories(oldResidence,sourceDirs);
|
|
1067
1107
|
if (leave === "pointer") {
|
|
1068
|
-
fs.writeFileSync(path.join(oldResidence,
|
|
1108
|
+
try {fs.writeFileSync(path.join(oldResidence,"README.md"),pointerReadme(manifest.name,parsed.feed,display(dest)),{flag:"wx"});}
|
|
1109
|
+
catch(cause){if(cause.code!=="EEXIST")throw cause;retain("README.md");}
|
|
1069
1110
|
} else if (leftovers.length === 0 && unscannable === null) {
|
|
1070
|
-
fs.
|
|
1111
|
+
try {fs.rmdirSync(oldResidence);}
|
|
1112
|
+
catch(cause) {
|
|
1113
|
+
if(cause.code!=="ENOENT") {
|
|
1114
|
+
keptResidence=true;
|
|
1115
|
+
try {for(const file of walk(oldResidence))retain(file.rel);}
|
|
1116
|
+
catch(error){unscannable=error;}
|
|
1117
|
+
}
|
|
1118
|
+
}
|
|
1071
1119
|
}
|
|
1072
1120
|
fault("retire:material");
|
|
1073
1121
|
|
|
@@ -1075,12 +1123,12 @@ export async function run(argv, options = {}) {
|
|
|
1075
1123
|
say(`vendor: resides at ${display(dest)}, green.`);
|
|
1076
1124
|
if (unscannable !== null) {
|
|
1077
1125
|
say(`vendor: the old residence at ${display(oldResidence)} could NOT be scanned — ${unscannable.message}`);
|
|
1078
|
-
say("vendor: so nothing there was removed.
|
|
1126
|
+
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
1127
|
say("vendor: and nothing here deletes files it could not account for. Clear it by hand when you can read it.");
|
|
1080
1128
|
} else if (leave === "pointer") {
|
|
1081
1129
|
say(`vendor: a pointer at ${display(oldResidence)}, green.`);
|
|
1082
|
-
} else if (leftovers.length) {
|
|
1083
|
-
say(`vendor:
|
|
1130
|
+
} else if (leftovers.length || keptResidence) {
|
|
1131
|
+
say(`vendor: the original manifest at ${display(oldResidence)} is retired; remaining content was preserved for review.`);
|
|
1084
1132
|
} else {
|
|
1085
1133
|
say(`vendor: nothing left at ${display(oldResidence)}.`);
|
|
1086
1134
|
}
|
|
@@ -1088,12 +1136,12 @@ export async function run(argv, options = {}) {
|
|
|
1088
1136
|
say(`vendor: ${leftovers.length} file(s) at the old residence were not moved by this run and were left alone: ${leftovers.join(", ")}.`);
|
|
1089
1137
|
say("vendor: nothing here deletes a file it cannot account for — they are yours to keep or remove.");
|
|
1090
1138
|
}
|
|
1091
|
-
if (
|
|
1092
|
-
say(`vendor: ${
|
|
1139
|
+
if (retiredGenerated.length) {
|
|
1140
|
+
say(`vendor: ${retiredGenerated.length} compiled artifact(s) did not travel and were retired with the old residence: ${retiredGenerated.join(", ")}.`);
|
|
1093
1141
|
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
1142
|
}
|
|
1095
|
-
if (sourceResidence === "in-repo") {
|
|
1096
|
-
say(`vendor: \`${display(path.join(
|
|
1143
|
+
if (sourceResidence === "in-repo" && !isInside(source,oldTree)) {
|
|
1144
|
+
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
1145
|
}
|
|
1098
1146
|
if (!parsed.repoRoots.length && cards.length) {
|
|
1099
1147
|
say(`vendor: no --repo-root was given, so the cross-repository half of "green at both ends" reported rather than checked.`);
|
|
@@ -1117,17 +1165,72 @@ export async function run(argv, options = {}) {
|
|
|
1117
1165
|
}
|
|
1118
1166
|
return 0;
|
|
1119
1167
|
} catch (error) {
|
|
1120
|
-
if (!pastTheFlip) await unwind(undo);
|
|
1168
|
+
if (!pastTheFlip) {const incomplete=await unwind(undo);if(incomplete.length)warn(`vendor: rollback incomplete; retained ${incomplete.join(", ")}`);}
|
|
1121
1169
|
const message = error instanceof VendorError ? error.message : `could not run — ${error.code === "EIO" ? error.message : (error.stack ?? error)}`;
|
|
1122
1170
|
warn(`vendor: ${message}`);
|
|
1123
1171
|
if (pastTheFlip) {
|
|
1124
1172
|
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:
|
|
1173
|
+
warn("vendor: run `doctor` at both ends to check the current governance and retained files.");
|
|
1126
1174
|
}
|
|
1127
1175
|
return 2;
|
|
1128
1176
|
}
|
|
1129
1177
|
}
|
|
1130
1178
|
|
|
1179
|
+
function makeOwnedDirectories(root,rel,created) {
|
|
1180
|
+
if(rel==="."||rel==="")return;
|
|
1181
|
+
let current="";
|
|
1182
|
+
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;}}
|
|
1183
|
+
}
|
|
1184
|
+
function cleanupPublications(root,published,previous,expectedRoot,removeRoot,created=new Map()) {
|
|
1185
|
+
if(!fs.existsSync(root))return [];
|
|
1186
|
+
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];}
|
|
1187
|
+
const real=fs.realpathSync(root),failed=[],directoryModes=[];
|
|
1188
|
+
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);}}
|
|
1189
|
+
const order=[...published.keys()].sort((a,b)=>a==="workspace.json"?-1:b==="workspace.json"?1:0);
|
|
1190
|
+
for(const rel of order) {
|
|
1191
|
+
const stamp=published.get(rel),full=path.join(root,rel),prior=previous.get(rel);
|
|
1192
|
+
try {
|
|
1193
|
+
if(!sameStamp(fileStamp(root,rel,real,stamp),stamp)){failed.push(full);continue;}
|
|
1194
|
+
const options={beforeDigest:stamp[3],identity:{dev:stamp[0],ino:stamp[1],mode:stamp[2]}};
|
|
1195
|
+
if(prior)atomicWrite(full,prior.bytes,{...options,mode:prior.mode});else guardedRemove(full,options);
|
|
1196
|
+
}catch{failed.push(full);}
|
|
1197
|
+
}
|
|
1198
|
+
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);}}
|
|
1199
|
+
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);}
|
|
1200
|
+
if(removeRoot){try{fs.rmdirSync(root);}catch(error){if(error.code!=="ENOENT")failed.push(root);}}
|
|
1201
|
+
return failed;
|
|
1202
|
+
}
|
|
1203
|
+
|
|
1204
|
+
function fileStamp(root,rel,realRoot,expected=null) {
|
|
1205
|
+
try {
|
|
1206
|
+
const file=path.join(root,rel),at=fs.lstatSync(file);
|
|
1207
|
+
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;
|
|
1208
|
+
return [at.dev,at.ino,at.mode,createHash("sha256").update(fs.readFileSync(file)).digest("hex")];
|
|
1209
|
+
} catch {return null;}
|
|
1210
|
+
}
|
|
1211
|
+
function readStamped(root,rel,stamp) {
|
|
1212
|
+
const full=path.join(root,rel),fd=fs.openSync(full,fs.constants.O_RDONLY|(fs.constants.O_NOFOLLOW??0)|fs.constants.O_NONBLOCK);
|
|
1213
|
+
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;}
|
|
1214
|
+
finally{fs.closeSync(fd);}
|
|
1215
|
+
}
|
|
1216
|
+
const sameStamp=(left,right)=>Boolean(left && right && left.every((value,index)=>value===right[index]));
|
|
1217
|
+
function retireObserved(root,rel,expected,realRoot) {
|
|
1218
|
+
if(!sameStamp(fileStamp(root,rel,realRoot,expected),expected))return false;
|
|
1219
|
+
const file=path.join(root,rel),claim=path.join(path.dirname(file),`.${path.basename(file)}.portulan-retire.${process.pid}.${randomUUID()}`);
|
|
1220
|
+
let made=false,moved=false,placeholder;
|
|
1221
|
+
try {
|
|
1222
|
+
const fd=fs.openSync(claim,"wx");made=true;const at=fs.fstatSync(fd);placeholder={dev:at.dev,ino:at.ino};fs.closeSync(fd);
|
|
1223
|
+
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;}
|
|
1224
|
+
if(!sameStamp(fileStamp(root,path.relative(root,claim),realRoot,expected),expected)) {
|
|
1225
|
+
try {fs.linkSync(claim,file);fs.unlinkSync(claim);moved=false;made=false;}
|
|
1226
|
+
catch {throw new VendorError(`${rel} changed during retirement; its data is preserved at ${claim}. Review both paths before cleanup`);}
|
|
1227
|
+
return false;
|
|
1228
|
+
}
|
|
1229
|
+
fs.unlinkSync(claim);moved=false;made=false;return true;
|
|
1230
|
+
} catch(error){if(!moved&&["EACCES","EPERM","EROFS"].includes(error.code))return false;throw error;}
|
|
1231
|
+
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`);}}
|
|
1232
|
+
}
|
|
1233
|
+
|
|
1131
1234
|
async function verdict(dir, roots) {
|
|
1132
1235
|
try {
|
|
1133
1236
|
const { findings } = await inspect(dir, roots);
|
|
@@ -1138,34 +1241,13 @@ async function verdict(dir, roots) {
|
|
|
1138
1241
|
}
|
|
1139
1242
|
|
|
1140
1243
|
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
|
-
}
|
|
1244
|
+
const failed=[];
|
|
1245
|
+
while(undo.length){try{const result=await undo.pop()();if(Array.isArray(result))failed.push(...result);}catch(error){failed.push(error.message);}}
|
|
1246
|
+
return failed;
|
|
1149
1247
|
}
|
|
1150
1248
|
|
|
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));
|
|
1249
|
+
function pruneObservedDirectories(root,observed) {
|
|
1250
|
+
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
1251
|
}
|
|
1170
1252
|
|
|
1171
1253
|
// `?? ""`: `process.argv[1]` is absent when a non-script imports this, and `pathToFileURL(undefined)` throws.
|