saasaloy 0.1.2 → 0.3.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 +5 -4
- package/dist/index.js +735 -63
- package/dist/index.js.map +1 -1
- package/package.json +7 -7
- package/schemas/manifest.schema.json +6 -1
- package/schemas/registry-item.schema.json +47 -2
- package/schemas/saasaloy.schema.json +5 -0
- package/templates/base/AGENTS.md +8 -1
- package/templates/base/DESIGN.md +4 -0
- package/templates/base/_agents/skills/saasaloy-design/SKILL.md +1 -1
- package/templates/base/_gitignore +4 -1
- package/templates/base/_saasaloy-base.json +10 -0
- package/templates/base/apps/web/package.json +1 -1
- package/templates/base/package.json +4 -4
- package/templates/base/packages/ui/package.json +3 -3
- package/templates/base/packages/ui/src/blocks/footer.tsx +5 -1
- package/templates/base/packages/ui/src/components/avatar.tsx +107 -0
- package/templates/base/packages/ui/src/components/collapsible.tsx +19 -0
- package/templates/base/packages/ui/src/components/dropdown-menu.tsx +270 -0
- package/templates/base/packages/ui/src/components/scroll-area.tsx +52 -0
- package/templates/base/packages/ui/src/components/sheet.tsx +132 -0
- package/templates/base/packages/ui/src/components/table.tsx +114 -0
- package/templates/base/packages/ui/src/components/tabs.tsx +81 -0
- package/templates/base/packages/ui/src/components/tooltip.tsx +64 -0
- package/templates/base/packages/ui/src/lib/sentinel.ts +1 -1
- package/templates/base/packages/ui/src/lib/theme.ts +124 -1
- package/templates/base/packages/ui/src/styles/globals.css +28 -1
- package/templates/base/stylelint.config.js +8 -0
package/dist/index.js
CHANGED
|
@@ -30,6 +30,7 @@ import { dirname as dirname3, posix as posix2 } from "path";
|
|
|
30
30
|
var EXIT_OK = 0;
|
|
31
31
|
var EXIT_FAILURE = 1;
|
|
32
32
|
var EXIT_REFUSED = 2;
|
|
33
|
+
var EXIT_MERGE_PENDING = 3;
|
|
33
34
|
var RefusalError = class extends Error {
|
|
34
35
|
name = "RefusalError";
|
|
35
36
|
};
|
|
@@ -489,7 +490,7 @@ function insertChainedRoute(source, patch) {
|
|
|
489
490
|
return source;
|
|
490
491
|
}
|
|
491
492
|
if (!(patch.import.name in mod.imports)) {
|
|
492
|
-
mod.imports.$
|
|
493
|
+
mod.imports.$prepend({
|
|
493
494
|
from: patch.import.from,
|
|
494
495
|
imported: patch.import.name,
|
|
495
496
|
local: patch.import.name
|
|
@@ -863,6 +864,193 @@ function toDiff(before, after, filename) {
|
|
|
863
864
|
return patch;
|
|
864
865
|
}
|
|
865
866
|
|
|
867
|
+
// src/lib/patch/drizzle-column.ts
|
|
868
|
+
import { generateCode as generateCode2, parseModule as parseModule3 } from "magicast";
|
|
869
|
+
var COLUMNS_ARG = 1;
|
|
870
|
+
function insertDrizzleColumn(source, patch) {
|
|
871
|
+
const mod = parseModule3(source);
|
|
872
|
+
const columns = columnsObject(mod, patch);
|
|
873
|
+
if (!columns) {
|
|
874
|
+
return source;
|
|
875
|
+
}
|
|
876
|
+
if (indexOfColumn(columns, patch.column) !== -1) {
|
|
877
|
+
return source;
|
|
878
|
+
}
|
|
879
|
+
parseExpressionNode(patch.value);
|
|
880
|
+
if (patch.import && foreignBinding(mod.imports, patch.import)) {
|
|
881
|
+
return source;
|
|
882
|
+
}
|
|
883
|
+
const withColumn = spliceColumn(source, columns, patch);
|
|
884
|
+
if (!withColumn) {
|
|
885
|
+
return source;
|
|
886
|
+
}
|
|
887
|
+
return addImport(withColumn, patch);
|
|
888
|
+
}
|
|
889
|
+
function removeDrizzleColumn(source, patch) {
|
|
890
|
+
const mod = parseModule3(source);
|
|
891
|
+
const columns = columnsObject(mod, patch);
|
|
892
|
+
const at = columns ? indexOfColumn(columns, patch.column) : -1;
|
|
893
|
+
if (!columns || at === -1) {
|
|
894
|
+
return source;
|
|
895
|
+
}
|
|
896
|
+
if (valueDrift(columns, at, patch)) {
|
|
897
|
+
return source;
|
|
898
|
+
}
|
|
899
|
+
if (patch.import && removeBindingConflict2(mod.imports, patch.import)) {
|
|
900
|
+
return source;
|
|
901
|
+
}
|
|
902
|
+
const withoutColumn = cutColumn(source, columns, at);
|
|
903
|
+
if (!withoutColumn) {
|
|
904
|
+
return source;
|
|
905
|
+
}
|
|
906
|
+
return dropImport(withoutColumn, patch);
|
|
907
|
+
}
|
|
908
|
+
function spliceColumn(source, columns, patch) {
|
|
909
|
+
const last = columns.properties.at(-1);
|
|
910
|
+
const end = offset(last, "end");
|
|
911
|
+
if (end === void 0) {
|
|
912
|
+
return void 0;
|
|
913
|
+
}
|
|
914
|
+
const indent = indentOf(source, offset(last, "start") ?? end);
|
|
915
|
+
const comma = source.slice(end).trimStart().startsWith(",");
|
|
916
|
+
const at = comma ? end + source.slice(end).indexOf(",") + 1 : end;
|
|
917
|
+
const line = `${indent}${patch.column}: ${patch.value}`;
|
|
918
|
+
return comma ? `${source.slice(0, at)}
|
|
919
|
+
${line},${source.slice(at)}` : `${source.slice(0, at)},
|
|
920
|
+
${line}${source.slice(at)}`;
|
|
921
|
+
}
|
|
922
|
+
function cutColumn(source, columns, at) {
|
|
923
|
+
const property = columns.properties[at];
|
|
924
|
+
const start = offset(property, "start");
|
|
925
|
+
const end = offset(property, "end");
|
|
926
|
+
if (start === void 0 || end === void 0) {
|
|
927
|
+
return void 0;
|
|
928
|
+
}
|
|
929
|
+
const from = source.lastIndexOf("\n", start) + 1;
|
|
930
|
+
const after = source.indexOf("\n", end);
|
|
931
|
+
const to = after === -1 ? source.length : after + 1;
|
|
932
|
+
const head = source.slice(from, start);
|
|
933
|
+
const tail = source.slice(end, to);
|
|
934
|
+
if (head.trim() !== "" || tail.replace(/^,/u, "").trim() !== "") {
|
|
935
|
+
return void 0;
|
|
936
|
+
}
|
|
937
|
+
return source.slice(0, from) + source.slice(to);
|
|
938
|
+
}
|
|
939
|
+
function indentOf(source, at) {
|
|
940
|
+
const from = source.lastIndexOf("\n", at) + 1;
|
|
941
|
+
return /^[\t ]*/u.exec(source.slice(from, at))?.[0] ?? "";
|
|
942
|
+
}
|
|
943
|
+
function offset(node, end) {
|
|
944
|
+
const value = node?.[end];
|
|
945
|
+
return typeof value === "number" ? value : void 0;
|
|
946
|
+
}
|
|
947
|
+
function addImport(source, patch) {
|
|
948
|
+
if (!patch.import) {
|
|
949
|
+
return source;
|
|
950
|
+
}
|
|
951
|
+
const mod = parseModule3(source);
|
|
952
|
+
if (patch.import.name in mod.imports) {
|
|
953
|
+
return source;
|
|
954
|
+
}
|
|
955
|
+
mod.imports.$prepend({
|
|
956
|
+
from: patch.import.from,
|
|
957
|
+
imported: patch.import.name,
|
|
958
|
+
local: patch.import.name
|
|
959
|
+
});
|
|
960
|
+
return print(source, mod);
|
|
961
|
+
}
|
|
962
|
+
function dropImport(source, patch) {
|
|
963
|
+
if (!patch.import) {
|
|
964
|
+
return source;
|
|
965
|
+
}
|
|
966
|
+
const mod = parseModule3(source);
|
|
967
|
+
const program = mod.$ast;
|
|
968
|
+
if (!(patch.import.name in mod.imports) || isReferenced(program, patch.import.name)) {
|
|
969
|
+
return source;
|
|
970
|
+
}
|
|
971
|
+
delete mod.imports[patch.import.name];
|
|
972
|
+
return print(source, mod);
|
|
973
|
+
}
|
|
974
|
+
function drizzleColumnInsertRefusal(source, patch) {
|
|
975
|
+
const mod = parseModule3(source);
|
|
976
|
+
if (!mod.exports[patch.exportName]) {
|
|
977
|
+
return `drizzle-column could not find an export named ${JSON.stringify(patch.exportName)}`;
|
|
978
|
+
}
|
|
979
|
+
const columns = columnsObject(mod, patch);
|
|
980
|
+
if (!columns) {
|
|
981
|
+
return `${JSON.stringify(patch.exportName)} is not a Drizzle table call with a columns object as its second argument`;
|
|
982
|
+
}
|
|
983
|
+
const at = indexOfColumn(columns, patch.column);
|
|
984
|
+
if (at !== -1 && valueDrift(columns, at, patch)) {
|
|
985
|
+
return `${JSON.stringify(patch.column)} on ${JSON.stringify(patch.exportName)} holds ${printed(columns, at)}, not the ${patch.value} this patch adds`;
|
|
986
|
+
}
|
|
987
|
+
if (at === -1 && patch.import) {
|
|
988
|
+
return insertBindingConflict2(mod.imports, patch);
|
|
989
|
+
}
|
|
990
|
+
return void 0;
|
|
991
|
+
}
|
|
992
|
+
function drizzleColumnRemoveRefusal(source, patch) {
|
|
993
|
+
const mod = parseModule3(source);
|
|
994
|
+
const columns = columnsObject(mod, patch);
|
|
995
|
+
const at = columns ? indexOfColumn(columns, patch.column) : -1;
|
|
996
|
+
if (!columns || at === -1) {
|
|
997
|
+
return void 0;
|
|
998
|
+
}
|
|
999
|
+
return valueDrift(columns, at, patch) ?? (patch.import ? removeBindingConflict2(mod.imports, patch.import) : void 0);
|
|
1000
|
+
}
|
|
1001
|
+
function columnsObject(mod, patch) {
|
|
1002
|
+
const { exports } = mod;
|
|
1003
|
+
const node = exports[patch.exportName]?.$args?.[COLUMNS_ARG]?.$ast;
|
|
1004
|
+
return node?.type === "ObjectExpression" ? node : void 0;
|
|
1005
|
+
}
|
|
1006
|
+
function indexOfColumn(columns, column) {
|
|
1007
|
+
return columns.properties.findIndex(
|
|
1008
|
+
(property) => property.key?.name === column || property.key?.value === column
|
|
1009
|
+
);
|
|
1010
|
+
}
|
|
1011
|
+
function parseExpressionNode(code) {
|
|
1012
|
+
const declaration = parseModule3(`const __drizzleColumn = ${code};`).$ast;
|
|
1013
|
+
const init = declaration.body.length === 1 ? declaration.body[0]?.declarations?.[0]?.init : void 0;
|
|
1014
|
+
if (!init) {
|
|
1015
|
+
throw new TypeError(
|
|
1016
|
+
`drizzle-column value is not a single expression: ${JSON.stringify(code)}`
|
|
1017
|
+
);
|
|
1018
|
+
}
|
|
1019
|
+
return init;
|
|
1020
|
+
}
|
|
1021
|
+
function printed(columns, at) {
|
|
1022
|
+
return generateCode2(columns.properties[at]?.value).code;
|
|
1023
|
+
}
|
|
1024
|
+
function valueDrift(columns, at, patch) {
|
|
1025
|
+
if (collapse(printed(columns, at)) === collapse(patch.value)) {
|
|
1026
|
+
return void 0;
|
|
1027
|
+
}
|
|
1028
|
+
return `${JSON.stringify(patch.column)} on ${JSON.stringify(patch.exportName)} reads ${printed(columns, at)} now, not the ${patch.value} that was applied`;
|
|
1029
|
+
}
|
|
1030
|
+
function collapse(code) {
|
|
1031
|
+
return code.replaceAll(/\s+/gu, "");
|
|
1032
|
+
}
|
|
1033
|
+
function insertBindingConflict2(imports, patch) {
|
|
1034
|
+
const held = patch.import ? foreignBinding(imports, patch.import) : void 0;
|
|
1035
|
+
if (!held || !patch.import) {
|
|
1036
|
+
return void 0;
|
|
1037
|
+
}
|
|
1038
|
+
return `${describeBinding(patch.import.name, held)}, so ${JSON.stringify(patch.column)} would read the wrong builder`;
|
|
1039
|
+
}
|
|
1040
|
+
function removeBindingConflict2(imports, want) {
|
|
1041
|
+
const held = foreignBinding(imports, want);
|
|
1042
|
+
if (!held) {
|
|
1043
|
+
return void 0;
|
|
1044
|
+
}
|
|
1045
|
+
return `${describeBinding(want.name, held)} now, not from ${JSON.stringify(want.from)}, so the column and its import are not the ones that were applied`;
|
|
1046
|
+
}
|
|
1047
|
+
function print(source, mod) {
|
|
1048
|
+
return keepTerminator(
|
|
1049
|
+
source,
|
|
1050
|
+
generateCode2(mod, { format: { objectCurlySpacing: true } }).code
|
|
1051
|
+
);
|
|
1052
|
+
}
|
|
1053
|
+
|
|
866
1054
|
// src/lib/patch/jsonc.ts
|
|
867
1055
|
import {
|
|
868
1056
|
applyEdits,
|
|
@@ -1137,9 +1325,9 @@ function upsertPackageJsonScript(source, patch) {
|
|
|
1137
1325
|
}
|
|
1138
1326
|
|
|
1139
1327
|
// src/lib/patch/ts-module.ts
|
|
1140
|
-
import { builders as builders2, generateCode as
|
|
1328
|
+
import { builders as builders2, generateCode as generateCode3, parseModule as parseModule4 } from "magicast";
|
|
1141
1329
|
function insertIntoPluginArray(source, patch) {
|
|
1142
|
-
const mod =
|
|
1330
|
+
const mod = parseModule4(source);
|
|
1143
1331
|
const exported = mod.exports[patch.exportName];
|
|
1144
1332
|
const callArg = exported?.$args?.[0];
|
|
1145
1333
|
if (!callArg) {
|
|
@@ -1150,7 +1338,7 @@ function insertIntoPluginArray(source, patch) {
|
|
|
1150
1338
|
return source;
|
|
1151
1339
|
}
|
|
1152
1340
|
if (!(patch.import.name in mod.imports)) {
|
|
1153
|
-
mod.imports.$
|
|
1341
|
+
mod.imports.$prepend({
|
|
1154
1342
|
from: patch.import.from,
|
|
1155
1343
|
imported: patch.import.name,
|
|
1156
1344
|
local: patch.import.name
|
|
@@ -1162,10 +1350,10 @@ function insertIntoPluginArray(source, patch) {
|
|
|
1162
1350
|
} else {
|
|
1163
1351
|
callArg[patch.arrayProp] = [newCall];
|
|
1164
1352
|
}
|
|
1165
|
-
return
|
|
1353
|
+
return print2(source, mod);
|
|
1166
1354
|
}
|
|
1167
1355
|
function removeFromPluginArray(source, patch) {
|
|
1168
|
-
const mod =
|
|
1356
|
+
const mod = parseModule4(source);
|
|
1169
1357
|
const exported = mod.exports[patch.exportName];
|
|
1170
1358
|
const callArg = exported?.$args?.[0];
|
|
1171
1359
|
if (!callArg) {
|
|
@@ -1182,7 +1370,7 @@ function removeFromPluginArray(source, patch) {
|
|
|
1182
1370
|
if (argumentDrift(array[index], patch)) {
|
|
1183
1371
|
return source;
|
|
1184
1372
|
}
|
|
1185
|
-
if (
|
|
1373
|
+
if (removeBindingConflict3(mod.imports, patch)) {
|
|
1186
1374
|
return source;
|
|
1187
1375
|
}
|
|
1188
1376
|
array.splice(index, 1);
|
|
@@ -1190,10 +1378,10 @@ function removeFromPluginArray(source, patch) {
|
|
|
1190
1378
|
if (patch.import.name in mod.imports && !isReferenced(program, patch.import.name)) {
|
|
1191
1379
|
delete mod.imports[patch.import.name];
|
|
1192
1380
|
}
|
|
1193
|
-
return
|
|
1381
|
+
return print2(source, mod);
|
|
1194
1382
|
}
|
|
1195
1383
|
function pluginArrayRemoveRefusal(source, patch) {
|
|
1196
|
-
const mod =
|
|
1384
|
+
const mod = parseModule4(source);
|
|
1197
1385
|
const callArg = mod.exports[patch.exportName]?.$args?.[0];
|
|
1198
1386
|
if (!callArg) {
|
|
1199
1387
|
return void 0;
|
|
@@ -1206,12 +1394,12 @@ function pluginArrayRemoveRefusal(source, patch) {
|
|
|
1206
1394
|
if (index === -1) {
|
|
1207
1395
|
return void 0;
|
|
1208
1396
|
}
|
|
1209
|
-
return argumentDrift(array[index], patch) ??
|
|
1397
|
+
return argumentDrift(array[index], patch) ?? removeBindingConflict3(mod.imports, patch);
|
|
1210
1398
|
}
|
|
1211
|
-
function
|
|
1399
|
+
function print2(source, mod) {
|
|
1212
1400
|
return keepTerminator(
|
|
1213
1401
|
source,
|
|
1214
|
-
|
|
1402
|
+
generateCode3(mod, { format: { objectCurlySpacing: true } }).code
|
|
1215
1403
|
);
|
|
1216
1404
|
}
|
|
1217
1405
|
function findCallIndex(array, call) {
|
|
@@ -1229,7 +1417,7 @@ function argumentDrift(element, patch) {
|
|
|
1229
1417
|
}
|
|
1230
1418
|
return `${patch.call}() in ${JSON.stringify(patch.arrayProp)} takes arguments now, not the bare call that was applied`;
|
|
1231
1419
|
}
|
|
1232
|
-
function
|
|
1420
|
+
function removeBindingConflict3(imports, patch) {
|
|
1233
1421
|
const held = foreignBinding(imports, patch.import);
|
|
1234
1422
|
if (!held) {
|
|
1235
1423
|
return void 0;
|
|
@@ -1244,6 +1432,7 @@ function isFunctionCall(value) {
|
|
|
1244
1432
|
var PATCH_KIND_KEYS = {
|
|
1245
1433
|
"chained-route": true,
|
|
1246
1434
|
"const-array": true,
|
|
1435
|
+
"drizzle-column": true,
|
|
1247
1436
|
"package-json-dependency": true,
|
|
1248
1437
|
"package-json-script": true,
|
|
1249
1438
|
"plugin-array": true,
|
|
@@ -1280,6 +1469,9 @@ function applyCodemod(source, patch) {
|
|
|
1280
1469
|
case "const-array": {
|
|
1281
1470
|
return insertIntoConstArray(source, patch);
|
|
1282
1471
|
}
|
|
1472
|
+
case "drizzle-column": {
|
|
1473
|
+
return insertDrizzleColumn(source, patch);
|
|
1474
|
+
}
|
|
1283
1475
|
case "wrangler-binding": {
|
|
1284
1476
|
return upsertWranglerBinding(source, patch);
|
|
1285
1477
|
}
|
|
@@ -1304,6 +1496,7 @@ function applyCodemod(source, patch) {
|
|
|
1304
1496
|
var INVERSES = {
|
|
1305
1497
|
"chained-route": removeChainedRoute,
|
|
1306
1498
|
"const-array": removeFromConstArray,
|
|
1499
|
+
"drizzle-column": removeDrizzleColumn,
|
|
1307
1500
|
"plugin-array": removeFromPluginArray,
|
|
1308
1501
|
"wrangler-binding": removeWranglerBinding
|
|
1309
1502
|
};
|
|
@@ -1327,10 +1520,12 @@ function reversePatch(source, patch, filename) {
|
|
|
1327
1520
|
var REFUSALS = {
|
|
1328
1521
|
"chained-route": chainedRouteInsertRefusal,
|
|
1329
1522
|
"const-array": constArrayInsertRefusal,
|
|
1523
|
+
"drizzle-column": drizzleColumnInsertRefusal,
|
|
1330
1524
|
"package-json-script": packageJsonScriptRefusal
|
|
1331
1525
|
};
|
|
1332
1526
|
var REVERSAL_REFUSALS = {
|
|
1333
1527
|
"chained-route": chainedRouteRemoveRefusal,
|
|
1528
|
+
"drizzle-column": drizzleColumnRemoveRefusal,
|
|
1334
1529
|
"plugin-array": pluginArrayRemoveRefusal,
|
|
1335
1530
|
"wrangler-binding": wranglerBindingRemoveRefusal
|
|
1336
1531
|
};
|
|
@@ -3676,7 +3871,7 @@ import pc3 from "picocolors";
|
|
|
3676
3871
|
import { createHash as createHash2 } from "crypto";
|
|
3677
3872
|
import { mkdtemp as mkdtemp2, readdir as readdir3, readFile as readFile12, rm as rm2 } from "fs/promises";
|
|
3678
3873
|
import { tmpdir as tmpdir2 } from "os";
|
|
3679
|
-
import {
|
|
3874
|
+
import { join as join10, posix as posix6 } from "path";
|
|
3680
3875
|
|
|
3681
3876
|
// src/lib/scaffold.ts
|
|
3682
3877
|
import { mkdir as mkdir5, readdir as readdir2, readFile as readFile11, writeFile as writeFile7 } from "fs/promises";
|
|
@@ -3686,7 +3881,7 @@ import { fileURLToPath as fileURLToPath3 } from "url";
|
|
|
3686
3881
|
// package.json
|
|
3687
3882
|
var package_default = {
|
|
3688
3883
|
name: "saasaloy",
|
|
3689
|
-
version: "0.
|
|
3884
|
+
version: "0.3.0",
|
|
3690
3885
|
description: "Composable SaaS accelerator kit.",
|
|
3691
3886
|
keywords: [
|
|
3692
3887
|
"saas",
|
|
@@ -3737,19 +3932,19 @@ var package_default = {
|
|
|
3737
3932
|
},
|
|
3738
3933
|
devDependencies: {
|
|
3739
3934
|
"@release-it/conventional-changelog": "12.0.0",
|
|
3740
|
-
"@types/node": "26.
|
|
3741
|
-
"@vitest/coverage-v8": "
|
|
3935
|
+
"@types/node": "26.4.1",
|
|
3936
|
+
"@vitest/coverage-v8": "5.0.0",
|
|
3742
3937
|
"release-it": "21.0.2",
|
|
3743
3938
|
tsup: "8.5.1",
|
|
3744
|
-
vitest: "
|
|
3939
|
+
vitest: "5.0.0"
|
|
3745
3940
|
},
|
|
3746
3941
|
dependencies: {
|
|
3747
3942
|
"@clack/prompts": "1.7.0",
|
|
3748
|
-
ajv: "8.
|
|
3943
|
+
ajv: "8.20.0",
|
|
3749
3944
|
diff: "9.0.0",
|
|
3750
|
-
giget: "3.3.
|
|
3945
|
+
giget: "3.3.1",
|
|
3751
3946
|
"jsonc-parser": "3.3.1",
|
|
3752
|
-
magicast: "0.5.
|
|
3947
|
+
magicast: "0.5.4",
|
|
3753
3948
|
picocolors: "1.1.1"
|
|
3754
3949
|
}
|
|
3755
3950
|
};
|
|
@@ -3826,10 +4021,20 @@ var BASE_MODULE = "base";
|
|
|
3826
4021
|
var DEFAULT_BASE_NAME = "web";
|
|
3827
4022
|
var BASE_NO_MERGE_BASE = "the template ships with the CLI; the previous version isn't on disk";
|
|
3828
4023
|
var BASE_INTENT = "the base template changed; keep local edits, take the upstream change";
|
|
4024
|
+
function compileGlob(pattern) {
|
|
4025
|
+
const source = pattern.split("/").map(
|
|
4026
|
+
(segment) => segment === "**" ? "[^\0]*" : segment.replaceAll(/[.+^${}()|[\]\\]/g, String.raw`\$&`).replaceAll("*", "[^/]*")
|
|
4027
|
+
).join("/").replaceAll("[^\0]*/", "(?:[^\0]*/)?");
|
|
4028
|
+
return new RegExp(`^${source}$`);
|
|
4029
|
+
}
|
|
4030
|
+
function ownedMatcher(declaration) {
|
|
4031
|
+
const patterns = declaration.ownedFiles.map(compileGlob);
|
|
4032
|
+
return (target) => patterns.some((re) => re.test(target));
|
|
4033
|
+
}
|
|
3829
4034
|
async function readBaseDeclaration(templateDir) {
|
|
3830
4035
|
const raw = await readIfPresent(join10(templateDir, BASE_DECLARATION));
|
|
3831
4036
|
if (raw === void 0) {
|
|
3832
|
-
return { seedFiles: [] };
|
|
4037
|
+
return { seedFiles: [], ownedFiles: [] };
|
|
3833
4038
|
}
|
|
3834
4039
|
const parsed = JSON.parse(raw);
|
|
3835
4040
|
const seedFiles = parsed.seedFiles;
|
|
@@ -3838,7 +4043,16 @@ async function readBaseDeclaration(templateDir) {
|
|
|
3838
4043
|
`${BASE_DECLARATION} must declare "seedFiles" as a list of project-relative paths.`
|
|
3839
4044
|
);
|
|
3840
4045
|
}
|
|
3841
|
-
|
|
4046
|
+
const ownedFiles = parsed.ownedFiles ?? [];
|
|
4047
|
+
if (!Array.isArray(ownedFiles) || !ownedFiles.every((entry) => typeof entry === "string" && entry !== "")) {
|
|
4048
|
+
throw new Error(
|
|
4049
|
+
`${BASE_DECLARATION} must declare "ownedFiles" as a list of project-relative globs.`
|
|
4050
|
+
);
|
|
4051
|
+
}
|
|
4052
|
+
return {
|
|
4053
|
+
seedFiles,
|
|
4054
|
+
ownedFiles
|
|
4055
|
+
};
|
|
3842
4056
|
}
|
|
3843
4057
|
async function listTemplateFiles(templateDir) {
|
|
3844
4058
|
const out = [];
|
|
@@ -3900,6 +4114,7 @@ function recordBaseFiles(manifest, files) {
|
|
|
3900
4114
|
manifest.managed[file.target] = {
|
|
3901
4115
|
module: BASE_MODULE,
|
|
3902
4116
|
hash: file.hash,
|
|
4117
|
+
...file.adopted ? { adopted: true } : {},
|
|
3903
4118
|
from: file.from
|
|
3904
4119
|
};
|
|
3905
4120
|
}
|
|
@@ -3920,13 +4135,15 @@ async function missingBaseTargets(root, manifest, templateDir) {
|
|
|
3920
4135
|
}
|
|
3921
4136
|
return missing.toSorted((a, b) => a < b ? -1 : 1);
|
|
3922
4137
|
}
|
|
3923
|
-
async function renderTemplate(root, templateDir) {
|
|
4138
|
+
async function renderTemplate(root, templateDir, projectName) {
|
|
3924
4139
|
const dir = await mkdtemp2(join10(tmpdir2(), "saasaloy-base-render-"));
|
|
3925
4140
|
const files = await copyTemplate(
|
|
3926
4141
|
templateDir,
|
|
3927
4142
|
dir,
|
|
3928
|
-
templateVars(
|
|
3929
|
-
{
|
|
4143
|
+
templateVars(projectName),
|
|
4144
|
+
{
|
|
4145
|
+
keepNames: true
|
|
4146
|
+
}
|
|
3930
4147
|
);
|
|
3931
4148
|
return {
|
|
3932
4149
|
dir,
|
|
@@ -3935,9 +4152,16 @@ async function renderTemplate(root, templateDir) {
|
|
|
3935
4152
|
};
|
|
3936
4153
|
}
|
|
3937
4154
|
async function adoptBase(args) {
|
|
3938
|
-
const {
|
|
4155
|
+
const {
|
|
4156
|
+
root,
|
|
4157
|
+
cliVersion,
|
|
4158
|
+
projectName,
|
|
4159
|
+
manifest,
|
|
4160
|
+
lock,
|
|
4161
|
+
dryRun = false
|
|
4162
|
+
} = args;
|
|
3939
4163
|
const templateDir = args.templateDir ?? await baseTemplateDir();
|
|
3940
|
-
const render = await renderTemplate(root, templateDir);
|
|
4164
|
+
const render = await renderTemplate(root, templateDir, projectName);
|
|
3941
4165
|
try {
|
|
3942
4166
|
const files = [];
|
|
3943
4167
|
const adopted = [];
|
|
@@ -3952,7 +4176,8 @@ async function adoptBase(args) {
|
|
|
3952
4176
|
files.push({
|
|
3953
4177
|
target: file.target,
|
|
3954
4178
|
from: file.from,
|
|
3955
|
-
hash: hashContent(mine)
|
|
4179
|
+
hash: hashContent(mine),
|
|
4180
|
+
adopted: true
|
|
3956
4181
|
});
|
|
3957
4182
|
}
|
|
3958
4183
|
}
|
|
@@ -3973,9 +4198,18 @@ async function adoptBase(args) {
|
|
|
3973
4198
|
}
|
|
3974
4199
|
}
|
|
3975
4200
|
async function baseUpdateInput(args) {
|
|
3976
|
-
const {
|
|
3977
|
-
|
|
3978
|
-
|
|
4201
|
+
const {
|
|
4202
|
+
root,
|
|
4203
|
+
manifest,
|
|
4204
|
+
lock,
|
|
4205
|
+
templateDir,
|
|
4206
|
+
cliVersion,
|
|
4207
|
+
projectName,
|
|
4208
|
+
comparison
|
|
4209
|
+
} = args;
|
|
4210
|
+
const render = await renderTemplate(root, templateDir, projectName);
|
|
4211
|
+
const declaration = await readBaseDeclaration(templateDir);
|
|
4212
|
+
const seed = new Set(declaration.seedFiles);
|
|
3979
4213
|
const shipped = render.files.filter((file) => !seed.has(file.target));
|
|
3980
4214
|
const targets = new Set(shipped.map((file) => file.target));
|
|
3981
4215
|
return {
|
|
@@ -4000,6 +4234,7 @@ async function baseUpdateInput(args) {
|
|
|
4000
4234
|
noMergeBase: BASE_NO_MERGE_BASE,
|
|
4001
4235
|
intent: [],
|
|
4002
4236
|
ignoreTargets: seed,
|
|
4237
|
+
isOwned: ownedMatcher(declaration),
|
|
4003
4238
|
reapplyPatches: manifest.patches.filter(
|
|
4004
4239
|
(patch) => targets.has(patch.file)
|
|
4005
4240
|
),
|
|
@@ -4014,8 +4249,10 @@ async function baseUpdateInput(args) {
|
|
|
4014
4249
|
}
|
|
4015
4250
|
|
|
4016
4251
|
// src/lib/doctor.ts
|
|
4252
|
+
import { findNodeAtLocation as findNodeAtLocation4, getNodeValue as getNodeValue4, parseTree as parseTree4 } from "jsonc-parser";
|
|
4253
|
+
import { parseModule as parseModule5 } from "magicast";
|
|
4017
4254
|
import { readFile as readFile13 } from "fs/promises";
|
|
4018
|
-
import { basename
|
|
4255
|
+
import { basename, dirname as dirname6, join as join11, posix as posix7, resolve as resolve3 } from "path";
|
|
4019
4256
|
var SKILL_PREFIX = "saasaloy-";
|
|
4020
4257
|
var PINNED_DEP = /^(@[^/@]+\/)?[^/@]+@\d+\.\d+\.\d+(-[\w.-]+)?(\+[\w.-]+)?$/;
|
|
4021
4258
|
function finding(module, where, message) {
|
|
@@ -4069,7 +4306,7 @@ async function readDescriptor(dir) {
|
|
|
4069
4306
|
}
|
|
4070
4307
|
async function checkModule(input) {
|
|
4071
4308
|
const { aliases, dir, siblings } = input;
|
|
4072
|
-
const module =
|
|
4309
|
+
const module = basename(dir);
|
|
4073
4310
|
const findings = [];
|
|
4074
4311
|
const report = () => ({ dir, findings, module });
|
|
4075
4312
|
const file = join11(dir, "registry-item.json");
|
|
@@ -4270,10 +4507,204 @@ function checkProject(args) {
|
|
|
4270
4507
|
)
|
|
4271
4508
|
);
|
|
4272
4509
|
}
|
|
4510
|
+
var KV_INDEX_FILE = "packages/kv/src/index.ts";
|
|
4511
|
+
var WRANGLER_FILE = "apps/api/wrangler.jsonc";
|
|
4512
|
+
var CLOUDFLARE_KV_MODULE = "kv-cloudflare";
|
|
4513
|
+
function checkPolicyBindings(args) {
|
|
4514
|
+
const bound = new Set(rateLimitBindingNames(args.wrangler));
|
|
4515
|
+
const findings = [];
|
|
4516
|
+
for (const entry of registeredPolicies(args)) {
|
|
4517
|
+
if (entry.name === void 0) {
|
|
4518
|
+
findings.push(
|
|
4519
|
+
finding(
|
|
4520
|
+
"ratelimit",
|
|
4521
|
+
`/policies/${String(entry.index)}`,
|
|
4522
|
+
`the policy at index ${String(entry.index)} of the \`policies\` array in ${KV_INDEX_FILE} has no name this check can read, so its RL_<NAME> binding in ${WRANGLER_FILE} was not checked. Write it as \`definePolicy({ name: "..." })\`, or as a factory exported from a file under packages/kv/src/policies/.`
|
|
4523
|
+
)
|
|
4524
|
+
);
|
|
4525
|
+
continue;
|
|
4526
|
+
}
|
|
4527
|
+
if (!bound.has(bindingFor(entry.name))) {
|
|
4528
|
+
findings.push(
|
|
4529
|
+
finding(
|
|
4530
|
+
"ratelimit",
|
|
4531
|
+
`/policies/${entry.name}`,
|
|
4532
|
+
`policy "${entry.name}" has no ${bindingFor(entry.name)} entry in ${WRANGLER_FILE} \u2014 consume({ policy: "${entry.name}" }) throws not_supported on the first request. Add the binding, or drop the policy from ${KV_INDEX_FILE}.`
|
|
4533
|
+
)
|
|
4534
|
+
);
|
|
4535
|
+
}
|
|
4536
|
+
}
|
|
4537
|
+
return findings;
|
|
4538
|
+
}
|
|
4539
|
+
function bindingFor(policy) {
|
|
4540
|
+
return `RL_${policy.toUpperCase()}`;
|
|
4541
|
+
}
|
|
4542
|
+
function rateLimitBindingNames(wrangler) {
|
|
4543
|
+
const root = parseTree4(wrangler);
|
|
4544
|
+
if (!root) {
|
|
4545
|
+
return [];
|
|
4546
|
+
}
|
|
4547
|
+
const node = findNodeAtLocation4(root, ["ratelimits"]);
|
|
4548
|
+
if (node?.type !== "array") {
|
|
4549
|
+
return [];
|
|
4550
|
+
}
|
|
4551
|
+
const names = [];
|
|
4552
|
+
for (const child of node.children ?? []) {
|
|
4553
|
+
const value = getNodeValue4(child);
|
|
4554
|
+
const name = asRecord(value).name;
|
|
4555
|
+
if (typeof name === "string") {
|
|
4556
|
+
names.push(name);
|
|
4557
|
+
}
|
|
4558
|
+
}
|
|
4559
|
+
return names;
|
|
4560
|
+
}
|
|
4561
|
+
function registeredPolicies(args) {
|
|
4562
|
+
let mod;
|
|
4563
|
+
try {
|
|
4564
|
+
mod = parseModule5(args.index);
|
|
4565
|
+
} catch {
|
|
4566
|
+
return [];
|
|
4567
|
+
}
|
|
4568
|
+
const callArg = mod.exports.kv?.$args?.[0];
|
|
4569
|
+
const array = callArg?.policies;
|
|
4570
|
+
if (!Array.isArray(array)) {
|
|
4571
|
+
return [];
|
|
4572
|
+
}
|
|
4573
|
+
const imports = mod.imports;
|
|
4574
|
+
const policies = [];
|
|
4575
|
+
for (let i = 0; i < array.length; i++) {
|
|
4576
|
+
const element = array[i];
|
|
4577
|
+
policies.push({ index: i, name: policyNameOf(element, args, imports) });
|
|
4578
|
+
}
|
|
4579
|
+
return policies;
|
|
4580
|
+
}
|
|
4581
|
+
function policyNameOf(element, args, imports) {
|
|
4582
|
+
const callee = calleeName(element);
|
|
4583
|
+
if (callee === void 0) {
|
|
4584
|
+
return void 0;
|
|
4585
|
+
}
|
|
4586
|
+
const inline = inlineNameArgument(element);
|
|
4587
|
+
if (inline !== void 0) {
|
|
4588
|
+
return inline;
|
|
4589
|
+
}
|
|
4590
|
+
const from = imports[callee]?.from;
|
|
4591
|
+
const source = typeof from === "string" ? args.policySources[from] : void 0;
|
|
4592
|
+
return source === void 0 ? void 0 : policyNameIn(source, callee);
|
|
4593
|
+
}
|
|
4594
|
+
function inlineNameArgument(element) {
|
|
4595
|
+
const args = element.$args;
|
|
4596
|
+
if (!Array.isArray(args)) {
|
|
4597
|
+
return void 0;
|
|
4598
|
+
}
|
|
4599
|
+
const name = asRecord(args[0]).name;
|
|
4600
|
+
return typeof name === "string" ? name : void 0;
|
|
4601
|
+
}
|
|
4602
|
+
function calleeName(element) {
|
|
4603
|
+
if (typeof element !== "object" || element === null) {
|
|
4604
|
+
return void 0;
|
|
4605
|
+
}
|
|
4606
|
+
const record = element;
|
|
4607
|
+
return record.$type === "function-call" && typeof record.$callee === "string" ? record.$callee : void 0;
|
|
4608
|
+
}
|
|
4609
|
+
function policyNameIn(source, callee) {
|
|
4610
|
+
let program;
|
|
4611
|
+
try {
|
|
4612
|
+
program = parseModule5(source).$ast;
|
|
4613
|
+
} catch {
|
|
4614
|
+
return void 0;
|
|
4615
|
+
}
|
|
4616
|
+
const factory = findNode(
|
|
4617
|
+
program,
|
|
4618
|
+
(node) => (node.type === "FunctionDeclaration" || node.type === "VariableDeclarator") && asRecord(node.id).name === callee
|
|
4619
|
+
);
|
|
4620
|
+
if (!factory) {
|
|
4621
|
+
return void 0;
|
|
4622
|
+
}
|
|
4623
|
+
const call = findNode(
|
|
4624
|
+
factory,
|
|
4625
|
+
(node) => node.type === "CallExpression" && asRecord(node.callee).name === "definePolicy"
|
|
4626
|
+
);
|
|
4627
|
+
const argument = asArray(call?.arguments)[0];
|
|
4628
|
+
const property = asArray(asRecord(argument).properties).find(
|
|
4629
|
+
(entry) => asRecord(asRecord(entry).key).name === "name"
|
|
4630
|
+
);
|
|
4631
|
+
const value = asRecord(asRecord(property).value).value;
|
|
4632
|
+
return typeof value === "string" ? value : void 0;
|
|
4633
|
+
}
|
|
4634
|
+
function findNode(root, accept) {
|
|
4635
|
+
if (Array.isArray(root)) {
|
|
4636
|
+
for (const child of root) {
|
|
4637
|
+
const found = findNode(child, accept);
|
|
4638
|
+
if (found) {
|
|
4639
|
+
return found;
|
|
4640
|
+
}
|
|
4641
|
+
}
|
|
4642
|
+
return void 0;
|
|
4643
|
+
}
|
|
4644
|
+
if (typeof root !== "object" || root === null) {
|
|
4645
|
+
return void 0;
|
|
4646
|
+
}
|
|
4647
|
+
const node = root;
|
|
4648
|
+
if (typeof node.type === "string") {
|
|
4649
|
+
const typed = node;
|
|
4650
|
+
if (accept(typed)) {
|
|
4651
|
+
return typed;
|
|
4652
|
+
}
|
|
4653
|
+
}
|
|
4654
|
+
for (const [key, value] of Object.entries(node)) {
|
|
4655
|
+
if (key === "loc" || key === "comments" || key === "original") {
|
|
4656
|
+
continue;
|
|
4657
|
+
}
|
|
4658
|
+
const found = findNode(value, accept);
|
|
4659
|
+
if (found) {
|
|
4660
|
+
return found;
|
|
4661
|
+
}
|
|
4662
|
+
}
|
|
4663
|
+
return void 0;
|
|
4664
|
+
}
|
|
4665
|
+
async function readPolicyState(root, installed) {
|
|
4666
|
+
if (!installed.includes(CLOUDFLARE_KV_MODULE)) {
|
|
4667
|
+
return void 0;
|
|
4668
|
+
}
|
|
4669
|
+
const index = await readIfPresent(resolveWithinRoot(root, KV_INDEX_FILE));
|
|
4670
|
+
const wrangler = await readIfPresent(resolveWithinRoot(root, WRANGLER_FILE));
|
|
4671
|
+
if (index === void 0 || wrangler === void 0) {
|
|
4672
|
+
return void 0;
|
|
4673
|
+
}
|
|
4674
|
+
const policySources = {};
|
|
4675
|
+
for (const specifier of relativeImportsIn(index)) {
|
|
4676
|
+
const source = await readIfPresent(
|
|
4677
|
+
resolveWithinRoot(
|
|
4678
|
+
root,
|
|
4679
|
+
posix7.join(posix7.dirname(KV_INDEX_FILE), `${specifier}.ts`)
|
|
4680
|
+
)
|
|
4681
|
+
);
|
|
4682
|
+
if (source !== void 0) {
|
|
4683
|
+
policySources[specifier] = source;
|
|
4684
|
+
}
|
|
4685
|
+
}
|
|
4686
|
+
return { index, policySources, wrangler };
|
|
4687
|
+
}
|
|
4688
|
+
function relativeImportsIn(index) {
|
|
4689
|
+
let mod;
|
|
4690
|
+
try {
|
|
4691
|
+
mod = parseModule5(index);
|
|
4692
|
+
} catch {
|
|
4693
|
+
return [];
|
|
4694
|
+
}
|
|
4695
|
+
const imports = mod.imports;
|
|
4696
|
+
const specifiers = /* @__PURE__ */ new Set();
|
|
4697
|
+
for (const held of Object.values(imports)) {
|
|
4698
|
+
if (typeof held?.from === "string" && held.from.startsWith(".")) {
|
|
4699
|
+
specifiers.add(held.from);
|
|
4700
|
+
}
|
|
4701
|
+
}
|
|
4702
|
+
return [...specifiers];
|
|
4703
|
+
}
|
|
4273
4704
|
async function resolveDoctorTarget(path) {
|
|
4274
4705
|
const dir = resolve3(path);
|
|
4275
4706
|
if (await pathExists(join11(dir, "registry-item.json"))) {
|
|
4276
|
-
return { names: [
|
|
4707
|
+
return { names: [basename(dir)], registryDir: dirname6(dir) };
|
|
4277
4708
|
}
|
|
4278
4709
|
return { names: await registryModuleNames(dir), registryDir: dir };
|
|
4279
4710
|
}
|
|
@@ -4415,9 +4846,11 @@ async function reportProject(path) {
|
|
|
4415
4846
|
seedFiles: (await readBaseDeclaration(await baseTemplateDir())).seedFiles
|
|
4416
4847
|
});
|
|
4417
4848
|
note2(wrapForNote(renderBase(base)), "Base");
|
|
4849
|
+
const policyState = await readPolicyState(path, config.installed);
|
|
4418
4850
|
const findings = [
|
|
4419
4851
|
...checkProject({ config, manifest }),
|
|
4420
|
-
...checkPartialInstalls({ installed: config.installed, manifest })
|
|
4852
|
+
...checkPartialInstalls({ installed: config.installed, manifest }),
|
|
4853
|
+
...policyState ? checkPolicyBindings(policyState) : []
|
|
4421
4854
|
];
|
|
4422
4855
|
if (findings.length === 0) {
|
|
4423
4856
|
const count = config.installed.length;
|
|
@@ -5122,8 +5555,8 @@ ${entry.declaration.description}`,
|
|
|
5122
5555
|
|
|
5123
5556
|
// src/commands/init.ts
|
|
5124
5557
|
import { spawn } from "child_process";
|
|
5125
|
-
import { readdir as readdir4 } from "fs/promises";
|
|
5126
|
-
import { basename as
|
|
5558
|
+
import { readdir as readdir4, readFile as readFile16 } from "fs/promises";
|
|
5559
|
+
import { basename as basename2, join as join14, resolve as resolve6 } from "path";
|
|
5127
5560
|
import {
|
|
5128
5561
|
cancel as cancel4,
|
|
5129
5562
|
intro as intro4,
|
|
@@ -5315,7 +5748,7 @@ async function runInit(argv) {
|
|
|
5315
5748
|
if (!trimmed) {
|
|
5316
5749
|
return "Enter a project name (or `.` for the current directory).";
|
|
5317
5750
|
}
|
|
5318
|
-
const name =
|
|
5751
|
+
const name = basename2(resolve6(process.cwd(), trimmed));
|
|
5319
5752
|
if (!NAME_PATTERN.test(name)) {
|
|
5320
5753
|
return "Use lowercase letters, digits, and hyphens (e.g. my-app).";
|
|
5321
5754
|
}
|
|
@@ -5329,7 +5762,7 @@ async function runInit(argv) {
|
|
|
5329
5762
|
nameArg = answer.trim();
|
|
5330
5763
|
}
|
|
5331
5764
|
const target = resolve6(process.cwd(), nameArg);
|
|
5332
|
-
const projectName =
|
|
5765
|
+
const projectName = basename2(target);
|
|
5333
5766
|
if (!NAME_PATTERN.test(projectName)) {
|
|
5334
5767
|
cancel4(
|
|
5335
5768
|
`Invalid project name "${projectName}". Use lowercase letters, digits, and hyphens (e.g. my-app).`
|
|
@@ -5356,6 +5789,17 @@ async function runInit(argv) {
|
|
|
5356
5789
|
s.stop(
|
|
5357
5790
|
`Scaffolded ${pc6.cyan(projectName)} ${pc6.dim("(apps/web \xB7 packages/ui \xB7 packages/tsconfig)")}`
|
|
5358
5791
|
);
|
|
5792
|
+
try {
|
|
5793
|
+
const config = await loadConfig(target);
|
|
5794
|
+
await saveConfig(target, { ...config, name: projectName });
|
|
5795
|
+
const entry = written.find((file) => file.target === CONFIG_FILE);
|
|
5796
|
+
if (entry) {
|
|
5797
|
+
entry.hash = hashContent(await readFile16(entry.path, "utf-8"));
|
|
5798
|
+
}
|
|
5799
|
+
} catch (error) {
|
|
5800
|
+
cancel4(`Couldn't record the project name: ${errorMessage(error)}`);
|
|
5801
|
+
return EXIT_FAILURE;
|
|
5802
|
+
}
|
|
5359
5803
|
try {
|
|
5360
5804
|
const manifest = await loadManifest(target);
|
|
5361
5805
|
const lock = await loadLock(target);
|
|
@@ -5919,11 +6363,11 @@ import { cancel as cancel7, intro as intro7, log as log5, note as note7, outro a
|
|
|
5919
6363
|
import pc9 from "picocolors";
|
|
5920
6364
|
|
|
5921
6365
|
// src/lib/updater.ts
|
|
5922
|
-
import { mkdir as mkdir8, readFile as
|
|
6366
|
+
import { mkdir as mkdir8, readFile as readFile18, rm as rmPath2, writeFile as writeFile11 } from "fs/promises";
|
|
5923
6367
|
import { dirname as dirname11, posix as posix9 } from "path";
|
|
5924
6368
|
|
|
5925
6369
|
// src/lib/remover.ts
|
|
5926
|
-
import { readdir as readdir5, readFile as
|
|
6370
|
+
import { readdir as readdir5, readFile as readFile17, rm as rmPath, writeFile as writeFile10 } from "fs/promises";
|
|
5927
6371
|
import { dirname as dirname10, join as join18, relative as relative5, sep as sep3 } from "path";
|
|
5928
6372
|
function classifyTrackedFile(content, manifestHash) {
|
|
5929
6373
|
if (content === void 0) {
|
|
@@ -5948,7 +6392,7 @@ async function buildRemovePlan(args) {
|
|
|
5948
6392
|
continue;
|
|
5949
6393
|
}
|
|
5950
6394
|
const targetAbs = resolveWithinRoot(root, target);
|
|
5951
|
-
const oldContent = await pathExists(targetAbs) ? await
|
|
6395
|
+
const oldContent = await pathExists(targetAbs) ? await readFile17(targetAbs, "utf-8") : void 0;
|
|
5952
6396
|
files.push({
|
|
5953
6397
|
module: name,
|
|
5954
6398
|
target,
|
|
@@ -6015,7 +6459,7 @@ async function previewPatchRemoval(root, entry) {
|
|
|
6015
6459
|
return { entry, action: "gone", diff: "" };
|
|
6016
6460
|
}
|
|
6017
6461
|
await assertNoSymlinkPath(root, fileAbs);
|
|
6018
|
-
const source = await
|
|
6462
|
+
const source = await readFile17(fileAbs, "utf-8");
|
|
6019
6463
|
const attempt = tryReversePatch(source, entry);
|
|
6020
6464
|
if (!attempt.ok) {
|
|
6021
6465
|
return { entry, action: "refused", diff: "", reason: attempt.reason };
|
|
@@ -6083,7 +6527,7 @@ async function recheckFile(file) {
|
|
|
6083
6527
|
if (file.oldContent === void 0) {
|
|
6084
6528
|
return "changed";
|
|
6085
6529
|
}
|
|
6086
|
-
const current = await
|
|
6530
|
+
const current = await readFile17(file.targetAbs, "utf-8");
|
|
6087
6531
|
return hashContent(current) === hashContent(file.oldContent) ? "unchanged" : "changed";
|
|
6088
6532
|
}
|
|
6089
6533
|
async function executeRemovePlan(plan, args) {
|
|
@@ -6135,7 +6579,7 @@ async function executeRemovePlan(plan, args) {
|
|
|
6135
6579
|
let reversed = false;
|
|
6136
6580
|
if (await pathExists(fileAbs)) {
|
|
6137
6581
|
await assertNoSymlinkPath(root, fileAbs);
|
|
6138
|
-
const source = await
|
|
6582
|
+
const source = await readFile17(fileAbs, "utf-8");
|
|
6139
6583
|
const attempt = tryReversePatch(source, entry);
|
|
6140
6584
|
if (!attempt.ok) {
|
|
6141
6585
|
patchRefusals.push({ patch: entry, reason: attempt.reason });
|
|
@@ -6418,18 +6862,21 @@ async function planOneModule(args) {
|
|
|
6418
6862
|
const baseFiles = base ? await listModuleFiles(base, aliasView, installedSet) : void 0;
|
|
6419
6863
|
const files = [];
|
|
6420
6864
|
for (const ref of theirsFiles.values()) {
|
|
6421
|
-
const theirsContent = await
|
|
6865
|
+
const theirsContent = await readFile18(ref.abs, "utf-8");
|
|
6422
6866
|
const baseRef = baseFiles?.get(ref.target);
|
|
6423
|
-
const baseContent = baseRef ? await
|
|
6867
|
+
const baseContent = baseRef ? await readFile18(baseRef.abs, "utf-8") : void 0;
|
|
6424
6868
|
const targetAbs = resolveWithinRoot(root, ref.target);
|
|
6425
6869
|
const managed = manifest.managed[ref.target];
|
|
6426
6870
|
const owned = managed?.module === name ? managed : void 0;
|
|
6427
6871
|
const mine = await readIfPresent(targetAbs);
|
|
6872
|
+
const projectOwned = input.isOwned?.(ref.target) ?? false;
|
|
6428
6873
|
const action = classifyUpdate(
|
|
6429
6874
|
baseContent,
|
|
6430
6875
|
theirsContent,
|
|
6431
6876
|
mine,
|
|
6432
|
-
owned?.hash
|
|
6877
|
+
owned?.hash,
|
|
6878
|
+
owned?.adopted ?? false,
|
|
6879
|
+
projectOwned
|
|
6433
6880
|
);
|
|
6434
6881
|
const patchedBy = patchersOf(manifest, ref.target);
|
|
6435
6882
|
files.push({
|
|
@@ -6438,6 +6885,8 @@ async function planOneModule(args) {
|
|
|
6438
6885
|
target: ref.target,
|
|
6439
6886
|
targetAbs,
|
|
6440
6887
|
action,
|
|
6888
|
+
...projectOwned ? { projectOwned: true } : {},
|
|
6889
|
+
...owned?.adopted ? { adopted: true } : {},
|
|
6441
6890
|
base: baseContent,
|
|
6442
6891
|
theirs: theirsContent,
|
|
6443
6892
|
mine,
|
|
@@ -6453,13 +6902,14 @@ async function planOneModule(args) {
|
|
|
6453
6902
|
const targetAbs = resolveWithinRoot(root, target);
|
|
6454
6903
|
const mine = await readIfPresent(targetAbs);
|
|
6455
6904
|
const baseRef = baseFiles?.get(target);
|
|
6905
|
+
const keep = mine !== void 0 && ((entry.adopted ?? false) || (input.isOwned?.(target) ?? false));
|
|
6456
6906
|
removals.push({
|
|
6457
6907
|
module: name,
|
|
6458
6908
|
from: entry.from ?? baseRef?.from ?? target,
|
|
6459
6909
|
target,
|
|
6460
6910
|
targetAbs,
|
|
6461
|
-
action: DELETE_ACTION[classifyTrackedFile(mine, entry.hash)],
|
|
6462
|
-
base: baseRef ? await
|
|
6911
|
+
action: keep ? "delete-drift" : DELETE_ACTION[classifyTrackedFile(mine, entry.hash)],
|
|
6912
|
+
base: baseRef ? await readFile18(baseRef.abs, "utf-8") : void 0,
|
|
6463
6913
|
mine
|
|
6464
6914
|
});
|
|
6465
6915
|
}
|
|
@@ -6580,7 +7030,7 @@ function newEnvVars(base, theirs) {
|
|
|
6580
7030
|
}
|
|
6581
7031
|
return out;
|
|
6582
7032
|
}
|
|
6583
|
-
function classifyUpdate(base, theirs, mine, managedHash) {
|
|
7033
|
+
function classifyUpdate(base, theirs, mine, managedHash, adopted, projectOwned) {
|
|
6584
7034
|
if (base !== void 0 && base === theirs) {
|
|
6585
7035
|
return "skip";
|
|
6586
7036
|
}
|
|
@@ -6593,6 +7043,9 @@ function classifyUpdate(base, theirs, mine, managedHash) {
|
|
|
6593
7043
|
if (managedHash === void 0) {
|
|
6594
7044
|
return "conflict";
|
|
6595
7045
|
}
|
|
7046
|
+
if (adopted || projectOwned) {
|
|
7047
|
+
return "drift";
|
|
7048
|
+
}
|
|
6596
7049
|
return hashContent(mine) === managedHash ? "overwrite" : "drift";
|
|
6597
7050
|
}
|
|
6598
7051
|
async function planLinks(root, theirs) {
|
|
@@ -6748,7 +7201,7 @@ async function executeUpdatePlan(plan, args) {
|
|
|
6748
7201
|
clean = false;
|
|
6749
7202
|
continue;
|
|
6750
7203
|
}
|
|
6751
|
-
const source = await
|
|
7204
|
+
const source = await readFile18(p.fileAbs, "utf-8");
|
|
6752
7205
|
const { content, changed } = applyPatch(source, p.patch, p.file);
|
|
6753
7206
|
if (!changed) {
|
|
6754
7207
|
continue;
|
|
@@ -7372,8 +7825,8 @@ ${pc10.dim("Hand-edited since \u2014 left on disk, now untracked.")}`
|
|
|
7372
7825
|
}
|
|
7373
7826
|
|
|
7374
7827
|
// src/commands/update.ts
|
|
7375
|
-
import { mkdir as
|
|
7376
|
-
import { basename as basename4, dirname as
|
|
7828
|
+
import { mkdir as mkdir10, realpath, writeFile as writeFile13 } from "fs/promises";
|
|
7829
|
+
import { basename as basename4, dirname as dirname13, isAbsolute as isAbsolute4, join as join20, resolve as resolve8 } from "path";
|
|
7377
7830
|
import {
|
|
7378
7831
|
cancel as cancel9,
|
|
7379
7832
|
confirm as confirm3,
|
|
@@ -7385,6 +7838,145 @@ import {
|
|
|
7385
7838
|
} from "@clack/prompts";
|
|
7386
7839
|
import pc11 from "picocolors";
|
|
7387
7840
|
|
|
7841
|
+
// src/lib/backup.ts
|
|
7842
|
+
import {
|
|
7843
|
+
copyFile,
|
|
7844
|
+
mkdir as mkdir9,
|
|
7845
|
+
readdir as readdir6,
|
|
7846
|
+
readFile as readFile19,
|
|
7847
|
+
rm as rmPath3,
|
|
7848
|
+
writeFile as writeFile12
|
|
7849
|
+
} from "fs/promises";
|
|
7850
|
+
import { dirname as dirname12, join as join19 } from "path";
|
|
7851
|
+
var BACKUP_DIR = join19(".saasaloy", "backups");
|
|
7852
|
+
var RECORD_FILE = "backup.json";
|
|
7853
|
+
var STATE_FILES = [
|
|
7854
|
+
"saasaloy.json",
|
|
7855
|
+
"saasaloy-lock.json",
|
|
7856
|
+
"package.json",
|
|
7857
|
+
join19(".saasaloy", "manifest.json")
|
|
7858
|
+
];
|
|
7859
|
+
function stampName(now) {
|
|
7860
|
+
return now.toISOString().replaceAll(/[:.]/g, "-");
|
|
7861
|
+
}
|
|
7862
|
+
async function writeBackup(args) {
|
|
7863
|
+
const { root, targets, cliVersion } = args;
|
|
7864
|
+
const createdAt = (args.now ?? /* @__PURE__ */ new Date()).toISOString();
|
|
7865
|
+
const rel = join19(BACKUP_DIR, stampName(args.now ?? new Date(createdAt)));
|
|
7866
|
+
const dir = join19(root, rel);
|
|
7867
|
+
const files = [];
|
|
7868
|
+
const absent = [];
|
|
7869
|
+
for (const target of /* @__PURE__ */ new Set([...targets, ...STATE_FILES])) {
|
|
7870
|
+
const source = resolveWithinRoot(root, target);
|
|
7871
|
+
if (!await pathExists(source)) {
|
|
7872
|
+
absent.push(target);
|
|
7873
|
+
continue;
|
|
7874
|
+
}
|
|
7875
|
+
const destination = join19(dir, target);
|
|
7876
|
+
await mkdir9(dirname12(destination), { recursive: true });
|
|
7877
|
+
await copyFile(source, destination);
|
|
7878
|
+
files.push(target);
|
|
7879
|
+
}
|
|
7880
|
+
const record = {
|
|
7881
|
+
createdAt,
|
|
7882
|
+
cliVersion,
|
|
7883
|
+
files: files.toSorted(),
|
|
7884
|
+
absent: absent.toSorted()
|
|
7885
|
+
};
|
|
7886
|
+
await mkdir9(dir, { recursive: true });
|
|
7887
|
+
await writeFile12(
|
|
7888
|
+
join19(dir, RECORD_FILE),
|
|
7889
|
+
`${JSON.stringify(record, null, 2)}
|
|
7890
|
+
`,
|
|
7891
|
+
"utf-8"
|
|
7892
|
+
);
|
|
7893
|
+
return rel;
|
|
7894
|
+
}
|
|
7895
|
+
async function listBackups(root) {
|
|
7896
|
+
const dir = join19(root, BACKUP_DIR);
|
|
7897
|
+
if (!await pathExists(dir)) {
|
|
7898
|
+
return [];
|
|
7899
|
+
}
|
|
7900
|
+
const entries = await readdir6(dir, { withFileTypes: true });
|
|
7901
|
+
return entries.filter((entry) => entry.isDirectory()).map((entry) => entry.name).toSorted();
|
|
7902
|
+
}
|
|
7903
|
+
async function restoreLatestBackup(root) {
|
|
7904
|
+
const backups = await listBackups(root);
|
|
7905
|
+
const newest = backups.at(-1);
|
|
7906
|
+
if (!newest) {
|
|
7907
|
+
throw new RefusalError(
|
|
7908
|
+
`No backup to restore \u2014 nothing under ${BACKUP_DIR}. \`saasaloy update\` writes one before it applies a plan.`
|
|
7909
|
+
);
|
|
7910
|
+
}
|
|
7911
|
+
const rel = join19(BACKUP_DIR, newest);
|
|
7912
|
+
const dir = join19(root, rel);
|
|
7913
|
+
const raw = await readIfPresent(join19(dir, RECORD_FILE));
|
|
7914
|
+
if (raw === void 0) {
|
|
7915
|
+
throw new RefusalError(
|
|
7916
|
+
`${join19(rel, RECORD_FILE)} is missing \u2014 that backup can't be restored safely.`
|
|
7917
|
+
);
|
|
7918
|
+
}
|
|
7919
|
+
let record;
|
|
7920
|
+
try {
|
|
7921
|
+
record = JSON.parse(raw);
|
|
7922
|
+
} catch (error) {
|
|
7923
|
+
throw new RefusalError(`${join19(rel, RECORD_FILE)} is invalid.`, {
|
|
7924
|
+
cause: error
|
|
7925
|
+
});
|
|
7926
|
+
}
|
|
7927
|
+
const restored = [];
|
|
7928
|
+
for (const target of record.files) {
|
|
7929
|
+
const source = join19(dir, target);
|
|
7930
|
+
const destination = resolveWithinRoot(root, target);
|
|
7931
|
+
if (!await pathExists(source)) {
|
|
7932
|
+
continue;
|
|
7933
|
+
}
|
|
7934
|
+
await mkdir9(dirname12(destination), { recursive: true });
|
|
7935
|
+
await copyFile(source, destination);
|
|
7936
|
+
restored.push(target);
|
|
7937
|
+
}
|
|
7938
|
+
const removed = [];
|
|
7939
|
+
for (const target of record.absent) {
|
|
7940
|
+
const destination = resolveWithinRoot(root, target);
|
|
7941
|
+
if (await pathExists(destination)) {
|
|
7942
|
+
await rmPath3(destination, { force: true });
|
|
7943
|
+
removed.push(target);
|
|
7944
|
+
}
|
|
7945
|
+
}
|
|
7946
|
+
return { from: rel, restored, removed };
|
|
7947
|
+
}
|
|
7948
|
+
|
|
7949
|
+
// src/lib/git-state.ts
|
|
7950
|
+
import { execFile } from "child_process";
|
|
7951
|
+
async function workingTreeState(root) {
|
|
7952
|
+
return new Promise((resolve9) => {
|
|
7953
|
+
execFile(
|
|
7954
|
+
"git",
|
|
7955
|
+
// `-- .` scopes the answer to the project. Without it a project nested inside a
|
|
7956
|
+
// larger repository reports every unrelated change in that repository as its own.
|
|
7957
|
+
["status", "--porcelain", "--untracked-files=no", "--", "."],
|
|
7958
|
+
{ cwd: root },
|
|
7959
|
+
(error, stdout) => {
|
|
7960
|
+
resolve9(error ? "unknown" : stdout.trim() === "" ? "clean" : "dirty");
|
|
7961
|
+
}
|
|
7962
|
+
);
|
|
7963
|
+
});
|
|
7964
|
+
}
|
|
7965
|
+
|
|
7966
|
+
// src/lib/project-name.ts
|
|
7967
|
+
import { basename as basename3 } from "path";
|
|
7968
|
+
async function resolveProjectName(root, config) {
|
|
7969
|
+
if (config.name) {
|
|
7970
|
+
return { name: config.name, source: "config" };
|
|
7971
|
+
}
|
|
7972
|
+
const pkg = await readRootPackageJson(root);
|
|
7973
|
+
const pkgName = typeof pkg?.name === "string" ? pkg.name : void 0;
|
|
7974
|
+
if (pkgName) {
|
|
7975
|
+
return { name: pkgName, source: "package.json" };
|
|
7976
|
+
}
|
|
7977
|
+
return { name: basename3(root), source: "directory" };
|
|
7978
|
+
}
|
|
7979
|
+
|
|
7388
7980
|
// src/lib/merge-plan.ts
|
|
7389
7981
|
function renderMergePlan(plan) {
|
|
7390
7982
|
const modules = plan.modules.filter((m) => m.needsMerge);
|
|
@@ -7499,6 +8091,17 @@ function renderFile(file) {
|
|
|
7499
8091
|
""
|
|
7500
8092
|
);
|
|
7501
8093
|
}
|
|
8094
|
+
if (file.projectOwned) {
|
|
8095
|
+
out.push(
|
|
8096
|
+
"This file is yours: the template ships it once and never writes it again. The template's copy moved, so the change is shown below for you to take what you want from it. Nothing here was written.",
|
|
8097
|
+
""
|
|
8098
|
+
);
|
|
8099
|
+
} else if (file.adopted) {
|
|
8100
|
+
out.push(
|
|
8101
|
+
"Saasaloy adopted this file rather than writing it, so the hash it records came off your disk and proves nothing about who wrote these bytes. It is treated as edited and was left alone.",
|
|
8102
|
+
""
|
|
8103
|
+
);
|
|
8104
|
+
}
|
|
7502
8105
|
if (file.patchedBy && file.patchedBy.length > 0) {
|
|
7503
8106
|
out.push(
|
|
7504
8107
|
`Note: \`${file.patchedBy.join("`, `")}\` also applied a config patch to this file. Part of the difference below is that patch rather than an edit anyone made by hand \u2014 keep those lines when you merge.`,
|
|
@@ -7611,11 +8214,13 @@ var KNOWN_FLAGS9 = /* @__PURE__ */ new Set([
|
|
|
7611
8214
|
"--diff",
|
|
7612
8215
|
"--yes",
|
|
7613
8216
|
"-y",
|
|
8217
|
+
"--force",
|
|
8218
|
+
"--abort",
|
|
7614
8219
|
"--help",
|
|
7615
8220
|
"-h"
|
|
7616
8221
|
]);
|
|
7617
8222
|
var VALUE_FLAGS2 = /* @__PURE__ */ new Set(["--ref", "--out"]);
|
|
7618
|
-
var USAGE9 = "saasaloy update [<module>|base] [--ref <ref>] [--out <path>] [--dry-run] [--diff] [--yes]";
|
|
8223
|
+
var USAGE9 = "saasaloy update [<module>|base] [--ref <ref>] [--out <path>] [--dry-run] [--diff] [--force] [--yes] | saasaloy update --abort";
|
|
7619
8224
|
var HELP9 = {
|
|
7620
8225
|
name: "update",
|
|
7621
8226
|
describe: DESCRIPTIONS.update,
|
|
@@ -7625,6 +8230,8 @@ var HELP9 = {
|
|
|
7625
8230
|
"--out <path>": "write the merge plan to a file instead of stdout",
|
|
7626
8231
|
"--dry-run": "show the plan and write nothing",
|
|
7627
8232
|
"--diff": "show a per-file diff and write nothing",
|
|
8233
|
+
"--force": "apply even with uncommitted changes in the working tree",
|
|
8234
|
+
"--abort": "restore the files the last update replaced, then stop",
|
|
7628
8235
|
"-y, --yes": "skip the confirmation prompt"
|
|
7629
8236
|
}
|
|
7630
8237
|
};
|
|
@@ -7664,6 +8271,8 @@ function parseArgs9(argv) {
|
|
|
7664
8271
|
dryRun: argv.includes("--dry-run"),
|
|
7665
8272
|
diff: argv.includes("--diff"),
|
|
7666
8273
|
yes: argv.includes("--yes") || argv.includes("-y"),
|
|
8274
|
+
force: argv.includes("--force"),
|
|
8275
|
+
abort: argv.includes("--abort"),
|
|
7667
8276
|
unknown
|
|
7668
8277
|
};
|
|
7669
8278
|
}
|
|
@@ -7824,13 +8433,13 @@ function skipReason(comparison, preview = false) {
|
|
|
7824
8433
|
}
|
|
7825
8434
|
}
|
|
7826
8435
|
}
|
|
7827
|
-
var
|
|
8436
|
+
var STATE_FILES2 = [CONFIG_FILE, LOCK_FILE, MANIFEST_FILE];
|
|
7828
8437
|
async function stateFileRefusal(root, out) {
|
|
7829
8438
|
if (!out) {
|
|
7830
8439
|
return void 0;
|
|
7831
8440
|
}
|
|
7832
8441
|
const target = await canonicalPath(resolvePlanOut(out));
|
|
7833
|
-
for (const file of
|
|
8442
|
+
for (const file of STATE_FILES2) {
|
|
7834
8443
|
if (await canonicalPath(resolve8(root, file)) === target) {
|
|
7835
8444
|
return `Refusing to write the merge plan to ${pc11.cyan(out)} \u2014 that is Saasaloy's own ${pc11.cyan(file)}. Pick another path.`;
|
|
7836
8445
|
}
|
|
@@ -7842,7 +8451,7 @@ async function canonicalPath(path) {
|
|
|
7842
8451
|
return await realpath(path);
|
|
7843
8452
|
} catch {
|
|
7844
8453
|
try {
|
|
7845
|
-
return
|
|
8454
|
+
return join20(await realpath(dirname13(path)), basename4(path));
|
|
7846
8455
|
} catch {
|
|
7847
8456
|
return path;
|
|
7848
8457
|
}
|
|
@@ -7857,10 +8466,24 @@ async function emitMergePlan(document, out) {
|
|
|
7857
8466
|
return;
|
|
7858
8467
|
}
|
|
7859
8468
|
const target = resolvePlanOut(out);
|
|
7860
|
-
await
|
|
7861
|
-
await
|
|
8469
|
+
await mkdir10(dirname13(target), { recursive: true });
|
|
8470
|
+
await writeFile13(target, document, "utf-8");
|
|
7862
8471
|
log7.success(`Merge plan written to ${pc11.cyan(out)}`, TUI_ON_STDERR);
|
|
7863
8472
|
}
|
|
8473
|
+
function backupTargets(plan) {
|
|
8474
|
+
const targets = /* @__PURE__ */ new Set();
|
|
8475
|
+
for (const mod of plan.modules) {
|
|
8476
|
+
for (const file of [...mod.files, ...mod.removals]) {
|
|
8477
|
+
if (!QUIET.has(file.action) && file.action !== "skip") {
|
|
8478
|
+
targets.add(file.target);
|
|
8479
|
+
}
|
|
8480
|
+
}
|
|
8481
|
+
for (const patch of mod.patches) {
|
|
8482
|
+
targets.add(patch.file);
|
|
8483
|
+
}
|
|
8484
|
+
}
|
|
8485
|
+
return [...targets].toSorted();
|
|
8486
|
+
}
|
|
7864
8487
|
function splitSlug2(slug) {
|
|
7865
8488
|
const [owner, repo, ...rest] = slug.split("/");
|
|
7866
8489
|
if (!owner || !repo || rest.length > 0) {
|
|
@@ -7898,12 +8521,43 @@ async function runUpdate(argv) {
|
|
|
7898
8521
|
cancel9(formatFailure(error), TUI_ON_STDERR);
|
|
7899
8522
|
return exitCodeFor(error);
|
|
7900
8523
|
}
|
|
8524
|
+
if (opts.abort) {
|
|
8525
|
+
try {
|
|
8526
|
+
const restored = await restoreLatestBackup(root);
|
|
8527
|
+
for (const target of restored.restored) {
|
|
8528
|
+
log7.step(`${pc11.green("restore")} ${target}`, TUI_ON_STDERR);
|
|
8529
|
+
}
|
|
8530
|
+
for (const target of restored.removed) {
|
|
8531
|
+
log7.step(
|
|
8532
|
+
`${pc11.red("remove")} ${target} ${pc11.dim("(the update created it)")}`,
|
|
8533
|
+
TUI_ON_STDERR
|
|
8534
|
+
);
|
|
8535
|
+
}
|
|
8536
|
+
outro9(
|
|
8537
|
+
pc11.green(
|
|
8538
|
+
`Restored ${restored.restored.length} file${restored.restored.length === 1 ? "" : "s"} from ${restored.from}.`
|
|
8539
|
+
),
|
|
8540
|
+
TUI_ON_STDERR
|
|
8541
|
+
);
|
|
8542
|
+
return EXIT_OK;
|
|
8543
|
+
} catch (error) {
|
|
8544
|
+
cancel9(formatFailure(error), TUI_ON_STDERR);
|
|
8545
|
+
return exitCodeFor(error);
|
|
8546
|
+
}
|
|
8547
|
+
}
|
|
7901
8548
|
const outRefusal = await stateFileRefusal(root, opts.out);
|
|
7902
8549
|
if (outRefusal) {
|
|
7903
8550
|
cancel9(outRefusal, TUI_ON_STDERR);
|
|
7904
8551
|
return EXIT_REFUSED;
|
|
7905
8552
|
}
|
|
7906
8553
|
const preview = opts.dryRun || opts.diff;
|
|
8554
|
+
if (!preview && !opts.force && await workingTreeState(root) === "dirty") {
|
|
8555
|
+
cancel9(
|
|
8556
|
+
"The working tree has uncommitted changes. Commit or stash them first, so `git diff` shows what this update did \u2014 or re-run with `--force`. Nothing was written.",
|
|
8557
|
+
TUI_ON_STDERR
|
|
8558
|
+
);
|
|
8559
|
+
return EXIT_REFUSED;
|
|
8560
|
+
}
|
|
7907
8561
|
if (!opts.yes && !preview && !process.stdin.isTTY) {
|
|
7908
8562
|
cancel9(
|
|
7909
8563
|
"No terminal to confirm in \u2014 re-run with `--yes` to apply, or `--dry-run` to preview.",
|
|
@@ -7937,12 +8591,20 @@ async function runUpdate(argv) {
|
|
|
7937
8591
|
const cliVersion = await readVersion();
|
|
7938
8592
|
let baseComparison;
|
|
7939
8593
|
let templateDir;
|
|
8594
|
+
const project = await resolveProjectName(root, config);
|
|
7940
8595
|
if (wantsBase) {
|
|
7941
8596
|
templateDir = await baseTemplateDir();
|
|
8597
|
+
if (project.source === "directory") {
|
|
8598
|
+
log7.warn(
|
|
8599
|
+
`No \`name\` in ${CONFIG_FILE} and no root package.json name \u2014 falling back to the directory name ${pc11.cyan(project.name)}. Set \`name\` in ${CONFIG_FILE} if that isn't this project's name.`,
|
|
8600
|
+
TUI_ON_STDERR
|
|
8601
|
+
);
|
|
8602
|
+
}
|
|
7942
8603
|
if (!isBaseTracked(lock, manifest)) {
|
|
7943
8604
|
const adoption = await adoptBase({
|
|
7944
8605
|
root,
|
|
7945
8606
|
cliVersion,
|
|
8607
|
+
projectName: project.name,
|
|
7946
8608
|
manifest,
|
|
7947
8609
|
lock,
|
|
7948
8610
|
templateDir,
|
|
@@ -7950,7 +8612,7 @@ async function runUpdate(argv) {
|
|
|
7950
8612
|
});
|
|
7951
8613
|
const count = adoption.adopted.length;
|
|
7952
8614
|
const lines = [
|
|
7953
|
-
`${preview ? "Would adopt" : "Adopted"} ${count} base file${count === 1 ? "" : "s"} at CLI ${pc11.cyan(cliVersion)}, each at the hash it has on disk
|
|
8615
|
+
`${preview ? "Would adopt" : "Adopted"} ${count} base file${count === 1 ? "" : "s"} at CLI ${pc11.cyan(cliVersion)}, each at the hash it has on disk. Those bytes may be yours, so the next update treats every one of them as edited and offers a merge rather than a write.`
|
|
7954
8616
|
];
|
|
7955
8617
|
if (adoption.absent.length > 0) {
|
|
7956
8618
|
lines.push(
|
|
@@ -8066,6 +8728,7 @@ async function runUpdate(argv) {
|
|
|
8066
8728
|
lock,
|
|
8067
8729
|
templateDir,
|
|
8068
8730
|
cliVersion,
|
|
8731
|
+
projectName: project.name,
|
|
8069
8732
|
comparison: baseComparison
|
|
8070
8733
|
});
|
|
8071
8734
|
cleanups.push(handle.cleanup);
|
|
@@ -8250,6 +8913,15 @@ async function runUpdate(argv) {
|
|
|
8250
8913
|
return EXIT_OK;
|
|
8251
8914
|
}
|
|
8252
8915
|
}
|
|
8916
|
+
const backupDir = await writeBackup({
|
|
8917
|
+
root,
|
|
8918
|
+
targets: backupTargets(plan),
|
|
8919
|
+
cliVersion
|
|
8920
|
+
});
|
|
8921
|
+
log7.info(
|
|
8922
|
+
`Pre-update copies in ${pc11.cyan(backupDir)} ${pc11.dim("\u2014 `saasaloy update --abort` restores them")}`,
|
|
8923
|
+
TUI_ON_STDERR
|
|
8924
|
+
);
|
|
8253
8925
|
const outcome = await executeUpdatePlan(plan, {
|
|
8254
8926
|
root,
|
|
8255
8927
|
config,
|
|
@@ -8288,7 +8960,7 @@ async function runUpdate(argv) {
|
|
|
8288
8960
|
);
|
|
8289
8961
|
}
|
|
8290
8962
|
outro9(summarizeOutcome(plan, result), TUI_ON_STDERR);
|
|
8291
|
-
return EXIT_OK;
|
|
8963
|
+
return plan.needsMerge ? EXIT_MERGE_PENDING : EXIT_OK;
|
|
8292
8964
|
} catch (error) {
|
|
8293
8965
|
cancel9(formatFailure(error), TUI_ON_STDERR);
|
|
8294
8966
|
return exitCodeFor(error);
|