saasaloy 0.4.0 → 0.5.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/dist/index.js CHANGED
@@ -367,6 +367,34 @@ var MANIFEST_FILE = join3(".saasaloy", "manifest.json");
367
367
  function samePatchEntry(a, b) {
368
368
  return a.module === b.module && a.file === b.file && JSON.stringify(a.patch) === JSON.stringify(b.patch);
369
369
  }
370
+ function recordManagedFile(manifest, record) {
371
+ manifest.managed[record.target] = {
372
+ hash: record.hash,
373
+ module: record.module,
374
+ ...record.adopted ? { adopted: true } : {},
375
+ ...record.from === void 0 ? {} : { from: record.from }
376
+ };
377
+ }
378
+ function untrackManagedFile(manifest, target) {
379
+ delete manifest.managed[target];
380
+ }
381
+ function recordPatch(manifest, entry) {
382
+ if (manifest.patches.some((existing) => samePatchEntry(existing, entry))) {
383
+ return;
384
+ }
385
+ manifest.patches.push(entry);
386
+ }
387
+ function untrackPatch(manifest, entry) {
388
+ manifest.patches = manifest.patches.filter(
389
+ (patch) => !samePatchEntry(patch, entry)
390
+ );
391
+ }
392
+ function recordLink(manifest, target, path) {
393
+ manifest.links[target] = path;
394
+ }
395
+ function untrackLink(manifest, target) {
396
+ delete manifest.links[target];
397
+ }
370
398
  function emptyManifest() {
371
399
  return { links: {}, managed: {}, patches: [], removeWarnings: {} };
372
400
  }
@@ -707,40 +735,45 @@ function buildRouteLink(receiver, path, call) {
707
735
 
708
736
  // src/lib/patch/const-array.ts
709
737
  import { parseModule as parseModule2 } from "magicast";
710
- function insertIntoConstArray(source, patch) {
711
- const mod = parseModule2(source);
712
- const array = findConstArray(mod.$ast, patch.constName);
713
- const wanted = patch.entry[patch.key];
714
- if (!array || wanted === void 0 || hasEntry(array, patch.key, wanted)) {
715
- return source;
738
+
739
+ // src/lib/patch/ts-array.ts
740
+ function findConstArray(program, constName) {
741
+ for (const statement of program.body) {
742
+ const declaration = statement.type === "ExportNamedDeclaration" ? statement.declaration : statement;
743
+ if (declaration?.type !== "VariableDeclaration" || declaration.kind !== "const") {
744
+ continue;
745
+ }
746
+ for (const declarator of declaration.declarations) {
747
+ if (declarator.id.type !== "Identifier" || declarator.id.name !== constName || !declarator.init) {
748
+ continue;
749
+ }
750
+ return asArray(declarator.init);
751
+ }
716
752
  }
717
- return appendEntry(source, array, renderObject(patch.entry));
753
+ return void 0;
718
754
  }
719
- function constArrayInsertRefusal(source, patch) {
720
- const mod = parseModule2(source);
721
- const array = findConstArray(mod.$ast, patch.constName);
722
- if (!array) {
723
- return `const-array could not find a module-scope const array named ${patch.constName}`;
724
- }
725
- if (patch.entry[patch.key] === void 0) {
726
- return `const-array entry has no identity property named ${patch.key}`;
755
+ function asArray(node) {
756
+ const value = unwrapTypeExpression(node);
757
+ return value.type === "ArrayExpression" ? value : void 0;
758
+ }
759
+ function findElement(array, key, wanted) {
760
+ for (const element of array.elements) {
761
+ if (element?.type === "ObjectExpression" && entryValue(element, key) === wanted) {
762
+ return element;
763
+ }
727
764
  }
728
765
  return void 0;
729
766
  }
730
- function removeFromConstArray(source, patch) {
731
- const mod = parseModule2(source);
732
- const array = findConstArray(mod.$ast, patch.constName);
733
- const wanted = patch.entry[patch.key];
734
- if (!array || wanted === void 0) {
735
- return source;
736
- }
737
- const index = array.elements.findIndex(
738
- (element) => entryValue(element, patch.key) === wanted
739
- );
740
- if (index === -1) {
741
- return source;
767
+ function propertyValue(object, key) {
768
+ for (const property of object.properties) {
769
+ if (property.type !== "ObjectProperty" || property.computed) {
770
+ continue;
771
+ }
772
+ if (propertyName(property.key) === key) {
773
+ return property.value;
774
+ }
742
775
  }
743
- return removeEntry(source, array, index);
776
+ return void 0;
744
777
  }
745
778
  function appendEntry(source, array, entry) {
746
779
  const close = array.end - 1;
@@ -797,6 +830,32 @@ function entryValue(element, key) {
797
830
  }
798
831
  return void 0;
799
832
  }
833
+ function renderObject(entry) {
834
+ return `{ ${renderProperties(entry).join(", ")} }`;
835
+ }
836
+ function renderProperties(entry) {
837
+ return Object.entries(entry).map(
838
+ ([key, value]) => `${renderKey(key)}: ${renderValue(value)}`
839
+ );
840
+ }
841
+ function renderKey(key) {
842
+ return /^[A-Za-z_$][\w$]*$/.test(key) ? key : JSON.stringify(key);
843
+ }
844
+ function renderValue(value) {
845
+ if (value === null || typeof value === "string" || typeof value === "boolean") {
846
+ return JSON.stringify(value);
847
+ }
848
+ if (typeof value === "number" && Number.isFinite(value)) {
849
+ return String(value);
850
+ }
851
+ if (Array.isArray(value)) {
852
+ return `[${value.map(renderValue).join(", ")}]`;
853
+ }
854
+ if (typeof value === "object") {
855
+ return renderObject(value);
856
+ }
857
+ throw new TypeError("patch entries must contain JSON values");
858
+ }
800
859
  function propertyName(node) {
801
860
  if (node.type === "Identifier") {
802
861
  return node.name;
@@ -809,22 +868,6 @@ function literalValue(node) {
809
868
  }
810
869
  return node.type === "NullLiteral" ? null : node.value;
811
870
  }
812
- function findConstArray(program, constName) {
813
- for (const statement of program.body) {
814
- const declaration = statement.type === "ExportNamedDeclaration" ? statement.declaration : statement;
815
- if (declaration?.type !== "VariableDeclaration" || declaration.kind !== "const") {
816
- continue;
817
- }
818
- for (const declarator of declaration.declarations) {
819
- if (declarator.id.type !== "Identifier" || declarator.id.name !== constName || !declarator.init) {
820
- continue;
821
- }
822
- const value = unwrapTypeExpression(declarator.init);
823
- return value.type === "ArrayExpression" ? value : void 0;
824
- }
825
- }
826
- return void 0;
827
- }
828
871
  function unwrapTypeExpression(node) {
829
872
  let value = node;
830
873
  while (value.type === "TSAsExpression" || value.type === "TSSatisfiesExpression" || value.type === "TypeCastExpression") {
@@ -832,26 +875,42 @@ function unwrapTypeExpression(node) {
832
875
  }
833
876
  return value;
834
877
  }
835
- function renderObject(entry) {
836
- return `{ ${Object.entries(entry).map(([key, value]) => `${renderKey(key)}: ${renderValue(value)}`).join(", ")} }`;
837
- }
838
- function renderKey(key) {
839
- return /^[A-Za-z_$][\w$]*$/.test(key) ? key : JSON.stringify(key);
878
+
879
+ // src/lib/patch/const-array.ts
880
+ function insertIntoConstArray(source, patch) {
881
+ const mod = parseModule2(source);
882
+ const array = findConstArray(mod.$ast, patch.constName);
883
+ const wanted = patch.entry[patch.key];
884
+ if (!array || wanted === void 0 || hasEntry(array, patch.key, wanted)) {
885
+ return source;
886
+ }
887
+ return appendEntry(source, array, renderObject(patch.entry));
840
888
  }
841
- function renderValue(value) {
842
- if (value === null || typeof value === "string" || typeof value === "boolean") {
843
- return JSON.stringify(value);
889
+ function constArrayInsertRefusal(source, patch) {
890
+ const mod = parseModule2(source);
891
+ const array = findConstArray(mod.$ast, patch.constName);
892
+ if (!array) {
893
+ return `const-array could not find a module-scope const array named ${patch.constName}`;
844
894
  }
845
- if (typeof value === "number" && Number.isFinite(value)) {
846
- return String(value);
895
+ if (patch.entry[patch.key] === void 0) {
896
+ return `const-array entry has no identity property named ${patch.key}`;
847
897
  }
848
- if (Array.isArray(value)) {
849
- return `[${value.map(renderValue).join(", ")}]`;
898
+ return void 0;
899
+ }
900
+ function removeFromConstArray(source, patch) {
901
+ const mod = parseModule2(source);
902
+ const array = findConstArray(mod.$ast, patch.constName);
903
+ const wanted = patch.entry[patch.key];
904
+ if (!array || wanted === void 0) {
905
+ return source;
850
906
  }
851
- if (typeof value === "object") {
852
- return renderObject(value);
907
+ const index = array.elements.findIndex(
908
+ (element) => entryValue(element, patch.key) === wanted
909
+ );
910
+ if (index === -1) {
911
+ return source;
853
912
  }
854
- throw new TypeError("const-array entries must contain JSON values");
913
+ return removeEntry(source, array, index);
855
914
  }
856
915
 
857
916
  // src/lib/patch/diff.ts
@@ -1211,6 +1270,233 @@ function inferFormatting(source) {
1211
1270
  };
1212
1271
  }
1213
1272
 
1273
+ // src/lib/patch/nav-entry.ts
1274
+ import { parseModule as parseModule5 } from "magicast";
1275
+
1276
+ // src/lib/patch/named-import.ts
1277
+ import { parseModule as parseModule4 } from "magicast";
1278
+ var PRINT_WIDTH = 80;
1279
+ function ensureNamedImport(source, want) {
1280
+ const program = parseModule4(source).$ast;
1281
+ const declaration = findImport(program, want.from);
1282
+ if (!declaration) {
1283
+ return insertDeclaration(source, program, want);
1284
+ }
1285
+ const named = namedSpecifiers(declaration).map(
1286
+ (specifier) => source.slice(specifier.start, specifier.end)
1287
+ );
1288
+ if (named.includes(want.name)) {
1289
+ return source;
1290
+ }
1291
+ const at = named.findIndex((text4) => localOf(text4) > want.name);
1292
+ const next = at === -1 ? [...named, want.name] : named.toSpliced(at, 0, want.name);
1293
+ return replaceDeclaration(source, declaration, next, want.from);
1294
+ }
1295
+ function removeNamedImport(source, want) {
1296
+ const program = parseModule4(source).$ast;
1297
+ const declaration = findImport(program, want.from);
1298
+ if (!declaration || isReferenced(program, want.name)) {
1299
+ return source;
1300
+ }
1301
+ const specifiers = namedSpecifiers(declaration);
1302
+ const texts = specifiers.map(
1303
+ (specifier) => source.slice(specifier.start, specifier.end)
1304
+ );
1305
+ const at = texts.indexOf(want.name);
1306
+ if (at === -1) {
1307
+ return source;
1308
+ }
1309
+ if (declaration.specifiers.length === 1) {
1310
+ const lineEnd = source.indexOf("\n", declaration.end);
1311
+ return lineEnd === -1 ? source.slice(0, declaration.start) : `${source.slice(0, declaration.start)}${source.slice(lineEnd + 1)}`;
1312
+ }
1313
+ return replaceDeclaration(
1314
+ source,
1315
+ declaration,
1316
+ texts.toSpliced(at, 1),
1317
+ want.from
1318
+ );
1319
+ }
1320
+ function importedElsewhere(source, want) {
1321
+ const program = parseModule4(source).$ast;
1322
+ for (const statement of program.body) {
1323
+ if (statement.type !== "ImportDeclaration") {
1324
+ continue;
1325
+ }
1326
+ const declaration = statement;
1327
+ for (const specifier of declaration.specifiers) {
1328
+ if (localName(specifier) !== want.name) {
1329
+ continue;
1330
+ }
1331
+ const ours = specifier.type === "ImportSpecifier" && importedName(specifier) === want.name && sourceValue(declaration) === want.from;
1332
+ if (!ours) {
1333
+ return true;
1334
+ }
1335
+ }
1336
+ }
1337
+ return false;
1338
+ }
1339
+ function replaceDeclaration(source, declaration, named, from) {
1340
+ const leading = declaration.specifiers.filter((specifier) => specifier.type !== "ImportSpecifier").map((specifier) => source.slice(specifier.start, specifier.end));
1341
+ const text4 = source.slice(declaration.start, declaration.end);
1342
+ const tail = `from ${JSON.stringify(from)}${text4.trimEnd().endsWith(";") ? ";" : ""}`;
1343
+ const keyword = declaration.importKind === "type" ? "import type" : "import";
1344
+ const inline = [...leading, `{ ${named.join(", ")} }`].join(", ");
1345
+ const oneLine = `${keyword} ${inline} ${tail}`;
1346
+ const printed2 = oneLine.length <= PRINT_WIDTH ? oneLine : `${[keyword, ...leading.map((part) => `${part},`)].join(" ")} {
1347
+ ${named.map((specifier) => ` ${specifier},
1348
+ `).join("")}} ${tail}`;
1349
+ return `${source.slice(0, declaration.start)}${printed2}${source.slice(declaration.end)}`;
1350
+ }
1351
+ function insertDeclaration(source, program, want) {
1352
+ const statement = `import { ${want.name} } from ${JSON.stringify(want.from)};`;
1353
+ const last = program.body.findLast(
1354
+ (node) => node.type === "ImportDeclaration"
1355
+ );
1356
+ if (!last) {
1357
+ return `${statement}
1358
+
1359
+ ${source}`;
1360
+ }
1361
+ return `${source.slice(0, last.end)}
1362
+ ${statement}${source.slice(last.end)}`;
1363
+ }
1364
+ function findImport(program, from) {
1365
+ for (const statement of program.body) {
1366
+ if (statement.type !== "ImportDeclaration") {
1367
+ continue;
1368
+ }
1369
+ const declaration = statement;
1370
+ if (declaration.importKind === "type") {
1371
+ continue;
1372
+ }
1373
+ if (sourceValue(declaration) === from) {
1374
+ return declaration;
1375
+ }
1376
+ }
1377
+ return void 0;
1378
+ }
1379
+ function namedSpecifiers(declaration) {
1380
+ return declaration.specifiers.filter(
1381
+ (specifier) => specifier.type === "ImportSpecifier" && specifier.importKind !== "type"
1382
+ );
1383
+ }
1384
+ function localOf(text4) {
1385
+ const [, local] = text4.split(/\s+as\s+/);
1386
+ return (local ?? text4).trim();
1387
+ }
1388
+ function sourceValue(declaration) {
1389
+ const value = declaration.source.value;
1390
+ return typeof value === "string" ? value : void 0;
1391
+ }
1392
+ function importedName(specifier) {
1393
+ return identifierName(specifier.imported);
1394
+ }
1395
+ function localName(specifier) {
1396
+ return identifierName(specifier.local);
1397
+ }
1398
+ function identifierName(node) {
1399
+ if (node?.type === "Identifier") {
1400
+ return node.name;
1401
+ }
1402
+ if (node?.type === "StringLiteral" || node?.type === "Literal") {
1403
+ const value = node.value;
1404
+ return typeof value === "string" ? value : void 0;
1405
+ }
1406
+ return void 0;
1407
+ }
1408
+
1409
+ // src/lib/patch/nav-entry.ts
1410
+ var IDENTITY = "to";
1411
+ function insertNavEntry(source, patch) {
1412
+ const items = findItems(source, patch);
1413
+ const wanted = patch.entry[IDENTITY];
1414
+ if (!items || wanted === void 0 || hasEntry(items, IDENTITY, wanted)) {
1415
+ return source;
1416
+ }
1417
+ if (importedElsewhere(source, patch.icon)) {
1418
+ return source;
1419
+ }
1420
+ const withEntry = appendEntry(source, items, renderItem(patch));
1421
+ return ensureNamedImport(withEntry, patch.icon);
1422
+ }
1423
+ function navEntryInsertRefusal(source, patch) {
1424
+ const program = parseModule5(source).$ast;
1425
+ const areas = findConstArray(program, patch.constName);
1426
+ if (!areas) {
1427
+ return `nav-entry could not find a module-scope const array named ${patch.constName}`;
1428
+ }
1429
+ const area = findElement(areas, IDENTITY, patch.area);
1430
+ if (!area) {
1431
+ return `nav-entry could not find an area with ${IDENTITY} ${JSON.stringify(patch.area)} in ${patch.constName}`;
1432
+ }
1433
+ const groups = arrayProperty(area, "groups");
1434
+ if (!groups) {
1435
+ return `nav-entry found the ${JSON.stringify(patch.area)} area but it has no groups array`;
1436
+ }
1437
+ const group = findElement(groups, "label", patch.group);
1438
+ if (!group) {
1439
+ return `nav-entry could not find a group labelled ${JSON.stringify(patch.group)} in the ${JSON.stringify(patch.area)} area`;
1440
+ }
1441
+ if (!arrayProperty(group, "items")) {
1442
+ return `nav-entry found the ${JSON.stringify(patch.group)} group but it has no items array`;
1443
+ }
1444
+ if (patch.entry[IDENTITY] === void 0) {
1445
+ return `nav-entry entry has no identity property named ${IDENTITY}`;
1446
+ }
1447
+ if (importedElsewhere(source, patch.icon)) {
1448
+ return `${JSON.stringify(patch.icon.name)} is already bound to something other than the named import from ${JSON.stringify(patch.icon.from)}, so the icon would render the wrong component`;
1449
+ }
1450
+ return void 0;
1451
+ }
1452
+ function removeNavEntry(source, patch) {
1453
+ const items = findItems(source, patch);
1454
+ const wanted = patch.entry[IDENTITY];
1455
+ if (!items || wanted === void 0) {
1456
+ return source;
1457
+ }
1458
+ const index = items.elements.findIndex(
1459
+ (element) => entryValue(element, IDENTITY) === wanted
1460
+ );
1461
+ if (index === -1) {
1462
+ return source;
1463
+ }
1464
+ const withoutEntry = removeEntry(source, items, index);
1465
+ return removeNamedImport(withoutEntry, patch.icon);
1466
+ }
1467
+ function navEntryRemoveRefusal(source, patch) {
1468
+ const items = findItems(source, patch);
1469
+ if (!items) {
1470
+ return `nav-entry could not find the ${JSON.stringify(patch.group)} group of the ${JSON.stringify(patch.area)} area in ${patch.constName}`;
1471
+ }
1472
+ return void 0;
1473
+ }
1474
+ function findItems(source, patch) {
1475
+ const program = parseModule5(source).$ast;
1476
+ const areas = findConstArray(program, patch.constName);
1477
+ if (!areas) {
1478
+ return void 0;
1479
+ }
1480
+ const area = findElement(areas, IDENTITY, patch.area);
1481
+ const groups = area && arrayProperty(area, "groups");
1482
+ if (!groups) {
1483
+ return void 0;
1484
+ }
1485
+ const group = findElement(groups, "label", patch.group);
1486
+ return group ? arrayProperty(group, "items") : void 0;
1487
+ }
1488
+ function arrayProperty(object, key) {
1489
+ const value = propertyValue(object, key);
1490
+ return value ? asArray(value) : void 0;
1491
+ }
1492
+ function renderItem(patch) {
1493
+ const properties = [
1494
+ ...renderProperties(patch.entry),
1495
+ `${renderKey("icon")}: ${patch.icon.name}`
1496
+ ];
1497
+ return `{ ${properties.join(", ")} }`;
1498
+ }
1499
+
1214
1500
  // src/lib/patch/pkg-json.ts
1215
1501
  import {
1216
1502
  applyEdits as applyEdits2,
@@ -1325,9 +1611,9 @@ function upsertPackageJsonScript(source, patch) {
1325
1611
  }
1326
1612
 
1327
1613
  // src/lib/patch/ts-module.ts
1328
- import { builders as builders2, generateCode as generateCode3, parseModule as parseModule4 } from "magicast";
1614
+ import { builders as builders2, generateCode as generateCode3, parseModule as parseModule6 } from "magicast";
1329
1615
  function insertIntoPluginArray(source, patch) {
1330
- const mod = parseModule4(source);
1616
+ const mod = parseModule6(source);
1331
1617
  const exported = mod.exports[patch.exportName];
1332
1618
  const callArg = exported?.$args?.[0];
1333
1619
  if (!callArg) {
@@ -1353,7 +1639,7 @@ function insertIntoPluginArray(source, patch) {
1353
1639
  return print2(source, mod);
1354
1640
  }
1355
1641
  function removeFromPluginArray(source, patch) {
1356
- const mod = parseModule4(source);
1642
+ const mod = parseModule6(source);
1357
1643
  const exported = mod.exports[patch.exportName];
1358
1644
  const callArg = exported?.$args?.[0];
1359
1645
  if (!callArg) {
@@ -1381,7 +1667,7 @@ function removeFromPluginArray(source, patch) {
1381
1667
  return print2(source, mod);
1382
1668
  }
1383
1669
  function pluginArrayRemoveRefusal(source, patch) {
1384
- const mod = parseModule4(source);
1670
+ const mod = parseModule6(source);
1385
1671
  const callArg = mod.exports[patch.exportName]?.$args?.[0];
1386
1672
  if (!callArg) {
1387
1673
  return void 0;
@@ -1433,6 +1719,7 @@ var PATCH_KIND_KEYS = {
1433
1719
  "chained-route": true,
1434
1720
  "const-array": true,
1435
1721
  "drizzle-column": true,
1722
+ "nav-entry": true,
1436
1723
  "package-json-dependency": true,
1437
1724
  "package-json-script": true,
1438
1725
  "plugin-array": true,
@@ -1469,6 +1756,9 @@ function applyCodemod(source, patch) {
1469
1756
  case "const-array": {
1470
1757
  return insertIntoConstArray(source, patch);
1471
1758
  }
1759
+ case "nav-entry": {
1760
+ return insertNavEntry(source, patch);
1761
+ }
1472
1762
  case "drizzle-column": {
1473
1763
  return insertDrizzleColumn(source, patch);
1474
1764
  }
@@ -1497,6 +1787,7 @@ var INVERSES = {
1497
1787
  "chained-route": removeChainedRoute,
1498
1788
  "const-array": removeFromConstArray,
1499
1789
  "drizzle-column": removeDrizzleColumn,
1790
+ "nav-entry": removeNavEntry,
1500
1791
  "plugin-array": removeFromPluginArray,
1501
1792
  "wrangler-binding": removeWranglerBinding
1502
1793
  };
@@ -1521,11 +1812,13 @@ var REFUSALS = {
1521
1812
  "chained-route": chainedRouteInsertRefusal,
1522
1813
  "const-array": constArrayInsertRefusal,
1523
1814
  "drizzle-column": drizzleColumnInsertRefusal,
1815
+ "nav-entry": navEntryInsertRefusal,
1524
1816
  "package-json-script": packageJsonScriptRefusal
1525
1817
  };
1526
1818
  var REVERSAL_REFUSALS = {
1527
1819
  "chained-route": chainedRouteRemoveRefusal,
1528
1820
  "drizzle-column": drizzleColumnRemoveRefusal,
1821
+ "nav-entry": navEntryRemoveRefusal,
1529
1822
  "plugin-array": pluginArrayRemoveRefusal,
1530
1823
  "wrangler-binding": wranglerBindingRemoveRefusal
1531
1824
  };
@@ -1957,11 +2250,12 @@ async function executePlan(plan, root, config, manifest) {
1957
2250
  } else {
1958
2251
  refreshed.push(file);
1959
2252
  }
1960
- manifest.managed[file.target] = {
1961
- module: file.module,
2253
+ recordManagedFile(manifest, {
2254
+ from: file.from,
1962
2255
  hash: file.newHash,
1963
- from: file.from
1964
- };
2256
+ module: file.module,
2257
+ target: file.target
2258
+ });
1965
2259
  }
1966
2260
  for (const p of plan.patches) {
1967
2261
  if (!await pathExists(p.fileAbs)) {
@@ -1974,14 +2268,11 @@ async function executePlan(plan, root, config, manifest) {
1974
2268
  if (changed) {
1975
2269
  await writeFile3(p.fileAbs, content, "utf-8");
1976
2270
  patched.push(p);
1977
- const entry = {
1978
- module: p.module,
2271
+ recordPatch(manifest, {
1979
2272
  file: p.file,
2273
+ module: p.module,
1980
2274
  patch: p.patch
1981
- };
1982
- if (!manifest.patches.some((existing) => samePatchEntry(existing, entry))) {
1983
- manifest.patches.push(entry);
1984
- }
2275
+ });
1985
2276
  } else if (reason !== void 0) {
1986
2277
  patchRefusals.push({ patch: p, reason });
1987
2278
  }
@@ -1995,7 +2286,7 @@ async function executePlan(plan, root, config, manifest) {
1995
2286
  await assertNoSymlinkPath(root, link.pathAbs);
1996
2287
  await createDirLink(link.pathAbs, link.targetAbs);
1997
2288
  }
1998
- manifest.links[link.target] = link.path;
2289
+ recordLink(manifest, link.target, link.path);
1999
2290
  links.push(link);
2000
2291
  }
2001
2292
  for (const [alias, prefix] of Object.entries(plan.aliases)) {
@@ -2633,6 +2924,24 @@ function upsertLock(lock, provenance, installed, graph) {
2633
2924
  }
2634
2925
  }
2635
2926
 
2927
+ // src/lib/ledger.ts
2928
+ async function persistLedger(input) {
2929
+ const saves = [
2930
+ [MANIFEST_FILE, () => saveManifest(input.root, input.manifest)],
2931
+ [CONFIG_FILE, () => saveConfig(input.root, input.config)],
2932
+ [LOCK_FILE, () => saveLock(input.root, input.lock)]
2933
+ ];
2934
+ const failures = [];
2935
+ for (const [file, save] of saves) {
2936
+ try {
2937
+ await save();
2938
+ } catch (error) {
2939
+ failures.push({ error, file });
2940
+ }
2941
+ }
2942
+ return failures;
2943
+ }
2944
+
2636
2945
  // src/lib/pkg-json.ts
2637
2946
  import { readFile as readFile8, writeFile as writeFile6 } from "fs/promises";
2638
2947
  import { join as join6 } from "path";
@@ -3478,18 +3787,11 @@ async function persistState(input) {
3478
3787
  failures.push(error);
3479
3788
  log.warn(`Couldn't pin ${LOCK_FILE} \u2014 ${formatFailure(error)}.`);
3480
3789
  }
3481
- const saves = [
3482
- [MANIFEST_FILE, () => saveManifest(input.root, input.manifest)],
3483
- [CONFIG_FILE, () => saveConfig(input.root, input.config)],
3484
- [LOCK_FILE, () => saveLock(input.root, input.lock)]
3485
- ];
3486
- for (const [file, save] of saves) {
3487
- try {
3488
- await save();
3489
- } catch (error) {
3490
- failures.push(error);
3491
- log.warn(`Couldn't write ${file} \u2014 ${formatFailure(error)}.`);
3492
- }
3790
+ for (const failure of await persistLedger(input)) {
3791
+ failures.push(failure.error);
3792
+ log.warn(
3793
+ `Couldn't write ${failure.file} \u2014 ${formatFailure(failure.error)}.`
3794
+ );
3493
3795
  }
3494
3796
  return failures;
3495
3797
  }
@@ -3881,7 +4183,7 @@ import { fileURLToPath as fileURLToPath3 } from "url";
3881
4183
  // package.json
3882
4184
  var package_default = {
3883
4185
  name: "saasaloy",
3884
- version: "0.4.0",
4186
+ version: "0.5.1",
3885
4187
  description: "Composable SaaS accelerator kit.",
3886
4188
  keywords: [
3887
4189
  "saas",
@@ -4107,16 +4409,17 @@ function baseEntries(manifest) {
4107
4409
  function recordBaseFiles(manifest, files) {
4108
4410
  for (const [target, entry] of Object.entries(manifest.managed)) {
4109
4411
  if (entry.module === BASE_MODULE) {
4110
- delete manifest.managed[target];
4412
+ untrackManagedFile(manifest, target);
4111
4413
  }
4112
4414
  }
4113
4415
  for (const file of files) {
4114
- manifest.managed[file.target] = {
4115
- module: BASE_MODULE,
4416
+ recordManagedFile(manifest, {
4417
+ adopted: file.adopted,
4418
+ from: file.from,
4116
4419
  hash: file.hash,
4117
- ...file.adopted ? { adopted: true } : {},
4118
- from: file.from
4119
- };
4420
+ module: BASE_MODULE,
4421
+ target: file.target
4422
+ });
4120
4423
  }
4121
4424
  }
4122
4425
  function isBaseTracked(lock, manifest) {
@@ -4250,7 +4553,7 @@ async function baseUpdateInput(args) {
4250
4553
 
4251
4554
  // src/lib/doctor.ts
4252
4555
  import { findNodeAtLocation as findNodeAtLocation4, getNodeValue as getNodeValue4, parseTree as parseTree4 } from "jsonc-parser";
4253
- import { parseModule as parseModule5 } from "magicast";
4556
+ import { parseModule as parseModule7 } from "magicast";
4254
4557
  import { readFile as readFile13 } from "fs/promises";
4255
4558
  import { basename, dirname as dirname6, join as join11, posix as posix7, resolve as resolve3 } from "path";
4256
4559
  var SKILL_PREFIX = "saasaloy-";
@@ -4258,7 +4561,7 @@ var PINNED_DEP = /^(@[^/@]+\/)?[^/@]+@\d+\.\d+\.\d+(-[\w.-]+)?(\+[\w.-]+)?$/;
4258
4561
  function finding(module, where, message) {
4259
4562
  return { message, module, where };
4260
4563
  }
4261
- function asArray(value) {
4564
+ function asArray2(value) {
4262
4565
  return Array.isArray(value) ? value : [];
4263
4566
  }
4264
4567
  function asRecord(value) {
@@ -4282,7 +4585,7 @@ async function collectAliases(registryDir, names) {
4282
4585
  const fromScaffolds = {};
4283
4586
  for (const name of names) {
4284
4587
  const item = asRecord(await readDescriptor(join11(registryDir, name)));
4285
- for (const scaffold of asArray(item.scaffolds)) {
4588
+ for (const scaffold of asArray2(item.scaffolds)) {
4286
4589
  const aliases = asRecord(asRecord(scaffold).aliases);
4287
4590
  for (const [alias, prefix] of Object.entries(aliases)) {
4288
4591
  if (typeof prefix === "string") {
@@ -4346,7 +4649,7 @@ async function checkModule(input) {
4346
4649
  }
4347
4650
  const known = { ...aliases.base, ...aliases.fromScaffolds };
4348
4651
  const knownList = Object.keys(known).toSorted().join(", ") || "(none)";
4349
- for (const [index, entry] of asArray(item.files).entries()) {
4652
+ for (const [index, entry] of asArray2(item.files).entries()) {
4350
4653
  await checkSourcePath(findings, module, dir, `/files/${index}/path`, entry);
4351
4654
  const target = asRecord(entry).target;
4352
4655
  const alias = typeof target === "string" ? target.split("/")[0] : void 0;
@@ -4360,8 +4663,8 @@ async function checkModule(input) {
4360
4663
  );
4361
4664
  }
4362
4665
  }
4363
- for (const [index, scaffold] of asArray(item.scaffolds).entries()) {
4364
- for (const [fileIndex, entry] of asArray(
4666
+ for (const [index, scaffold] of asArray2(item.scaffolds).entries()) {
4667
+ for (const [fileIndex, entry] of asArray2(
4365
4668
  asRecord(scaffold).files
4366
4669
  ).entries()) {
4367
4670
  await checkSourcePath(
@@ -4379,7 +4682,7 @@ async function checkModule(input) {
4379
4682
  ["requiresOneOf", item.requiresOneOf],
4380
4683
  ["conflictsWith", item.conflictsWith]
4381
4684
  ]) {
4382
- for (const [index, name] of asArray(names).entries()) {
4685
+ for (const [index, name] of asArray2(names).entries()) {
4383
4686
  if (typeof name === "string" && !siblingSet.has(name)) {
4384
4687
  findings.push(
4385
4688
  finding(
@@ -4395,7 +4698,7 @@ async function checkModule(input) {
4395
4698
  ["dependencies", item.dependencies],
4396
4699
  ["devDependencies", item.devDependencies]
4397
4700
  ]) {
4398
- for (const [index, dep] of asArray(deps).entries()) {
4701
+ for (const [index, dep] of asArray2(deps).entries()) {
4399
4702
  if (typeof dep === "string" && !PINNED_DEP.test(dep)) {
4400
4703
  findings.push(
4401
4704
  finding(
@@ -4438,7 +4741,7 @@ async function checkModule(input) {
4438
4741
  );
4439
4742
  }
4440
4743
  }
4441
- for (const [index, skill] of asArray(asRecord(item.agent).skills).entries()) {
4744
+ for (const [index, skill] of asArray2(asRecord(item.agent).skills).entries()) {
4442
4745
  if (typeof skill !== "string") {
4443
4746
  continue;
4444
4747
  }
@@ -4561,7 +4864,7 @@ function rateLimitBindingNames(wrangler) {
4561
4864
  function registeredPolicies(args) {
4562
4865
  let mod;
4563
4866
  try {
4564
- mod = parseModule5(args.index);
4867
+ mod = parseModule7(args.index);
4565
4868
  } catch {
4566
4869
  return [];
4567
4870
  }
@@ -4609,7 +4912,7 @@ function calleeName(element) {
4609
4912
  function policyNameIn(source, callee) {
4610
4913
  let program;
4611
4914
  try {
4612
- program = parseModule5(source).$ast;
4915
+ program = parseModule7(source).$ast;
4613
4916
  } catch {
4614
4917
  return void 0;
4615
4918
  }
@@ -4624,8 +4927,8 @@ function policyNameIn(source, callee) {
4624
4927
  factory,
4625
4928
  (node) => node.type === "CallExpression" && asRecord(node.callee).name === "definePolicy"
4626
4929
  );
4627
- const argument = asArray(call?.arguments)[0];
4628
- const property = asArray(asRecord(argument).properties).find(
4930
+ const argument = asArray2(call?.arguments)[0];
4931
+ const property = asArray2(asRecord(argument).properties).find(
4629
4932
  (entry) => asRecord(asRecord(entry).key).name === "name"
4630
4933
  );
4631
4934
  const value = asRecord(asRecord(property).value).value;
@@ -4688,7 +4991,7 @@ async function readPolicyState(root, installed) {
4688
4991
  function relativeImportsIn(index) {
4689
4992
  let mod;
4690
4993
  try {
4691
- mod = parseModule5(index);
4994
+ mod = parseModule7(index);
4692
4995
  } catch {
4693
4996
  return [];
4694
4997
  }
@@ -6552,7 +6855,7 @@ async function executeRemovePlan(plan, args) {
6552
6855
  } else {
6553
6856
  missingUntracked.push(file);
6554
6857
  }
6555
- delete manifest.managed[file.target];
6858
+ untrackManagedFile(manifest, file.target);
6556
6859
  }
6557
6860
  const linksRemoved = [];
6558
6861
  const linkConflicts = [];
@@ -6568,7 +6871,7 @@ async function executeRemovePlan(plan, args) {
6568
6871
  } else if (link.action === "conflict") {
6569
6872
  linkConflicts.push(link);
6570
6873
  }
6571
- delete manifest.links[link.target];
6874
+ untrackLink(manifest, link.target);
6572
6875
  }
6573
6876
  const patchesReversed = [];
6574
6877
  const patchesDropped = [];
@@ -6591,9 +6894,7 @@ async function executeRemovePlan(plan, args) {
6591
6894
  }
6592
6895
  }
6593
6896
  (reversed ? patchesReversed : patchesDropped).push(entry);
6594
- manifest.patches = manifest.patches.filter(
6595
- (p) => !samePatchEntry(p, entry)
6596
- );
6897
+ untrackPatch(manifest, entry);
6597
6898
  }
6598
6899
  const prunedDirs = await pruneEmptyDirs(root, [
6599
6900
  ...deleted.map((f) => f.targetAbs),
@@ -7173,11 +7474,12 @@ async function executeUpdatePlan(plan, args) {
7173
7474
  await writeFile11(file.targetAbs, file.theirs, "utf-8");
7174
7475
  result.written.push(file);
7175
7476
  }
7176
- manifest.managed[file.target] = {
7177
- module: file.module,
7477
+ recordManagedFile(manifest, {
7478
+ from: file.from,
7178
7479
  hash: file.newHash ?? hashContent(file.theirs),
7179
- from: file.from
7180
- };
7480
+ module: file.module,
7481
+ target: file.target
7482
+ });
7181
7483
  }
7182
7484
  for (const file of mod.removals) {
7183
7485
  if (file.action === "delete") {
@@ -7193,7 +7495,7 @@ async function executeUpdatePlan(plan, args) {
7193
7495
  } else {
7194
7496
  result.untracked.push(file);
7195
7497
  }
7196
- delete manifest.managed[file.target];
7498
+ untrackManagedFile(manifest, file.target);
7197
7499
  }
7198
7500
  for (const p of mod.patches) {
7199
7501
  if (!await pathExists(p.fileAbs)) {
@@ -7208,14 +7510,11 @@ async function executeUpdatePlan(plan, args) {
7208
7510
  }
7209
7511
  await writeFile11(p.fileAbs, content, "utf-8");
7210
7512
  result.patched.push(p);
7211
- const entry = {
7212
- module: p.module,
7513
+ recordPatch(manifest, {
7213
7514
  file: p.file,
7515
+ module: p.module,
7214
7516
  patch: p.patch
7215
- };
7216
- if (!manifest.patches.some((existing) => samePatchEntry(existing, entry))) {
7217
- manifest.patches.push(entry);
7218
- }
7517
+ });
7219
7518
  }
7220
7519
  for (const link of mod.links) {
7221
7520
  if (link.action === "conflict") {
@@ -7225,7 +7524,7 @@ async function executeUpdatePlan(plan, args) {
7225
7524
  if (link.action === "create") {
7226
7525
  await createDirLink(link.pathAbs, link.targetAbs);
7227
7526
  }
7228
- manifest.links[link.target] = link.path;
7527
+ recordLink(manifest, link.target, link.path);
7229
7528
  result.links.push(link);
7230
7529
  }
7231
7530
  for (const bump of mod.depBumps) {
@@ -7737,6 +8036,7 @@ async function runRemove(argv) {
7737
8036
  }
7738
8037
  }
7739
8038
  let result;
8039
+ let ledgerFailures = [];
7740
8040
  try {
7741
8041
  result = await executeRemovePlan(plan, {
7742
8042
  root,
@@ -7746,9 +8046,11 @@ async function runRemove(argv) {
7746
8046
  deleteDrifted
7747
8047
  });
7748
8048
  } finally {
7749
- await saveManifest(root, manifest);
7750
- await saveConfig(root, config);
7751
- await saveLock(root, lock);
8049
+ ledgerFailures = await persistLedger({ config, lock, manifest, root });
8050
+ }
8051
+ const failedSave = ledgerFailures[0];
8052
+ if (failedSave) {
8053
+ throw failedSave.error;
7752
8054
  }
7753
8055
  for (const file of result.deleted) {
7754
8056
  log6.step(`${pc10.red("delete")} ${file.target}`);
@@ -8705,7 +9007,11 @@ async function runUpdate(argv) {
8705
9007
  const skipped = comparisons.filter((c) => c.status !== "outdated");
8706
9008
  if (outdated.length === 0) {
8707
9009
  if (!preview && recordRefRewrites(lock, comparisons).length > 0) {
8708
- await saveLock(root, lock);
9010
+ const failures2 = await persistLedger({ config, lock, manifest, root });
9011
+ const failedSave2 = failures2[0];
9012
+ if (failedSave2) {
9013
+ throw failedSave2.error;
9014
+ }
8709
9015
  }
8710
9016
  for (const comparison of skipped) {
8711
9017
  log7.info(
@@ -8932,17 +9238,13 @@ async function runUpdate(argv) {
8932
9238
  (value) => ({ ok: true, value }),
8933
9239
  (error) => ({ ok: false, error })
8934
9240
  );
8935
- const saves = await Promise.allSettled([
8936
- saveManifest(root, manifest),
8937
- saveConfig(root, config),
8938
- saveLock(root, lock)
8939
- ]);
9241
+ const failures = await persistLedger({ config, lock, manifest, root });
8940
9242
  if (!outcome.ok) {
8941
9243
  throw outcome.error;
8942
9244
  }
8943
- const failedSave = saves.find((save) => save.status === "rejected");
9245
+ const failedSave = failures[0];
8944
9246
  if (failedSave) {
8945
- throw failedSave.reason;
9247
+ throw failedSave.error;
8946
9248
  }
8947
9249
  const result = outcome.value;
8948
9250
  reportResult(result);