@danieljvdm/dev-kit 0.7.0 → 0.7.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@danieljvdm/dev-kit",
3
- "version": "0.7.0",
3
+ "version": "0.7.1",
4
4
  "private": false,
5
5
  "description": "Declarative project development toolkit with portable agent skills.",
6
6
  "license": "MIT",
@@ -27,6 +27,7 @@ const textEncoder = new TextEncoder();
27
27
  // Git preserves only the executable distinction for regular files. Canonicalizing
28
28
  // the remaining bits keeps digests stable across checkout and copy umasks.
29
29
  const canonicalFileMode = (mode: number): number => ((mode & 0o111) === 0 ? 0o644 : 0o755);
30
+ const rawFileMode = (mode: number): number => mode & 0o777;
30
31
 
31
32
  const frame = (value: string | Uint8Array): Uint8Array => {
32
33
  const bytes = typeof value === "string" ? textEncoder.encode(value) : value;
@@ -75,6 +76,7 @@ const digestFrames = Effect.fn("digestPathFrames")(function* (
75
76
 
76
77
  const digestFileSystemPath = Effect.fn("digestFileSystemPath")(function* (
77
78
  absolutePath: string,
79
+ fileMode: (mode: number) => number,
78
80
  ): Effect.fn.Return<
79
81
  ObservedPath,
80
82
  PlatformError.PlatformError | PathInspectionError,
@@ -107,7 +109,7 @@ const digestFileSystemPath = Effect.fn("digestFileSystemPath")(function* (
107
109
  kind: "file",
108
110
  digest: yield* digestFrames([
109
111
  "file-v1",
110
- String(canonicalFileMode(info.mode)),
112
+ String(fileMode(info.mode)),
111
113
  yield* fs.readFile(absolutePath),
112
114
  ]),
113
115
  };
@@ -119,7 +121,7 @@ const digestFileSystemPath = Effect.fn("digestFileSystemPath")(function* (
119
121
 
120
122
  for (const entry of entries) {
121
123
  const childPath = path.join(absolutePath, entry);
122
- const child = yield* digestFileSystemPath(childPath);
124
+ const child = yield* digestFileSystemPath(childPath, fileMode);
123
125
 
124
126
  if (child.kind === "missing") {
125
127
  return yield* new PathInspectionError({
@@ -142,7 +144,19 @@ const digestFileSystemPath = Effect.fn("digestFileSystemPath")(function* (
142
144
  });
143
145
 
144
146
  export const observePath = Effect.fn("observeManagedPath")(function* (absolutePath: string) {
145
- return yield* digestFileSystemPath(absolutePath).pipe(
147
+ return yield* digestFileSystemPath(absolutePath, canonicalFileMode).pipe(
148
+ Effect.mapError((cause) =>
149
+ cause instanceof PathInspectionError
150
+ ? cause
151
+ : new PathInspectionError({ path: absolutePath, operation: "inspect", cause }),
152
+ ),
153
+ );
154
+ });
155
+
156
+ export const observePathWithRawModes = Effect.fn("observeManagedPathWithRawModes")(function* (
157
+ absolutePath: string,
158
+ ) {
159
+ return yield* digestFileSystemPath(absolutePath, rawFileMode).pipe(
146
160
  Effect.mapError((cause) =>
147
161
  cause instanceof PathInspectionError
148
162
  ? cause
package/src/sync.ts CHANGED
@@ -23,6 +23,7 @@ import {
23
23
  digestSymlinkTarget,
24
24
  digestText,
25
25
  observePath,
26
+ observePathWithRawModes,
26
27
  type ObservedPath,
27
28
  } from "./path-digest.ts";
28
29
  import { readDirectDependencyNames } from "./project-package.ts";
@@ -430,6 +431,27 @@ const outputIdentity = (output: ManagedOutput) =>
430
431
  : { sourcePath: output.sourcePath }),
431
432
  });
432
433
 
434
+ const outputOwnershipIdentity = (output: ManagedOutput) =>
435
+ JSON.stringify({
436
+ resourceId: output.resourceId,
437
+ path: output.path,
438
+ mode: output.mode,
439
+ kind: output.kind,
440
+ ...("skill" in output
441
+ ? { skill: output.skill, target: output.target }
442
+ : { sourcePath: output.sourcePath }),
443
+ });
444
+
445
+ const usesRawFileModeDigests = (toolVersion: string): boolean => {
446
+ const match = /^(\d+)\.(\d+)\./.exec(toolVersion);
447
+
448
+ if (match === null) return false;
449
+ const major = Number(match[1]);
450
+ const minor = Number(match[2]);
451
+
452
+ return major === 0 && minor <= 6;
453
+ };
454
+
433
455
  const validateInventory = Effect.fn("validateManagedInventory")(function* (
434
456
  projectDir: string,
435
457
  outputs: ReadonlyArray<ManagedOutput | OwnershipReceipt>,
@@ -743,12 +765,31 @@ const planDesiredOutputs = Effect.fn("planDesiredSkillOutputs")(function* (
743
765
  const receipt = receiptsById.get(output.resourceId);
744
766
  const sameReceipt = receipt?.path === output.path ? receipt : undefined;
745
767
  const locked = lockById.get(output.resourceId);
746
- const adoptable = locked !== undefined && outputIdentity(locked) === outputIdentity(output);
768
+ const matchingLockedOutput =
769
+ locked !== undefined &&
770
+ outputOwnershipIdentity(locked) === outputOwnershipIdentity(output) &&
771
+ observed.kind === locked.kind
772
+ ? locked
773
+ : undefined;
774
+ const rawModeObservation =
775
+ matchingLockedOutput !== undefined &&
776
+ observed.kind !== "missing" &&
777
+ observed.digest !== matchingLockedOutput.digest &&
778
+ currentLock !== undefined &&
779
+ usesRawFileModeDigests(currentLock.toolVersion)
780
+ ? yield* observePathWithRawModes(output.destination)
781
+ : undefined;
782
+ const lockedOwnsObserved =
783
+ matchingLockedOutput !== undefined &&
784
+ observed.kind !== "missing" &&
785
+ (observed.digest === matchingLockedOutput.digest ||
786
+ (rawModeObservation?.kind === matchingLockedOutput.kind &&
787
+ rawModeObservation.digest === matchingLockedOutput.digest));
747
788
 
748
789
  if (observed.kind === "missing") {
749
790
  actions.push({ action: "create", desired: output, observed });
750
791
  } else if (observed.kind === output.kind && observed.digest === output.digest) {
751
- if (sameReceipt || adoptable || ("adoptIfExact" in output && output.adoptIfExact)) {
792
+ if (sameReceipt || lockedOwnsObserved || ("adoptIfExact" in output && output.adoptIfExact)) {
752
793
  actions.push({ action: "unchanged", desired: output, observed, adopted: !sameReceipt });
753
794
  } else {
754
795
  actions.push({
@@ -758,9 +799,10 @@ const planDesiredOutputs = Effect.fn("planDesiredSkillOutputs")(function* (
758
799
  });
759
800
  }
760
801
  } else if (
761
- sameReceipt &&
762
- observed.kind === sameReceipt.kind &&
763
- observed.digest === sameReceipt.digest
802
+ (sameReceipt !== undefined &&
803
+ observed.kind === sameReceipt.kind &&
804
+ observed.digest === sameReceipt.digest) ||
805
+ lockedOwnsObserved
764
806
  ) {
765
807
  actions.push({ action: "update", desired: output, observed });
766
808
  } else {