@graftland/package-manager 0.1.0 → 0.2.0-beta.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/dist/manifest.js CHANGED
@@ -1,15 +1,21 @@
1
- import { Array, Effect, Record, Schema, String } from "effect";
1
+ import { Array, Effect, HashMap, Option, Record, Schema, String } from "effect";
2
2
  // src/manifest.ts
3
- const PackageName = Schema.String.check(Schema.isPattern(/^(?:@[a-z0-9-~][a-z0-9-._~]*\/)?[a-z0-9-~][a-z0-9-._~]*$/i), Schema.isMaxLength(214));
4
- const Dependencies = Schema.Record(PackageName, Schema.String);
3
+ const PackageName = Schema.String.check(Schema.isPattern(/^(?!(?:node_modules|favicon\.ico)$)(?:@[a-z0-9-~][a-z0-9-._~]*\/)?[a-z0-9-~][a-z0-9-._~]*$/i), Schema.isMaxLength(214), Schema.isTrimmed());
4
+ const isPackageName = Schema.is(PackageName);
5
+ const dependencyNames = Schema.makeFilter((record) => Array.map(Array.filter(Record.keys(record), (name) => !isPackageName(name)), (name) => ({
6
+ path: [name],
7
+ issue: "Invalid dependency name"
8
+ })));
9
+ const Dependencies = Schema.Record(Schema.String, Schema.String).check(dependencyNames);
5
10
  const Manifest = Schema.StructWithRest(Schema.Struct({
6
11
  packageManager: Schema.optionalKey(Schema.String),
7
12
  dependencies: Schema.optionalKey(Dependencies),
8
13
  devDependencies: Schema.optionalKey(Dependencies),
9
14
  optionalDependencies: Schema.optionalKey(Dependencies)
10
15
  }), [Schema.Record(Schema.String, Schema.Unknown)]);
11
- const decodeManifest = Schema.decodeUnknownEffect(Schema.fromJsonString(Manifest));
12
- const Changes = Schema.Record(PackageName, Schema.Union([Schema.String.check(Schema.isPattern(/\S/)), Schema.Null]));
16
+ const decodeJsonManifest = Schema.decodeUnknownEffect(Schema.fromJsonString(Manifest));
17
+ const decodeManifest = (source) => decodeJsonManifest(String.replace(/^\uFEFF/, "")(source));
18
+ const Changes = Schema.Record(Schema.String, Schema.Union([Schema.String.check(Schema.isPattern(/\S/)), Schema.Null])).check(dependencyNames);
13
19
  const fields = [
14
20
  "dependencies",
15
21
  "devDependencies",
@@ -19,7 +25,7 @@ const applyChanges = (manifest, changes) => {
19
25
  let next = manifest;
20
26
  for (const [name, spec] of Record.toEntries(changes)) {
21
27
  const existing = Array.filter(fields, (field) => Record.has(next[field] ?? {}, name));
22
- const targets = spec === null ? fields : existing.length > 0 ? existing : ["dependencies"];
28
+ const targets = spec === null || existing.length > 0 ? existing : ["dependencies"];
23
29
  for (const field of targets) {
24
30
  const deps = next[field] ?? {};
25
31
  next = {
@@ -30,11 +36,108 @@ const applyChanges = (manifest, changes) => {
30
36
  }
31
37
  return next;
32
38
  };
39
+ const decodeKey = Schema.decodeEffect(Schema.fromJsonString(Schema.String));
40
+ const encodeString = Schema.encodeEffect(Schema.fromJsonString(Schema.String));
41
+ /** Read member spans from a JSON object already validated by decodeManifest. */
42
+ const readObject = Effect.fnUntraced(function* (source) {
43
+ const tokens = Array.fromIterable(String.matchAll(/"(?:\\[\s\S]|[^"\\])*"|[{}\[\]:,]|[^\s{}\[\]:,]+/g)(source));
44
+ let members = [];
45
+ let cursor = 1;
46
+ let start = (tokens[0]?.index ?? 0) + 1;
47
+ while (cursor < tokens.length && tokens[cursor]?.[0] !== "}") {
48
+ const key = tokens[cursor];
49
+ const valueStart = tokens[cursor + 2].index;
50
+ cursor += 2;
51
+ let depth = 0;
52
+ let end = valueStart;
53
+ do {
54
+ const token = tokens[cursor++];
55
+ if (token[0] === "{" || token[0] === "[") depth++;
56
+ if (token[0] === "}" || token[0] === "]") depth--;
57
+ end = token.index + token[0].length;
58
+ } while (depth > 0);
59
+ members = Array.append(members, {
60
+ name: yield* decodeKey(key[0]),
61
+ leading: String.slice(start, key.index)(source),
62
+ key: key[0],
63
+ colon: String.slice(key.index + key[0].length, valueStart)(source),
64
+ value: String.slice(valueStart, end)(source),
65
+ trailing: tokens[cursor]?.[0] === "," ? String.slice(end, tokens[cursor].index)(source) : ""
66
+ });
67
+ start = end;
68
+ if (tokens[cursor]?.[0] !== ",") break;
69
+ start = tokens[cursor++].index + 1;
70
+ }
71
+ const close = tokens[cursor].index;
72
+ const tail = String.slice(start, close)(source);
73
+ return {
74
+ members,
75
+ tail,
76
+ open: tokens[0].index,
77
+ close
78
+ };
79
+ });
80
+ const editObject = Effect.fnUntraced(function* (source, changes) {
81
+ const { members, tail, open, close } = yield* readObject(source);
82
+ const edits = HashMap.fromIterable(changes);
83
+ const first = Array.head(members);
84
+ const lastOccurrences = HashMap.fromIterable(Array.map(members, (member, index) => [member.name, index]));
85
+ const retained = Array.flatMap(members, (member, index) => Option.match(HashMap.get(edits, member.name), {
86
+ onNone: () => [member],
87
+ onSome: (change) => Option.match(change, {
88
+ onNone: () => [],
89
+ onSome: (value) => [Option.getOrUndefined(HashMap.get(lastOccurrences, member.name)) === index ? {
90
+ ...member,
91
+ value
92
+ } : member]
93
+ })
94
+ }));
95
+ const added = yield* Effect.forEach(Array.filter(changes, ([name]) => !HashMap.has(lastOccurrences, name)), Effect.fnUntraced(function* ([name, change]) {
96
+ if (Option.isNone(change)) return [];
97
+ const key = yield* encodeString(name);
98
+ return [{
99
+ name,
100
+ leading: Option.match(first, {
101
+ onNone: () => tail,
102
+ onSome: (member) => member.leading
103
+ }),
104
+ key,
105
+ colon: Option.match(first, {
106
+ onNone: () => ": ",
107
+ onSome: (member) => member.colon
108
+ }),
109
+ value: change.value,
110
+ trailing: ""
111
+ }];
112
+ }));
113
+ return String.slice(0, open + 1)(source) + Array.join(Array.map([...retained, ...Array.flatten(added)], (member) => member.leading + member.key + member.colon + member.value + member.trailing), ",") + tail + String.slice(close)(source);
114
+ });
33
115
  const encodeManifest = Effect.fnUntraced(function* (manifest, original) {
34
- const space = /^[{[]\r?\n([ \t]+)/.exec(original)?.[1] ?? 2;
35
- const text = yield* Schema.encodeEffect(Schema.fromJsonString(Manifest, { space }))(manifest);
36
- const eol = String.includes("\r\n")(original) ? "\r\n" : "\n";
37
- return String.replaceAll("\n", eol)(text) + (/\r?\n$/.test(original) ? eol : "");
116
+ const before = yield* decodeManifest(original);
117
+ const { members } = yield* readObject(original);
118
+ const changes = yield* Effect.reduce(fields, () => [], Effect.fnUntraced(function* (changes, field) {
119
+ const oldSpecs = HashMap.fromIterable(Record.toEntries(before[field] ?? {}));
120
+ const nextSpecs = HashMap.fromIterable(Record.toEntries(manifest[field] ?? {}));
121
+ const specs = yield* Effect.reduce(Array.dedupe([...Record.keys(before[field] ?? {}), ...Record.keys(manifest[field] ?? {})]), () => [], Effect.fnUntraced(function* (specs, name) {
122
+ const old = HashMap.get(oldSpecs, name);
123
+ const next = HashMap.get(nextSpecs, name);
124
+ if (Option.getOrUndefined(old) === Option.getOrUndefined(next)) return specs;
125
+ const value = yield* Option.match(next, {
126
+ onNone: () => Effect.succeedNone,
127
+ onSome: (value) => encodeString(value).pipe(Effect.asSome)
128
+ });
129
+ return Array.append(specs, [name, value]);
130
+ }));
131
+ if (specs.length === 0) return changes;
132
+ const section = Array.findLast(members, (member) => member.name === field);
133
+ const source = Option.match(section, {
134
+ onNone: () => "{}",
135
+ onSome: (member) => member.value
136
+ });
137
+ const edited = yield* editObject(source, specs);
138
+ return Array.append(changes, [field, Option.some(edited)]);
139
+ }));
140
+ return changes.length === 0 ? original : yield* editObject(original, changes);
38
141
  });
39
142
  export { Changes, Manifest, PackageName, applyChanges, decodeManifest, encodeManifest, fields };
40
143
 
@@ -1 +1 @@
1
- {"version":3,"file":"manifest.js","names":["Arr","Str"],"sources":["../src/manifest.ts"],"sourcesContent":["import { Array as Arr, Effect, Record, Schema, String as Str } from \"effect\"\n\nexport const PackageName = Schema.String.check(\n\tSchema.isPattern(/^(?:@[a-z0-9-~][a-z0-9-._~]*\\/)?[a-z0-9-~][a-z0-9-._~]*$/i),\n\tSchema.isMaxLength(214),\n)\nconst Dependencies = Schema.Record(PackageName, Schema.String)\nexport const Manifest = Schema.StructWithRest(\n\tSchema.Struct({\n\t\tpackageManager: Schema.optionalKey(Schema.String),\n\t\tdependencies: Schema.optionalKey(Dependencies),\n\t\tdevDependencies: Schema.optionalKey(Dependencies),\n\t\toptionalDependencies: Schema.optionalKey(Dependencies),\n\t}),\n\t[Schema.Record(Schema.String, Schema.Unknown)],\n)\nexport type Manifest = typeof Manifest.Type\nexport const decodeManifest = Schema.decodeUnknownEffect(\n\tSchema.fromJsonString(Manifest),\n)\nexport const Changes = Schema.Record(\n\tPackageName,\n\tSchema.Union([Schema.String.check(Schema.isPattern(/\\S/)), Schema.Null]),\n)\n/** A spec adds or updates a dependency; null removes it from all dependency fields. */\nexport type Changes = typeof Changes.Type\nexport const fields = [\n\t\"dependencies\",\n\t\"devDependencies\",\n\t\"optionalDependencies\",\n] as const\n\nexport const applyChanges = (\n\tmanifest: Manifest,\n\tchanges: Changes,\n): Manifest => {\n\tlet next = manifest\n\tfor (const [name, spec] of Record.toEntries(changes)) {\n\t\tconst existing = Arr.filter(fields, (field) =>\n\t\t\tRecord.has(next[field] ?? {}, name),\n\t\t)\n\t\tconst targets =\n\t\t\tspec === null\n\t\t\t\t? fields\n\t\t\t\t: existing.length > 0\n\t\t\t\t\t? existing\n\t\t\t\t\t: ([\"dependencies\"] as const)\n\t\tfor (const field of targets) {\n\t\t\tconst deps = next[field] ?? {}\n\t\t\tnext = {\n\t\t\t\t...next,\n\t\t\t\t[field]:\n\t\t\t\t\tspec === null\n\t\t\t\t\t\t? Record.remove(deps, name)\n\t\t\t\t\t\t: Record.set(deps, name, spec),\n\t\t\t}\n\t\t}\n\t}\n\treturn next\n}\n\nexport const encodeManifest = Effect.fnUntraced(function* (\n\tmanifest: Manifest,\n\toriginal: string,\n) {\n\tconst space = /^[{[]\\r?\\n([ \\t]+)/.exec(original)?.[1] ?? 2\n\tconst text = yield* Schema.encodeEffect(\n\t\tSchema.fromJsonString(Manifest, { space }),\n\t)(manifest)\n\tconst eol = Str.includes(\"\\r\\n\")(original) ? \"\\r\\n\" : \"\\n\"\n\treturn Str.replaceAll(\"\\n\", eol)(text) + (/\\r?\\n$/.test(original) ? eol : \"\")\n})\n"],"mappings":";;AAEA,MAAa,cAAc,OAAO,OAAO,MACxC,OAAO,UAAU,2DAA2D,GAC5E,OAAO,YAAY,GAAG,CACvB;AACA,MAAM,eAAe,OAAO,OAAO,aAAa,OAAO,MAAM;AAC7D,MAAa,WAAW,OAAO,eAC9B,OAAO,OAAO;CACb,gBAAgB,OAAO,YAAY,OAAO,MAAM;CAChD,cAAc,OAAO,YAAY,YAAY;CAC7C,iBAAiB,OAAO,YAAY,YAAY;CAChD,sBAAsB,OAAO,YAAY,YAAY;AACtD,CAAC,GACD,CAAC,OAAO,OAAO,OAAO,QAAQ,OAAO,OAAO,CAAC,CAC9C;AAEA,MAAa,iBAAiB,OAAO,oBACpC,OAAO,eAAe,QAAQ,CAC/B;AACA,MAAa,UAAU,OAAO,OAC7B,aACA,OAAO,MAAM,CAAC,OAAO,OAAO,MAAM,OAAO,UAAU,IAAI,CAAC,GAAG,OAAO,IAAI,CAAC,CACxE;AAGA,MAAa,SAAS;CACrB;CACA;CACA;AACD;AAEA,MAAa,gBACZ,UACA,YACc;CACd,IAAI,OAAO;CACX,KAAK,MAAM,CAAC,MAAM,SAAS,OAAO,UAAU,OAAO,GAAG;EACrD,MAAM,WAAWA,MAAI,OAAO,SAAS,UACpC,OAAO,IAAI,KAAK,UAAU,CAAC,GAAG,IAAI,CACnC;EACA,MAAM,UACL,SAAS,OACN,SACA,SAAS,SAAS,IACjB,WACC,CAAC,cAAc;EACrB,KAAK,MAAM,SAAS,SAAS;GAC5B,MAAM,OAAO,KAAK,UAAU,CAAC;GAC7B,OAAO;IACN,GAAG;KACF,QACA,SAAS,OACN,OAAO,OAAO,MAAM,IAAI,IACxB,OAAO,IAAI,MAAM,MAAM,IAAI;GAChC;EACD;CACD;CACA,OAAO;AACR;AAEA,MAAa,iBAAiB,OAAO,WAAW,WAC/C,UACA,UACC;CACD,MAAM,QAAQ,qBAAqB,KAAK,QAAQ,CAAC,GAAG,MAAM;CAC1D,MAAM,OAAO,OAAO,OAAO,aAC1B,OAAO,eAAe,UAAU,EAAE,MAAM,CAAC,CAC1C,CAAC,CAAC,QAAQ;CACV,MAAM,MAAMC,OAAI,SAAS,MAAM,CAAC,CAAC,QAAQ,IAAI,SAAS;CACtD,OAAOA,OAAI,WAAW,MAAM,GAAG,CAAC,CAAC,IAAI,KAAK,SAAS,KAAK,QAAQ,IAAI,MAAM;AAC3E,CAAC"}
1
+ {"version":3,"file":"manifest.js","names":[],"sources":["../src/manifest.ts"],"sourcesContent":["import { Array, Effect, HashMap, Option, Record, Schema, String } from \"effect\"\n\nexport const PackageName = Schema.String.check(\n\tSchema.isPattern(\n\t\t/^(?!(?:node_modules|favicon\\.ico)$)(?:@[a-z0-9-~][a-z0-9-._~]*\\/)?[a-z0-9-~][a-z0-9-._~]*$/i,\n\t),\n\tSchema.isMaxLength(214),\n\tSchema.isTrimmed(),\n)\nconst isPackageName = Schema.is(PackageName)\n// A refined Record key filters unmatched keys. Validate names after retaining\n// every key so invalid input cannot silently become a partial change set.\nconst dependencyNames = Schema.makeFilter(\n\t(record: Readonly<Record<string, unknown>>) =>\n\t\tArray.map(\n\t\t\tArray.filter(Record.keys(record), (name) => !isPackageName(name)),\n\t\t\t(name) => ({\n\t\t\t\tpath: [name],\n\t\t\t\tissue: \"Invalid dependency name\",\n\t\t\t}),\n\t\t),\n)\nconst Dependencies = Schema.Record(Schema.String, Schema.String).check(\n\tdependencyNames,\n)\nexport const Manifest = Schema.StructWithRest(\n\tSchema.Struct({\n\t\tpackageManager: Schema.optionalKey(Schema.String),\n\t\tdependencies: Schema.optionalKey(Dependencies),\n\t\tdevDependencies: Schema.optionalKey(Dependencies),\n\t\toptionalDependencies: Schema.optionalKey(Dependencies),\n\t}),\n\t[Schema.Record(Schema.String, Schema.Unknown)],\n)\nexport type Manifest = typeof Manifest.Type\nconst decodeJsonManifest = Schema.decodeUnknownEffect(\n\tSchema.fromJsonString(Manifest),\n)\n// A BOM is a source-format marker, not part of the JSON value.\nexport const decodeManifest = (source: string) =>\n\tdecodeJsonManifest(String.replace(/^\\uFEFF/, \"\")(source))\n\nexport const Changes = Schema.Record(\n\tSchema.String,\n\tSchema.Union([Schema.String.check(Schema.isPattern(/\\S/)), Schema.Null]),\n).check(dependencyNames)\n/** A spec adds or updates a dependency; null removes it from all dependency fields. */\nexport type Changes = typeof Changes.Type\nexport const fields = [\n\t\"dependencies\",\n\t\"devDependencies\",\n\t\"optionalDependencies\",\n] as const\n\nexport const applyChanges = (\n\tmanifest: Manifest,\n\tchanges: Changes,\n): Manifest => {\n\tlet next = manifest\n\tfor (const [name, spec] of Record.toEntries(changes)) {\n\t\tconst existing = Array.filter(fields, (field) =>\n\t\t\tRecord.has(next[field] ?? {}, name),\n\t\t)\n\t\tconst targets =\n\t\t\tspec === null || existing.length > 0\n\t\t\t\t? existing\n\t\t\t\t: ([\"dependencies\"] as const)\n\t\tfor (const field of targets) {\n\t\t\tconst deps = next[field] ?? {}\n\t\t\tnext = {\n\t\t\t\t...next,\n\t\t\t\t[field]:\n\t\t\t\t\tspec === null\n\t\t\t\t\t\t? Record.remove(deps, name)\n\t\t\t\t\t\t: Record.set(deps, name, spec),\n\t\t\t}\n\t\t}\n\t}\n\treturn next\n}\n\nconst decodeKey = Schema.decodeEffect(Schema.fromJsonString(Schema.String))\nconst encodeString = Schema.encodeEffect(Schema.fromJsonString(Schema.String))\n\ninterface Member {\n\treadonly name: string\n\treadonly leading: string\n\treadonly key: string\n\treadonly colon: string\n\treadonly value: string\n\treadonly trailing: string\n}\n\n/** Read member spans from a JSON object already validated by decodeManifest. */\nconst readObject = Effect.fnUntraced(function* (source: string) {\n\t// Strings are single tokens, so punctuation inside them cannot affect nesting.\n\tconst tokens = Array.fromIterable(\n\t\tString.matchAll(/\"(?:\\\\[\\s\\S]|[^\"\\\\])*\"|[{}\\[\\]:,]|[^\\s{}\\[\\]:,]+/g)(\n\t\t\tsource,\n\t\t),\n\t)\n\tlet members: ReadonlyArray<Member> = []\n\tlet cursor = 1\n\tlet start = (tokens[0]?.index ?? 0) + 1\n\twhile (cursor < tokens.length && tokens[cursor]?.[0] !== \"}\") {\n\t\tconst key = tokens[cursor]!\n\t\tconst valueStart = tokens[cursor + 2]!.index\n\t\tcursor += 2\n\t\tlet depth = 0\n\t\tlet end = valueStart\n\t\tdo {\n\t\t\tconst token = tokens[cursor++]!\n\t\t\tif (token[0] === \"{\" || token[0] === \"[\") depth++\n\t\t\tif (token[0] === \"}\" || token[0] === \"]\") depth--\n\t\t\tend = token.index! + token[0].length\n\t\t} while (depth > 0)\n\t\tmembers = Array.append(members, {\n\t\t\tname: yield* decodeKey(key[0]),\n\t\t\tleading: String.slice(start, key.index)(source),\n\t\t\tkey: key[0],\n\t\t\tcolon: String.slice(key.index! + key[0].length, valueStart)(source),\n\t\t\tvalue: String.slice(valueStart, end)(source),\n\t\t\ttrailing:\n\t\t\t\ttokens[cursor]?.[0] === \",\"\n\t\t\t\t\t? String.slice(end, tokens[cursor]!.index)(source)\n\t\t\t\t\t: \"\",\n\t\t})\n\t\tstart = end\n\t\tif (tokens[cursor]?.[0] !== \",\") break\n\t\tstart = tokens[cursor++]!.index! + 1\n\t}\n\tconst close = tokens[cursor]!.index\n\tconst tail = String.slice(start, close)(source)\n\treturn { members, tail, open: tokens[0]!.index!, close }\n})\n\n/** Ordered edits preserve insertion order; None removes every occurrence. */\ntype Edits = ReadonlyArray<readonly [string, Option.Option<string>]>\n\nconst editObject = Effect.fnUntraced(function* (\n\tsource: string,\n\tchanges: Edits,\n) {\n\tconst { members, tail, open, close } = yield* readObject(source)\n\tconst edits = HashMap.fromIterable(changes)\n\tconst first = Array.head(members)\n\tconst lastOccurrences = HashMap.fromIterable(\n\t\tArray.map(members, (member, index) => [member.name, index] as const),\n\t)\n\tconst retained = Array.flatMap(members, (member, index) =>\n\t\tOption.match(HashMap.get(edits, member.name), {\n\t\t\tonNone: () => [member],\n\t\t\tonSome: (change) =>\n\t\t\t\tOption.match(change, {\n\t\t\t\t\t// Removing every occurrence prevents an earlier value from becoming active.\n\t\t\t\t\tonNone: () => [],\n\t\t\t\t\tonSome: (value) => [\n\t\t\t\t\t\tOption.getOrUndefined(HashMap.get(lastOccurrences, member.name)) ===\n\t\t\t\t\t\tindex\n\t\t\t\t\t\t\t? { ...member, value }\n\t\t\t\t\t\t\t: member,\n\t\t\t\t\t],\n\t\t\t\t}),\n\t\t}),\n\t)\n\tconst added = yield* Effect.forEach(\n\t\tArray.filter(changes, ([name]) => !HashMap.has(lastOccurrences, name)),\n\t\tEffect.fnUntraced(function* ([name, change]) {\n\t\t\tif (Option.isNone(change)) return []\n\t\t\tconst key = yield* encodeString(name)\n\t\t\treturn [\n\t\t\t\t{\n\t\t\t\t\tname,\n\t\t\t\t\tleading: Option.match(first, {\n\t\t\t\t\t\tonNone: () => tail,\n\t\t\t\t\t\tonSome: (member) => member.leading,\n\t\t\t\t\t}),\n\t\t\t\t\tkey,\n\t\t\t\t\tcolon: Option.match(first, {\n\t\t\t\t\t\tonNone: () => \": \",\n\t\t\t\t\t\tonSome: (member) => member.colon,\n\t\t\t\t\t}),\n\t\t\t\t\tvalue: change.value,\n\t\t\t\t\ttrailing: \"\",\n\t\t\t\t},\n\t\t\t]\n\t\t}),\n\t)\n\treturn (\n\t\tString.slice(0, open + 1)(source) +\n\t\tArray.join(\n\t\t\tArray.map(\n\t\t\t\t[...retained, ...Array.flatten(added)],\n\t\t\t\t(member) =>\n\t\t\t\t\tmember.leading +\n\t\t\t\t\tmember.key +\n\t\t\t\t\tmember.colon +\n\t\t\t\t\tmember.value +\n\t\t\t\t\tmember.trailing,\n\t\t\t),\n\t\t\t\",\",\n\t\t) +\n\t\ttail +\n\t\tString.slice(close)(source)\n\t)\n})\n\nexport const encodeManifest = Effect.fnUntraced(function* (\n\tmanifest: Manifest,\n\toriginal: string,\n) {\n\tconst before = yield* decodeManifest(original)\n\tconst { members } = yield* readObject(original)\n\tconst changes = yield* Effect.reduce(\n\t\tfields,\n\t\t(): Edits => [],\n\t\tEffect.fnUntraced(function* (changes, field) {\n\t\t\tconst oldSpecs = HashMap.fromIterable(\n\t\t\t\tRecord.toEntries(before[field] ?? {}),\n\t\t\t)\n\t\t\tconst nextSpecs = HashMap.fromIterable(\n\t\t\t\tRecord.toEntries(manifest[field] ?? {}),\n\t\t\t)\n\t\t\tconst specs = yield* Effect.reduce(\n\t\t\t\tArray.dedupe([\n\t\t\t\t\t...Record.keys(before[field] ?? {}),\n\t\t\t\t\t...Record.keys(manifest[field] ?? {}),\n\t\t\t\t]),\n\t\t\t\t(): Edits => [],\n\t\t\t\tEffect.fnUntraced(function* (specs, name) {\n\t\t\t\t\tconst old = HashMap.get(oldSpecs, name)\n\t\t\t\t\tconst next = HashMap.get(nextSpecs, name)\n\t\t\t\t\tif (Option.getOrUndefined(old) === Option.getOrUndefined(next))\n\t\t\t\t\t\treturn specs\n\t\t\t\t\tconst value = yield* Option.match(next, {\n\t\t\t\t\t\tonNone: () => Effect.succeedNone,\n\t\t\t\t\t\tonSome: (value) => encodeString(value).pipe(Effect.asSome),\n\t\t\t\t\t})\n\t\t\t\t\treturn Array.append(specs, [name, value] as const)\n\t\t\t\t}),\n\t\t\t)\n\t\t\tif (specs.length === 0) return changes\n\t\t\tconst section = Array.findLast(members, (member) => member.name === field)\n\t\t\tconst source = Option.match(section, {\n\t\t\t\tonNone: () => \"{}\",\n\t\t\t\tonSome: (member) => member.value,\n\t\t\t})\n\t\t\tconst edited = yield* editObject(source, specs)\n\t\t\treturn Array.append(changes, [field, Option.some(edited)] as const)\n\t\t}),\n\t)\n\treturn changes.length === 0 ? original : yield* editObject(original, changes)\n})\n"],"mappings":"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"}
@@ -0,0 +1,50 @@
1
+ import { Array, Cause, Data, Effect, Exit, PlatformError, Schedule, Schema } from "effect";
2
+ import * as NodeProcess from "node:process";
3
+ // src/process.ts
4
+ const missingGroup = Schema.is(Schema.Struct({ code: Schema.Literal("ESRCH") }));
5
+ const unsignallableGroup = Schema.is(Schema.Struct({ code: Schema.Literal("EPERM") }));
6
+ const failedSignal = Schema.is(Schema.Struct({ message: Schema.Literal("Failed to kill child process") }));
7
+ const isSignalFailure = (error) => error.reason.module === "ChildProcess" && error.reason.method === "kill" && (missingGroup(error.cause) || error.reason._tag === "Unknown" && failedSignal(error.cause));
8
+ /** Signal zero checks the group without signalling any member. */
9
+ const groupExists = (pid) => Effect.try({
10
+ try: () => {
11
+ NodeProcess.kill(-pid, 0);
12
+ return true;
13
+ },
14
+ catch: (cause) => PlatformError.systemError({
15
+ _tag: "Unknown",
16
+ module: "ChildProcess",
17
+ method: "probeGroup",
18
+ cause
19
+ })
20
+ }).pipe(Effect.catchIf((error) => missingGroup(error.cause) || unsignallableGroup(error.cause), (error) => Effect.succeed(!missingGroup(error.cause))));
21
+ /** Close the original group, including descendants that remain in it after leader exit. */
22
+ const closeProcess = (process, options, probe = groupExists) => Effect.gen(function* () {
23
+ const killed = yield* Effect.suspend(() => process.kill(options)).pipe(Effect.exit);
24
+ if (NodeProcess.platform === "win32") return yield* killed;
25
+ const checked = yield* Effect.suspend(() => probe(process.pid)).pipe(Effect.exit, Effect.repeat({
26
+ while: (exit) => Exit.isSuccess(exit) && exit.value,
27
+ times: 100,
28
+ schedule: Schedule.spaced("20 millis")
29
+ }));
30
+ if (Exit.isFailure(checked)) return yield* Effect.failCause(Exit.isFailure(killed) ? Cause.combine(killed.cause, checked.cause) : checked.cause);
31
+ if (checked.value) {
32
+ if (Exit.isFailure(killed)) return yield* Effect.failCause(killed.cause);
33
+ return yield* PlatformError.systemError({
34
+ _tag: "Unknown",
35
+ module: "ChildProcess",
36
+ method: "confirmGroupStopped",
37
+ description: `Process group ${process.pid} still exists after the two-second confirmation window`
38
+ });
39
+ }
40
+ if (Exit.isFailure(killed)) {
41
+ const remaining = Array.filter(killed.cause.reasons, (reason) => !(Cause.isFailReason(reason) && isSignalFailure(reason.error)));
42
+ if (remaining.length > 0) return yield* Effect.failCause(Cause.fromReasons(remaining));
43
+ }
44
+ });
45
+ const Termination = Data.taggedEnum();
46
+ /** Only proves termination of the original group, not detached groups or daemons. */
47
+ const terminateProcess = (process, options) => Effect.suspend(() => closeProcess(process, options)).pipe(Effect.exit, Effect.map((exit) => Exit.isSuccess(exit) ? Termination.OriginalGroupStopped() : Termination.Unconfirmed({ cause: exit.cause })));
48
+ export { Termination, closeProcess, groupExists, terminateProcess };
49
+
50
+ //# sourceMappingURL=process.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"process.js","names":[],"sources":["../src/process.ts"],"sourcesContent":["// The spawner exposes no process-group existence check after kill loses errno.\nimport * as NodeProcess from \"node:process\"\nimport {\n\tArray,\n\tCause,\n\tData,\n\tEffect,\n\tExit,\n\tPlatformError,\n\tSchedule,\n\tSchema,\n} from \"effect\"\nimport type { ChildProcess, ChildProcessSpawner } from \"effect/process\"\n\nconst missingGroup = Schema.is(Schema.Struct({ code: Schema.Literal(\"ESRCH\") }))\nconst unsignallableGroup = Schema.is(\n\tSchema.Struct({ code: Schema.Literal(\"EPERM\") }),\n)\n// Node's kill(false) fallback loses errno, but has a specific signal-failure shape.\nconst failedSignal = Schema.is(\n\tSchema.Struct({ message: Schema.Literal(\"Failed to kill child process\") }),\n)\nconst isSignalFailure = (error: PlatformError.PlatformError) =>\n\terror.reason.module === \"ChildProcess\" &&\n\terror.reason.method === \"kill\" &&\n\t(missingGroup(error.cause) ||\n\t\t(error.reason._tag === \"Unknown\" && failedSignal(error.cause)))\n\n/** Signal zero checks the group without signalling any member. */\nexport const groupExists = (pid: ChildProcessSpawner.ProcessId) =>\n\tEffect.try({\n\t\ttry: () => {\n\t\t\tNodeProcess.kill(-pid, 0)\n\t\t\treturn true\n\t\t},\n\t\tcatch: (cause) =>\n\t\t\tPlatformError.systemError({\n\t\t\t\t_tag: \"Unknown\",\n\t\t\t\tmodule: \"ChildProcess\",\n\t\t\t\tmethod: \"probeGroup\",\n\t\t\t\tcause,\n\t\t\t}),\n\t}).pipe(\n\t\tEffect.catchIf(\n\t\t\t(error) => missingGroup(error.cause) || unsignallableGroup(error.cause),\n\t\t\t// EPERM can mean zombies on macOS. It never establishes group absence.\n\t\t\t(error) => Effect.succeed(!missingGroup(error.cause)),\n\t\t),\n\t)\n\n/** Close the original group, including descendants that remain in it after leader exit. */\nexport const closeProcess = (\n\tprocess: ChildProcessSpawner.ChildProcessHandle,\n\toptions: ChildProcess.KillOptions,\n\tprobe: typeof groupExists = groupExists,\n) =>\n\tEffect.gen(function* () {\n\t\tconst killed = yield* Effect.suspend(() => process.kill(options)).pipe(\n\t\t\tEffect.exit,\n\t\t)\n\t\t// Windows retains its as-is adapter contract; POSIX requires group evidence.\n\t\tif (NodeProcess.platform === \"win32\") return yield* killed\n\t\tconst checked = yield* Effect.suspend(() => probe(process.pid)).pipe(\n\t\t\tEffect.exit,\n\t\t\tEffect.repeat({\n\t\t\t\twhile: (exit) => Exit.isSuccess(exit) && exit.value,\n\t\t\t\ttimes: 100,\n\t\t\t\tschedule: Schedule.spaced(\"20 millis\"),\n\t\t\t}),\n\t\t)\n\t\tif (Exit.isFailure(checked))\n\t\t\treturn yield* Effect.failCause(\n\t\t\t\tExit.isFailure(killed)\n\t\t\t\t\t? Cause.combine(killed.cause, checked.cause)\n\t\t\t\t\t: checked.cause,\n\t\t\t)\n\t\tif (checked.value) {\n\t\t\tif (Exit.isFailure(killed)) return yield* Effect.failCause(killed.cause)\n\t\t\treturn yield* PlatformError.systemError({\n\t\t\t\t_tag: \"Unknown\",\n\t\t\t\tmodule: \"ChildProcess\",\n\t\t\t\tmethod: \"confirmGroupStopped\",\n\t\t\t\tdescription: `Process group ${process.pid} still exists after the two-second confirmation window`,\n\t\t\t})\n\t\t}\n\t\tif (Exit.isFailure(killed)) {\n\t\t\t// Absence resolves only a failed signal to a nonexistent group, not other cleanup failures.\n\t\t\tconst remaining = Array.filter(\n\t\t\t\tkilled.cause.reasons,\n\t\t\t\t(reason) =>\n\t\t\t\t\t!(Cause.isFailReason(reason) && isSignalFailure(reason.error)),\n\t\t\t)\n\t\t\tif (remaining.length > 0)\n\t\t\t\treturn yield* Effect.failCause(Cause.fromReasons(remaining))\n\t\t}\n\t})\n\nexport type Termination = Data.TaggedEnum<{\n\tOriginalGroupStopped: {}\n\tUnconfirmed: { readonly cause: Cause.Cause<PlatformError.PlatformError> }\n}>\n\nexport const Termination = Data.taggedEnum<Termination>()\n\nexport type OriginalGroupStopped = Data.TaggedEnum.Value<\n\tTermination,\n\t\"OriginalGroupStopped\"\n>\n\n/** Only proves termination of the original group, not detached groups or daemons. */\nexport const terminateProcess = (\n\tprocess: ChildProcessSpawner.ChildProcessHandle,\n\toptions: ChildProcess.KillOptions,\n) =>\n\tEffect.suspend(() => closeProcess(process, options)).pipe(\n\t\tEffect.exit,\n\t\tEffect.map((exit): Termination =>\n\t\t\tExit.isSuccess(exit)\n\t\t\t\t? Termination.OriginalGroupStopped()\n\t\t\t\t: Termination.Unconfirmed({ cause: exit.cause }),\n\t\t),\n\t)\n"],"mappings":";;;AAcA,MAAM,eAAe,OAAO,GAAG,OAAO,OAAO,EAAE,MAAM,OAAO,QAAQ,OAAO,EAAE,CAAC,CAAC;AAC/E,MAAM,qBAAqB,OAAO,GACjC,OAAO,OAAO,EAAE,MAAM,OAAO,QAAQ,OAAO,EAAE,CAAC,CAChD;AAEA,MAAM,eAAe,OAAO,GAC3B,OAAO,OAAO,EAAE,SAAS,OAAO,QAAQ,8BAA8B,EAAE,CAAC,CAC1E;AACA,MAAM,mBAAmB,UACxB,MAAM,OAAO,WAAW,kBACxB,MAAM,OAAO,WAAW,WACvB,aAAa,MAAM,KAAK,KACvB,MAAM,OAAO,SAAS,aAAa,aAAa,MAAM,KAAK;;AAG9D,MAAa,eAAe,QAC3B,OAAO,IAAI;CACV,WAAW;EACV,YAAY,KAAK,CAAC,KAAK,CAAC;EACxB,OAAO;CACR;CACA,QAAQ,UACP,cAAc,YAAY;EACzB,MAAM;EACN,QAAQ;EACR,QAAQ;EACR;CACD,CAAC;AACH,CAAC,CAAC,CAAC,KACF,OAAO,SACL,UAAU,aAAa,MAAM,KAAK,KAAK,mBAAmB,MAAM,KAAK,IAErE,UAAU,OAAO,QAAQ,CAAC,aAAa,MAAM,KAAK,CAAC,CACrD,CACD;;AAGD,MAAa,gBACZ,SACA,SACA,QAA4B,gBAE5B,OAAO,IAAI,aAAa;CACvB,MAAM,SAAS,OAAO,OAAO,cAAc,QAAQ,KAAK,OAAO,CAAC,CAAC,CAAC,KACjE,OAAO,IACR;CAEA,IAAI,YAAY,aAAa,SAAS,OAAO,OAAO;CACpD,MAAM,UAAU,OAAO,OAAO,cAAc,MAAM,QAAQ,GAAG,CAAC,CAAC,CAAC,KAC/D,OAAO,MACP,OAAO,OAAO;EACb,QAAQ,SAAS,KAAK,UAAU,IAAI,KAAK,KAAK;EAC9C,OAAO;EACP,UAAU,SAAS,OAAO,WAAW;CACtC,CAAC,CACF;CACA,IAAI,KAAK,UAAU,OAAO,GACzB,OAAO,OAAO,OAAO,UACpB,KAAK,UAAU,MAAM,IAClB,MAAM,QAAQ,OAAO,OAAO,QAAQ,KAAK,IACzC,QAAQ,KACZ;CACD,IAAI,QAAQ,OAAO;EAClB,IAAI,KAAK,UAAU,MAAM,GAAG,OAAO,OAAO,OAAO,UAAU,OAAO,KAAK;EACvE,OAAO,OAAO,cAAc,YAAY;GACvC,MAAM;GACN,QAAQ;GACR,QAAQ;GACR,aAAa,iBAAiB,QAAQ,IAAI;EAC3C,CAAC;CACF;CACA,IAAI,KAAK,UAAU,MAAM,GAAG;EAE3B,MAAM,YAAY,MAAM,OACvB,OAAO,MAAM,UACZ,WACA,EAAE,MAAM,aAAa,MAAM,KAAK,gBAAgB,OAAO,KAAK,EAC9D;EACA,IAAI,UAAU,SAAS,GACtB,OAAO,OAAO,OAAO,UAAU,MAAM,YAAY,SAAS,CAAC;CAC7D;AACD,CAAC;AAOF,MAAa,cAAc,KAAK,WAAwB;;AAQxD,MAAa,oBACZ,SACA,YAEA,OAAO,cAAc,aAAa,SAAS,OAAO,CAAC,CAAC,CAAC,KACpD,OAAO,MACP,OAAO,KAAK,SACX,KAAK,UAAU,IAAI,IAChB,YAAY,qBAAqB,IACjC,YAAY,YAAY,EAAE,OAAO,KAAK,MAAM,CAAC,CACjD,CACD"}
@@ -0,0 +1,15 @@
1
+ import { Cause, Effect, FileSystem, Option, Path, PlatformError, Schema } from "effect";
2
+ export interface SnapshotCleanupFailure {
3
+ readonly snapshotDirectory: string;
4
+ readonly cause: Cause.Cause<PlatformError.PlatformError>;
5
+ }
6
+ declare const UnsupportedFileTypeError_base: Schema.Class<UnsupportedFileTypeError, Schema.TaggedStruct<"UnsupportedFileTypeError", {
7
+ readonly path: Schema.String;
8
+ readonly fileType: Schema.String;
9
+ readonly message: Schema.String;
10
+ }>, Cause.YieldableError & {
11
+ readonly brand: unique symbol;
12
+ }>;
13
+ export declare class UnsupportedFileTypeError extends UnsupportedFileTypeError_base {}
14
+ export {};
15
+ //# sourceMappingURL=snapshot.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"snapshot.d.ts","names":[],"sources":["../src/snapshot.ts"],"mappings":";iBAiBiB;WACP;WACA,OAAO,MAAM,MAAM,cAAc;;;;;;;WAoDxB;;qBAFN,iCAAiC"}
@@ -0,0 +1,104 @@
1
+ import { Array, Cause, Effect, Equal, Exit, Option, Random, Schema, Scope } from "effect";
2
+ // src/snapshot.ts
3
+ /** Separate the transaction's outcome from disposal of its snapshot. */
4
+ const withSnapshot = (fs, canDelete, use) => Effect.uninterruptibleMask((resume) => Effect.gen(function* () {
5
+ const snapshotDirectory = yield* fs.makeTempDirectory({ prefix: "graft-recovery-" });
6
+ const outcome = yield* resume(Effect.suspend(() => use(snapshotDirectory))).pipe(Effect.exit);
7
+ const cleanup = (yield* canDelete) ? yield* Effect.suspend(() => fs.remove(snapshotDirectory, { recursive: true })).pipe(Effect.exit) : Exit.void;
8
+ if (Exit.isFailure(outcome)) return yield* Effect.failCause(Exit.isFailure(cleanup) ? Cause.combine(outcome.cause, cleanup.cause) : outcome.cause);
9
+ return yield* resume(Effect.succeed({
10
+ value: outcome.value,
11
+ snapshotCleanupFailure: Exit.isFailure(cleanup) ? Option.some({
12
+ snapshotDirectory,
13
+ cause: cleanup.cause
14
+ }) : Option.none()
15
+ })).pipe(Effect.onExit((exit) => Exit.isFailure(exit) && Exit.isFailure(cleanup) ? Effect.failCause(cleanup.cause) : Effect.void));
16
+ }));
17
+ var UnsupportedFileTypeError = class extends Schema.TaggedError()("UnsupportedFileTypeError", {
18
+ path: Schema.String,
19
+ fileType: Schema.String,
20
+ message: Schema.String
21
+ }) {};
22
+ /** lstat distinguishes missing paths from links, including dangling links. */
23
+ const inspectManagedFiles = Effect.fnUntraced(function* (fs, path, directory, names) {
24
+ return yield* Effect.forEach(names, (name) => Effect.gen(function* () {
25
+ const file = path.join(directory, name);
26
+ return {
27
+ name,
28
+ file,
29
+ metadata: yield* fs.lstat(file).pipe(Effect.flatMap((info) => info.type === "File" ? Effect.succeed({
30
+ mode: info.mode & 4095,
31
+ uid: info.uid,
32
+ gid: info.gid
33
+ }) : Effect.fail(UnsupportedFileTypeError.make({
34
+ path: file,
35
+ fileType: info.type,
36
+ message: `Managed paths must be regular files or absent: ${file} is ${info.type}`
37
+ }))), Effect.asSome, Effect.catchCauseIf((cause) => Array.every(cause.reasons, (reason) => Cause.isFailReason(reason) && reason.error._tag === "PlatformError" && reason.error.reason.module === "FileSystem" && reason.error.reason.method === "lstat" && reason.error.reason._tag === "NotFound"), () => Effect.succeedNone))
38
+ };
39
+ }));
40
+ });
41
+ const encodeInventory = Schema.encodeEffect(Schema.fromJsonString(Schema.Struct({
42
+ directory: Schema.String,
43
+ agent: Schema.String,
44
+ files: Schema.Array(Schema.Struct({
45
+ name: Schema.String,
46
+ backup: Schema.NullOr(Schema.String),
47
+ mode: Schema.NullOr(Schema.Int),
48
+ uid: Schema.NullOr(Schema.Int),
49
+ gid: Schema.NullOr(Schema.Int)
50
+ }))
51
+ }), { space: 2 }));
52
+ /** Persist every byte before allowing the installer to modify the project. */
53
+ const writeSnapshot = Effect.fnUntraced(function* (fs, path, snapshotDirectory, directory, agent, backups) {
54
+ const files = yield* Effect.forEach(backups, (entry, index) => Effect.gen(function* () {
55
+ if (Option.isNone(entry.contents)) return {
56
+ name: entry.name,
57
+ backup: null,
58
+ mode: null,
59
+ uid: null,
60
+ gid: null
61
+ };
62
+ const { bytes, mode, uid, gid } = entry.contents.value;
63
+ const backup = `${index}.bin`;
64
+ yield* fs.writeFile(path.join(snapshotDirectory, backup), bytes, { mode: 384 });
65
+ return {
66
+ name: entry.name,
67
+ backup,
68
+ mode,
69
+ uid: Option.getOrNull(uid),
70
+ gid: Option.getOrNull(gid)
71
+ };
72
+ }));
73
+ const inventory = yield* encodeInventory({
74
+ directory,
75
+ agent,
76
+ files
77
+ });
78
+ yield* fs.writeFileString(path.join(snapshotDirectory, "snapshot.json"), inventory, { mode: 384 });
79
+ return { restore: (_idle) => Array.reduce(backups, Effect.void, (previous, { file, contents }) => previous.pipe(Effect.onExit(() => Option.match(contents, {
80
+ onNone: () => fs.remove(file, { force: true }),
81
+ onSome: (contents) => restoreFile(fs, path, file, contents)
82
+ })))) };
83
+ }, Effect.uninterruptible);
84
+ /** Create privately and restore metadata before making the replacement visible. */
85
+ const restoreFile = Effect.fnUntraced(function* (fs, path, target, contents) {
86
+ yield* fs.makeDirectory(path.dirname(target), { recursive: true });
87
+ const temporary = `${target}.${(yield* Random.nextIntBetween(0, 4294967295)).toString(16)}.tmp`;
88
+ yield* Effect.acquireUseRelease(Scope.make(), (scope) => Effect.acquireUseRelease(fs.open(temporary, {
89
+ flag: "wx",
90
+ mode: 384
91
+ }).pipe(Effect.provideService(Scope.Scope, scope)), (file) => Effect.gen(function* () {
92
+ yield* file.writeAll(contents.bytes);
93
+ const current = yield* file.stat;
94
+ const { uid, gid } = contents;
95
+ if (Option.isSome(uid) && Option.isSome(gid) && (!Equal.equals(uid, current.uid) || !Equal.equals(gid, current.gid))) yield* fs.chown(temporary, uid.value, gid.value);
96
+ yield* fs.chmod(temporary, contents.mode);
97
+ yield* file.sync;
98
+ yield* Scope.close(scope, Exit.void);
99
+ yield* fs.rename(temporary, target);
100
+ }), (_, exit) => Exit.isSuccess(exit) ? Effect.void : Scope.close(scope, exit).pipe(Effect.onExit(() => fs.remove(temporary, { force: true })))), (scope, exit) => Scope.close(scope, exit));
101
+ });
102
+ export { UnsupportedFileTypeError, inspectManagedFiles, withSnapshot, writeSnapshot };
103
+
104
+ //# sourceMappingURL=snapshot.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"snapshot.js","names":[],"sources":["../src/snapshot.ts"],"sourcesContent":["import {\n\tArray,\n\tCause,\n\tEffect,\n\tEqual,\n\tExit,\n\tOption,\n\tRandom,\n\tSchema,\n\tScope,\n} from \"effect\"\nimport type { FileSystem, Path, PlatformError } from \"effect\"\n/** File writes have completed and the original installer group is stopped or absent. */\nexport interface ManagedWritersIdle {\n\treadonly _tag: \"ManagedWritersIdle\"\n}\n\nexport interface SnapshotCleanupFailure {\n\treadonly snapshotDirectory: string\n\treadonly cause: Cause.Cause<PlatformError.PlatformError>\n}\n\n/** Separate the transaction's outcome from disposal of its snapshot. */\nexport const withSnapshot = <A, E, R>(\n\tfs: FileSystem.FileSystem,\n\tcanDelete: Effect.Effect<boolean>,\n\tuse: (directory: string) => Effect.Effect<A, E, R>,\n) =>\n\tEffect.uninterruptibleMask((resume) =>\n\t\tEffect.gen(function* () {\n\t\t\tconst snapshotDirectory = yield* fs.makeTempDirectory({\n\t\t\t\tprefix: \"graft-recovery-\",\n\t\t\t})\n\t\t\tconst outcome = yield* resume(\n\t\t\t\tEffect.suspend(() => use(snapshotDirectory)),\n\t\t\t).pipe(Effect.exit)\n\t\t\tconst cleanup = (yield* canDelete)\n\t\t\t\t? yield* Effect.suspend(() =>\n\t\t\t\t\t\tfs.remove(snapshotDirectory, { recursive: true }),\n\t\t\t\t\t).pipe(Effect.exit)\n\t\t\t\t: Exit.void\n\t\t\tif (Exit.isFailure(outcome)) {\n\t\t\t\treturn yield* Effect.failCause(\n\t\t\t\t\tExit.isFailure(cleanup)\n\t\t\t\t\t\t? Cause.combine(outcome.cause, cleanup.cause)\n\t\t\t\t\t\t: outcome.cause,\n\t\t\t\t)\n\t\t\t}\n\t\t\treturn yield* resume(\n\t\t\t\tEffect.succeed({\n\t\t\t\t\tvalue: outcome.value,\n\t\t\t\t\tsnapshotCleanupFailure: Exit.isFailure(cleanup)\n\t\t\t\t\t\t? Option.some<SnapshotCleanupFailure>({\n\t\t\t\t\t\t\t\tsnapshotDirectory,\n\t\t\t\t\t\t\t\tcause: cleanup.cause,\n\t\t\t\t\t\t\t})\n\t\t\t\t\t\t: Option.none<SnapshotCleanupFailure>(),\n\t\t\t\t}),\n\t\t\t).pipe(\n\t\t\t\t// Cancellation can prevent delivery, but must not hide a cleanup failure.\n\t\t\t\tEffect.onExit((exit) =>\n\t\t\t\t\tExit.isFailure(exit) && Exit.isFailure(cleanup)\n\t\t\t\t\t\t? Effect.failCause(cleanup.cause)\n\t\t\t\t\t\t: Effect.void,\n\t\t\t\t),\n\t\t\t)\n\t\t}),\n\t)\n\nexport class UnsupportedFileTypeError extends Schema.TaggedError<\n\tUnsupportedFileTypeError,\n\t{ readonly brand: unique symbol }\n>()(\"UnsupportedFileTypeError\", {\n\tpath: Schema.String,\n\tfileType: Schema.String,\n\tmessage: Schema.String,\n}) {}\n\ninterface RegularFileMetadata {\n\treadonly mode: number\n\treadonly uid: Option.Option<number>\n\treadonly gid: Option.Option<number>\n}\nexport interface Contents extends RegularFileMetadata {\n\treadonly bytes: Uint8Array\n}\nexport interface Backup {\n\treadonly name: string\n\treadonly file: string\n\treadonly contents: Option.Option<Contents>\n}\n\n/** lstat distinguishes missing paths from links, including dangling links. */\nexport const inspectManagedFiles = Effect.fnUntraced(function* (\n\tfs: FileSystem.FileSystem,\n\tpath: Path.Path,\n\tdirectory: string,\n\tnames: ReadonlyArray<string>,\n) {\n\treturn yield* Effect.forEach(names, (name) =>\n\t\tEffect.gen(function* () {\n\t\t\tconst file = path.join(directory, name)\n\t\t\tconst metadata = yield* fs.lstat(file).pipe(\n\t\t\t\tEffect.flatMap((info) =>\n\t\t\t\t\tinfo.type === \"File\"\n\t\t\t\t\t\t? Effect.succeed<RegularFileMetadata>({\n\t\t\t\t\t\t\t\tmode: info.mode & 0o7777,\n\t\t\t\t\t\t\t\tuid: info.uid,\n\t\t\t\t\t\t\t\tgid: info.gid,\n\t\t\t\t\t\t\t})\n\t\t\t\t\t\t: Effect.fail(\n\t\t\t\t\t\t\t\tUnsupportedFileTypeError.make({\n\t\t\t\t\t\t\t\t\tpath: file,\n\t\t\t\t\t\t\t\t\tfileType: info.type,\n\t\t\t\t\t\t\t\t\tmessage: `Managed paths must be regular files or absent: ${file} is ${info.type}`,\n\t\t\t\t\t\t\t\t}),\n\t\t\t\t\t\t\t),\n\t\t\t\t),\n\t\t\t\tEffect.asSome,\n\t\t\t\t// Only an entirely missing-path failure establishes absence.\n\t\t\t\tEffect.catchCauseIf(\n\t\t\t\t\t(cause) =>\n\t\t\t\t\t\tArray.every(\n\t\t\t\t\t\t\tcause.reasons,\n\t\t\t\t\t\t\t(reason) =>\n\t\t\t\t\t\t\t\tCause.isFailReason(reason) &&\n\t\t\t\t\t\t\t\treason.error._tag === \"PlatformError\" &&\n\t\t\t\t\t\t\t\treason.error.reason.module === \"FileSystem\" &&\n\t\t\t\t\t\t\t\treason.error.reason.method === \"lstat\" &&\n\t\t\t\t\t\t\t\treason.error.reason._tag === \"NotFound\",\n\t\t\t\t\t\t),\n\t\t\t\t\t() => Effect.succeedNone,\n\t\t\t\t),\n\t\t\t)\n\t\t\treturn { name, file, metadata }\n\t\t}),\n\t)\n})\n\nconst encodeInventory = Schema.encodeEffect(\n\tSchema.fromJsonString(\n\t\tSchema.Struct({\n\t\t\tdirectory: Schema.String,\n\t\t\tagent: Schema.String,\n\t\t\tfiles: Schema.Array(\n\t\t\t\tSchema.Struct({\n\t\t\t\t\tname: Schema.String,\n\t\t\t\t\tbackup: Schema.NullOr(Schema.String),\n\t\t\t\t\tmode: Schema.NullOr(Schema.Int),\n\t\t\t\t\tuid: Schema.NullOr(Schema.Int),\n\t\t\t\t\tgid: Schema.NullOr(Schema.Int),\n\t\t\t\t}),\n\t\t\t),\n\t\t}),\n\t\t{ space: 2 },\n\t),\n)\n\n/** Persist every byte before allowing the installer to modify the project. */\nexport const writeSnapshot = Effect.fnUntraced(function* (\n\tfs: FileSystem.FileSystem,\n\tpath: Path.Path,\n\tsnapshotDirectory: string,\n\tdirectory: string,\n\tagent: string,\n\tbackups: ReadonlyArray<Backup>,\n) {\n\tconst files = yield* Effect.forEach(backups, (entry, index) =>\n\t\tEffect.gen(function* () {\n\t\t\tif (Option.isNone(entry.contents))\n\t\t\t\treturn {\n\t\t\t\t\tname: entry.name,\n\t\t\t\t\tbackup: null,\n\t\t\t\t\tmode: null,\n\t\t\t\t\tuid: null,\n\t\t\t\t\tgid: null,\n\t\t\t\t}\n\t\t\tconst { bytes, mode, uid, gid } = entry.contents.value\n\t\t\tconst backup = `${index}.bin`\n\t\t\tyield* fs.writeFile(path.join(snapshotDirectory, backup), bytes, {\n\t\t\t\tmode: 0o600,\n\t\t\t})\n\t\t\treturn {\n\t\t\t\tname: entry.name,\n\t\t\t\tbackup,\n\t\t\t\tmode,\n\t\t\t\tuid: Option.getOrNull(uid),\n\t\t\t\tgid: Option.getOrNull(gid),\n\t\t\t}\n\t\t}),\n\t)\n\tconst inventory = yield* encodeInventory({ directory, agent, files })\n\tyield* fs.writeFileString(\n\t\tpath.join(snapshotDirectory, \"snapshot.json\"),\n\t\tinventory,\n\t\t{ mode: 0o600 },\n\t)\n\treturn {\n\t\t// The caller must establish that file writes and the original process group have stopped.\n\t\trestore: (_idle: ManagedWritersIdle) =>\n\t\t\tArray.reduce<Backup, Effect.Effect<void, PlatformError.PlatformError>>(\n\t\t\t\tbackups,\n\t\t\t\tEffect.void,\n\t\t\t\t(previous, { file, contents }) =>\n\t\t\t\t\tprevious.pipe(\n\t\t\t\t\t\tEffect.onExit(() =>\n\t\t\t\t\t\t\tOption.match(contents, {\n\t\t\t\t\t\t\t\tonNone: () => fs.remove(file, { force: true }),\n\t\t\t\t\t\t\t\tonSome: (contents) => restoreFile(fs, path, file, contents),\n\t\t\t\t\t\t\t}),\n\t\t\t\t\t\t),\n\t\t\t\t\t),\n\t\t\t),\n\t}\n}, Effect.uninterruptible)\n\n/** Create privately and restore metadata before making the replacement visible. */\nconst restoreFile = Effect.fnUntraced(function* (\n\tfs: FileSystem.FileSystem,\n\tpath: Path.Path,\n\ttarget: string,\n\tcontents: Contents,\n) {\n\tyield* fs.makeDirectory(path.dirname(target), { recursive: true })\n\tconst suffix = yield* Random.nextIntBetween(0, 0xffffffff)\n\tconst temporary = `${target}.${suffix.toString(16)}.tmp`\n\tyield* Effect.acquireUseRelease(\n\t\tScope.make(),\n\t\t(scope) =>\n\t\t\tEffect.acquireUseRelease(\n\t\t\t\tfs\n\t\t\t\t\t.open(temporary, { flag: \"wx\", mode: 0o600 })\n\t\t\t\t\t.pipe(Effect.provideService(Scope.Scope, scope)),\n\t\t\t\t(file) =>\n\t\t\t\t\tEffect.gen(function* () {\n\t\t\t\t\t\tyield* file.writeAll(contents.bytes)\n\t\t\t\t\t\tconst current = yield* file.stat\n\t\t\t\t\t\tconst { uid, gid } = contents\n\t\t\t\t\t\tif (\n\t\t\t\t\t\t\tOption.isSome(uid) &&\n\t\t\t\t\t\t\tOption.isSome(gid) &&\n\t\t\t\t\t\t\t(!Equal.equals(uid, current.uid) ||\n\t\t\t\t\t\t\t\t!Equal.equals(gid, current.gid))\n\t\t\t\t\t\t) {\n\t\t\t\t\t\t\tyield* fs.chown(temporary, uid.value, gid.value)\n\t\t\t\t\t\t}\n\t\t\t\t\t\tyield* fs.chmod(temporary, contents.mode)\n\t\t\t\t\t\tyield* file.sync\n\t\t\t\t\t\tyield* Scope.close(scope, Exit.void)\n\t\t\t\t\t\tyield* fs.rename(temporary, target)\n\t\t\t\t\t}),\n\t\t\t\t(_, exit) =>\n\t\t\t\t\tExit.isSuccess(exit)\n\t\t\t\t\t\t? Effect.void\n\t\t\t\t\t\t: Scope.close(scope, exit).pipe(\n\t\t\t\t\t\t\t\tEffect.onExit(() => fs.remove(temporary, { force: true })),\n\t\t\t\t\t\t\t),\n\t\t\t),\n\t\t(scope, exit) => Scope.close(scope, exit),\n\t)\n})\n"],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,9 @@
1
1
  {
2
2
  "name": "@graftland/package-manager",
3
- "version": "0.1.0",
3
+ "engines": {
4
+ "node": ">=22.19.0"
5
+ },
6
+ "version": "0.2.0-beta.0",
4
7
  "type": "module",
5
8
  "description": "Installs dependencies with npm, pnpm or Yarn, and rolls back on failure",
6
9
  "license": "Apache-2.0",
@@ -12,14 +15,17 @@
12
15
  "sideEffects": false,
13
16
  "files": [
14
17
  "dist",
15
- "src"
18
+ "src",
19
+ "README.zh-CN.md",
20
+ "README.zh-TW.md"
16
21
  ],
17
22
  "exports": {
18
23
  ".": "./dist/index.js",
24
+ "./PackageManagerDeclaration": "./dist/PackageManagerDeclaration.js",
19
25
  "./package.json": "./package.json"
20
26
  },
21
27
  "peerDependencies": {
22
- "effect": "4.1.2"
28
+ "effect": "4.1.*"
23
29
  },
24
30
  "devDependencies": {
25
31
  "@effect/vitest": "4.1.2",
@@ -0,0 +1,42 @@
1
+ import { Schema } from "effect"
2
+
3
+ export const PackageManagerName = Schema.Literals(["npm", "pnpm", "yarn"])
4
+
5
+ const CorepackHash = Schema.Union([
6
+ Schema.Tuple([
7
+ Schema.Literal("sha224"),
8
+ Schema.String.check(Schema.isPattern(/^[0-9a-f]{56}$/)),
9
+ ]),
10
+ Schema.Tuple([
11
+ Schema.Literal("sha256"),
12
+ Schema.String.check(Schema.isPattern(/^[0-9a-f]{64}$/)),
13
+ ]),
14
+ Schema.Tuple([
15
+ Schema.Literal("sha384"),
16
+ Schema.String.check(Schema.isPattern(/^[0-9a-f]{96}$/)),
17
+ ]),
18
+ Schema.Tuple([
19
+ Schema.Literal("sha512"),
20
+ Schema.String.check(Schema.isPattern(/^[0-9a-f]{128}$/)),
21
+ ]),
22
+ ])
23
+ const isCorepackHash = Schema.is(CorepackHash)
24
+ const Version = Schema.String.check(Schema.isMaxLength(256))
25
+ .pipe(Schema.decodeTo(Schema.SemVerFromString))
26
+ .check(
27
+ Schema.makeFilter(
28
+ (version) => version.build.length === 0 || isCorepackHash(version.build),
29
+ {
30
+ expected:
31
+ "no build metadata, or a Corepack sha224/sha256/sha384/sha512 lowercase hexadecimal hash",
32
+ },
33
+ ),
34
+ )
35
+
36
+ /** Exact SemVer with optional prerelease and Corepack hash; ranges, URLs and arbitrary build metadata are unsupported. */
37
+ export const PackageManagerDeclaration = Schema.TemplateLiteralParser([
38
+ PackageManagerName,
39
+ "@",
40
+ Version,
41
+ ])
42
+ export type PackageManagerDeclaration = typeof PackageManagerDeclaration.Type