saasaloy 0.1.1 → 0.2.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 +6 -3
- package/dist/index.js +376 -56
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/schemas/manifest.schema.json +4 -0
- package/schemas/saasaloy.schema.json +5 -0
- package/templates/base/_gitignore +4 -1
- package/templates/base/_saasaloy-base.json +10 -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
|
};
|
|
@@ -877,7 +878,8 @@ function upsertWranglerBinding(source, patch) {
|
|
|
877
878
|
if (!root) {
|
|
878
879
|
return source;
|
|
879
880
|
}
|
|
880
|
-
const
|
|
881
|
+
const bindingPath = splitBindingType(patch.bindingType);
|
|
882
|
+
const arrayNode = findNodeAtLocation(root, bindingPath);
|
|
881
883
|
const formattingOptions = inferFormatting(source);
|
|
882
884
|
if (arrayNode?.type === "array") {
|
|
883
885
|
const existing = (arrayNode.children ?? []).map(
|
|
@@ -892,7 +894,7 @@ function upsertWranglerBinding(source, patch) {
|
|
|
892
894
|
}
|
|
893
895
|
const edits2 = modify(
|
|
894
896
|
source,
|
|
895
|
-
[
|
|
897
|
+
[...bindingPath, existing.length],
|
|
896
898
|
patch.entry,
|
|
897
899
|
{
|
|
898
900
|
formattingOptions,
|
|
@@ -901,7 +903,7 @@ function upsertWranglerBinding(source, patch) {
|
|
|
901
903
|
);
|
|
902
904
|
return applyEdits(source, edits2);
|
|
903
905
|
}
|
|
904
|
-
const edits = modify(source,
|
|
906
|
+
const edits = modify(source, bindingPath, [patch.entry], {
|
|
905
907
|
formattingOptions
|
|
906
908
|
});
|
|
907
909
|
return applyEdits(source, edits);
|
|
@@ -911,7 +913,8 @@ function removeWranglerBinding(source, patch) {
|
|
|
911
913
|
if (!root) {
|
|
912
914
|
return source;
|
|
913
915
|
}
|
|
914
|
-
const
|
|
916
|
+
const bindingPath = splitBindingType(patch.bindingType);
|
|
917
|
+
const arrayNode = findNodeAtLocation(root, bindingPath);
|
|
915
918
|
if (arrayNode?.type !== "array") {
|
|
916
919
|
return source;
|
|
917
920
|
}
|
|
@@ -925,7 +928,7 @@ function removeWranglerBinding(source, patch) {
|
|
|
925
928
|
if (matchWranglerBinding(source, patch)) {
|
|
926
929
|
return source;
|
|
927
930
|
}
|
|
928
|
-
const path = existing.length === 1 ?
|
|
931
|
+
const path = existing.length === 1 ? emptiedAncestorPath(root, bindingPath) : [...bindingPath, index];
|
|
929
932
|
const edits = modify(source, path, void 0, {
|
|
930
933
|
formattingOptions: inferFormatting(source)
|
|
931
934
|
});
|
|
@@ -938,6 +941,20 @@ function wranglerBindingRemoveRefusal(source, patch) {
|
|
|
938
941
|
}
|
|
939
942
|
return `${match.key} holds ${JSON.stringify(match.current)} now, not the entry that was applied, so it is not ours to delete`;
|
|
940
943
|
}
|
|
944
|
+
function splitBindingType(bindingType) {
|
|
945
|
+
return bindingType.split(".");
|
|
946
|
+
}
|
|
947
|
+
function emptiedAncestorPath(root, bindingPath) {
|
|
948
|
+
let cut = bindingPath.length;
|
|
949
|
+
for (let depth = bindingPath.length - 1; depth >= 1; depth -= 1) {
|
|
950
|
+
const parent = findNodeAtLocation(root, bindingPath.slice(0, depth));
|
|
951
|
+
if (parent?.type !== "object" || (parent.children?.length ?? 0) !== 1) {
|
|
952
|
+
break;
|
|
953
|
+
}
|
|
954
|
+
cut = depth;
|
|
955
|
+
}
|
|
956
|
+
return bindingPath.slice(0, cut);
|
|
957
|
+
}
|
|
941
958
|
function findEntryIndex(existing, patch) {
|
|
942
959
|
const { entry } = patch;
|
|
943
960
|
if (typeof entry === "string") {
|
|
@@ -957,7 +974,10 @@ function matchWranglerBinding(source, patch) {
|
|
|
957
974
|
if (!root) {
|
|
958
975
|
return void 0;
|
|
959
976
|
}
|
|
960
|
-
const arrayNode = findNodeAtLocation(
|
|
977
|
+
const arrayNode = findNodeAtLocation(
|
|
978
|
+
root,
|
|
979
|
+
splitBindingType(patch.bindingType)
|
|
980
|
+
);
|
|
961
981
|
if (arrayNode?.type !== "array") {
|
|
962
982
|
return void 0;
|
|
963
983
|
}
|
|
@@ -3657,7 +3677,7 @@ import pc3 from "picocolors";
|
|
|
3657
3677
|
import { createHash as createHash2 } from "crypto";
|
|
3658
3678
|
import { mkdtemp as mkdtemp2, readdir as readdir3, readFile as readFile12, rm as rm2 } from "fs/promises";
|
|
3659
3679
|
import { tmpdir as tmpdir2 } from "os";
|
|
3660
|
-
import {
|
|
3680
|
+
import { join as join10, posix as posix6 } from "path";
|
|
3661
3681
|
|
|
3662
3682
|
// src/lib/scaffold.ts
|
|
3663
3683
|
import { mkdir as mkdir5, readdir as readdir2, readFile as readFile11, writeFile as writeFile7 } from "fs/promises";
|
|
@@ -3667,7 +3687,7 @@ import { fileURLToPath as fileURLToPath3 } from "url";
|
|
|
3667
3687
|
// package.json
|
|
3668
3688
|
var package_default = {
|
|
3669
3689
|
name: "saasaloy",
|
|
3670
|
-
version: "0.
|
|
3690
|
+
version: "0.2.0",
|
|
3671
3691
|
description: "Composable SaaS accelerator kit.",
|
|
3672
3692
|
keywords: [
|
|
3673
3693
|
"saas",
|
|
@@ -3807,10 +3827,20 @@ var BASE_MODULE = "base";
|
|
|
3807
3827
|
var DEFAULT_BASE_NAME = "web";
|
|
3808
3828
|
var BASE_NO_MERGE_BASE = "the template ships with the CLI; the previous version isn't on disk";
|
|
3809
3829
|
var BASE_INTENT = "the base template changed; keep local edits, take the upstream change";
|
|
3830
|
+
function compileGlob(pattern) {
|
|
3831
|
+
const source = pattern.split("/").map(
|
|
3832
|
+
(segment) => segment === "**" ? "[^\0]*" : segment.replaceAll(/[.+^${}()|[\]\\]/g, String.raw`\$&`).replaceAll("*", "[^/]*")
|
|
3833
|
+
).join("/").replaceAll("[^\0]*/", "(?:[^\0]*/)?");
|
|
3834
|
+
return new RegExp(`^${source}$`);
|
|
3835
|
+
}
|
|
3836
|
+
function ownedMatcher(declaration) {
|
|
3837
|
+
const patterns = declaration.ownedFiles.map(compileGlob);
|
|
3838
|
+
return (target) => patterns.some((re) => re.test(target));
|
|
3839
|
+
}
|
|
3810
3840
|
async function readBaseDeclaration(templateDir) {
|
|
3811
3841
|
const raw = await readIfPresent(join10(templateDir, BASE_DECLARATION));
|
|
3812
3842
|
if (raw === void 0) {
|
|
3813
|
-
return { seedFiles: [] };
|
|
3843
|
+
return { seedFiles: [], ownedFiles: [] };
|
|
3814
3844
|
}
|
|
3815
3845
|
const parsed = JSON.parse(raw);
|
|
3816
3846
|
const seedFiles = parsed.seedFiles;
|
|
@@ -3819,7 +3849,16 @@ async function readBaseDeclaration(templateDir) {
|
|
|
3819
3849
|
`${BASE_DECLARATION} must declare "seedFiles" as a list of project-relative paths.`
|
|
3820
3850
|
);
|
|
3821
3851
|
}
|
|
3822
|
-
|
|
3852
|
+
const ownedFiles = parsed.ownedFiles ?? [];
|
|
3853
|
+
if (!Array.isArray(ownedFiles) || !ownedFiles.every((entry) => typeof entry === "string" && entry !== "")) {
|
|
3854
|
+
throw new Error(
|
|
3855
|
+
`${BASE_DECLARATION} must declare "ownedFiles" as a list of project-relative globs.`
|
|
3856
|
+
);
|
|
3857
|
+
}
|
|
3858
|
+
return {
|
|
3859
|
+
seedFiles,
|
|
3860
|
+
ownedFiles
|
|
3861
|
+
};
|
|
3823
3862
|
}
|
|
3824
3863
|
async function listTemplateFiles(templateDir) {
|
|
3825
3864
|
const out = [];
|
|
@@ -3881,6 +3920,7 @@ function recordBaseFiles(manifest, files) {
|
|
|
3881
3920
|
manifest.managed[file.target] = {
|
|
3882
3921
|
module: BASE_MODULE,
|
|
3883
3922
|
hash: file.hash,
|
|
3923
|
+
...file.adopted ? { adopted: true } : {},
|
|
3884
3924
|
from: file.from
|
|
3885
3925
|
};
|
|
3886
3926
|
}
|
|
@@ -3888,13 +3928,28 @@ function recordBaseFiles(manifest, files) {
|
|
|
3888
3928
|
function isBaseTracked(lock, manifest) {
|
|
3889
3929
|
return lock.base !== void 0 && Object.keys(baseEntries(manifest)).length > 0;
|
|
3890
3930
|
}
|
|
3891
|
-
async function
|
|
3931
|
+
async function missingBaseTargets(root, manifest, templateDir) {
|
|
3932
|
+
const seed = new Set((await readBaseDeclaration(templateDir)).seedFiles);
|
|
3933
|
+
const missing = [];
|
|
3934
|
+
for (const target of Object.keys(baseEntries(manifest))) {
|
|
3935
|
+
if (seed.has(target)) {
|
|
3936
|
+
continue;
|
|
3937
|
+
}
|
|
3938
|
+
if (await readIfPresent(resolveWithinRoot(root, target)) === void 0) {
|
|
3939
|
+
missing.push(target);
|
|
3940
|
+
}
|
|
3941
|
+
}
|
|
3942
|
+
return missing.toSorted((a, b) => a < b ? -1 : 1);
|
|
3943
|
+
}
|
|
3944
|
+
async function renderTemplate(root, templateDir, projectName) {
|
|
3892
3945
|
const dir = await mkdtemp2(join10(tmpdir2(), "saasaloy-base-render-"));
|
|
3893
3946
|
const files = await copyTemplate(
|
|
3894
3947
|
templateDir,
|
|
3895
3948
|
dir,
|
|
3896
|
-
templateVars(
|
|
3897
|
-
{
|
|
3949
|
+
templateVars(projectName),
|
|
3950
|
+
{
|
|
3951
|
+
keepNames: true
|
|
3952
|
+
}
|
|
3898
3953
|
);
|
|
3899
3954
|
return {
|
|
3900
3955
|
dir,
|
|
@@ -3903,9 +3958,16 @@ async function renderTemplate(root, templateDir) {
|
|
|
3903
3958
|
};
|
|
3904
3959
|
}
|
|
3905
3960
|
async function adoptBase(args) {
|
|
3906
|
-
const {
|
|
3961
|
+
const {
|
|
3962
|
+
root,
|
|
3963
|
+
cliVersion,
|
|
3964
|
+
projectName,
|
|
3965
|
+
manifest,
|
|
3966
|
+
lock,
|
|
3967
|
+
dryRun = false
|
|
3968
|
+
} = args;
|
|
3907
3969
|
const templateDir = args.templateDir ?? await baseTemplateDir();
|
|
3908
|
-
const render = await renderTemplate(root, templateDir);
|
|
3970
|
+
const render = await renderTemplate(root, templateDir, projectName);
|
|
3909
3971
|
try {
|
|
3910
3972
|
const files = [];
|
|
3911
3973
|
const adopted = [];
|
|
@@ -3920,7 +3982,8 @@ async function adoptBase(args) {
|
|
|
3920
3982
|
files.push({
|
|
3921
3983
|
target: file.target,
|
|
3922
3984
|
from: file.from,
|
|
3923
|
-
hash: hashContent(mine)
|
|
3985
|
+
hash: hashContent(mine),
|
|
3986
|
+
adopted: true
|
|
3924
3987
|
});
|
|
3925
3988
|
}
|
|
3926
3989
|
}
|
|
@@ -3941,9 +4004,18 @@ async function adoptBase(args) {
|
|
|
3941
4004
|
}
|
|
3942
4005
|
}
|
|
3943
4006
|
async function baseUpdateInput(args) {
|
|
3944
|
-
const {
|
|
3945
|
-
|
|
3946
|
-
|
|
4007
|
+
const {
|
|
4008
|
+
root,
|
|
4009
|
+
manifest,
|
|
4010
|
+
lock,
|
|
4011
|
+
templateDir,
|
|
4012
|
+
cliVersion,
|
|
4013
|
+
projectName,
|
|
4014
|
+
comparison
|
|
4015
|
+
} = args;
|
|
4016
|
+
const render = await renderTemplate(root, templateDir, projectName);
|
|
4017
|
+
const declaration = await readBaseDeclaration(templateDir);
|
|
4018
|
+
const seed = new Set(declaration.seedFiles);
|
|
3947
4019
|
const shipped = render.files.filter((file) => !seed.has(file.target));
|
|
3948
4020
|
const targets = new Set(shipped.map((file) => file.target));
|
|
3949
4021
|
return {
|
|
@@ -3968,6 +4040,7 @@ async function baseUpdateInput(args) {
|
|
|
3968
4040
|
noMergeBase: BASE_NO_MERGE_BASE,
|
|
3969
4041
|
intent: [],
|
|
3970
4042
|
ignoreTargets: seed,
|
|
4043
|
+
isOwned: ownedMatcher(declaration),
|
|
3971
4044
|
reapplyPatches: manifest.patches.filter(
|
|
3972
4045
|
(patch) => targets.has(patch.file)
|
|
3973
4046
|
),
|
|
@@ -3983,7 +4056,7 @@ async function baseUpdateInput(args) {
|
|
|
3983
4056
|
|
|
3984
4057
|
// src/lib/doctor.ts
|
|
3985
4058
|
import { readFile as readFile13 } from "fs/promises";
|
|
3986
|
-
import { basename
|
|
4059
|
+
import { basename, dirname as dirname6, join as join11, posix as posix7, resolve as resolve3 } from "path";
|
|
3987
4060
|
var SKILL_PREFIX = "saasaloy-";
|
|
3988
4061
|
var PINNED_DEP = /^(@[^/@]+\/)?[^/@]+@\d+\.\d+\.\d+(-[\w.-]+)?(\+[\w.-]+)?$/;
|
|
3989
4062
|
function finding(module, where, message) {
|
|
@@ -4037,7 +4110,7 @@ async function readDescriptor(dir) {
|
|
|
4037
4110
|
}
|
|
4038
4111
|
async function checkModule(input) {
|
|
4039
4112
|
const { aliases, dir, siblings } = input;
|
|
4040
|
-
const module =
|
|
4113
|
+
const module = basename(dir);
|
|
4041
4114
|
const findings = [];
|
|
4042
4115
|
const report = () => ({ dir, findings, module });
|
|
4043
4116
|
const file = join11(dir, "registry-item.json");
|
|
@@ -4241,7 +4314,7 @@ function checkProject(args) {
|
|
|
4241
4314
|
async function resolveDoctorTarget(path) {
|
|
4242
4315
|
const dir = resolve3(path);
|
|
4243
4316
|
if (await pathExists(join11(dir, "registry-item.json"))) {
|
|
4244
|
-
return { names: [
|
|
4317
|
+
return { names: [basename(dir)], registryDir: dirname6(dir) };
|
|
4245
4318
|
}
|
|
4246
4319
|
return { names: await registryModuleNames(dir), registryDir: dir };
|
|
4247
4320
|
}
|
|
@@ -4326,7 +4399,7 @@ var HELP2 = {
|
|
|
4326
4399
|
usage: USAGE2,
|
|
4327
4400
|
flags: {}
|
|
4328
4401
|
};
|
|
4329
|
-
var DEFAULT_PATH = "
|
|
4402
|
+
var DEFAULT_PATH = ".";
|
|
4330
4403
|
function parseArgs2(argv) {
|
|
4331
4404
|
const positional = [];
|
|
4332
4405
|
const unknown = [];
|
|
@@ -4425,9 +4498,8 @@ async function runDoctor(argv) {
|
|
|
4425
4498
|
const path = opts.path ?? DEFAULT_PATH;
|
|
4426
4499
|
try {
|
|
4427
4500
|
if (!await pathExists(path)) {
|
|
4428
|
-
const hint = opts.path === void 0 && await pathExists(CONFIG_FILE) ? ` Run \`saasaloy doctor .\` to check this project instead.` : "";
|
|
4429
4501
|
cancel2(
|
|
4430
|
-
`No such path: ${path} \u2014 point \`doctor\` at a module folder or at a directory of them
|
|
4502
|
+
`No such path: ${path} \u2014 point \`doctor\` at a module folder or at a directory of them.`
|
|
4431
4503
|
);
|
|
4432
4504
|
return EXIT_REFUSED;
|
|
4433
4505
|
}
|
|
@@ -5091,8 +5163,8 @@ ${entry.declaration.description}`,
|
|
|
5091
5163
|
|
|
5092
5164
|
// src/commands/init.ts
|
|
5093
5165
|
import { spawn } from "child_process";
|
|
5094
|
-
import { readdir as readdir4 } from "fs/promises";
|
|
5095
|
-
import { basename as
|
|
5166
|
+
import { readdir as readdir4, readFile as readFile16 } from "fs/promises";
|
|
5167
|
+
import { basename as basename2, join as join14, resolve as resolve6 } from "path";
|
|
5096
5168
|
import {
|
|
5097
5169
|
cancel as cancel4,
|
|
5098
5170
|
intro as intro4,
|
|
@@ -5284,7 +5356,7 @@ async function runInit(argv) {
|
|
|
5284
5356
|
if (!trimmed) {
|
|
5285
5357
|
return "Enter a project name (or `.` for the current directory).";
|
|
5286
5358
|
}
|
|
5287
|
-
const name =
|
|
5359
|
+
const name = basename2(resolve6(process.cwd(), trimmed));
|
|
5288
5360
|
if (!NAME_PATTERN.test(name)) {
|
|
5289
5361
|
return "Use lowercase letters, digits, and hyphens (e.g. my-app).";
|
|
5290
5362
|
}
|
|
@@ -5298,7 +5370,7 @@ async function runInit(argv) {
|
|
|
5298
5370
|
nameArg = answer.trim();
|
|
5299
5371
|
}
|
|
5300
5372
|
const target = resolve6(process.cwd(), nameArg);
|
|
5301
|
-
const projectName =
|
|
5373
|
+
const projectName = basename2(target);
|
|
5302
5374
|
if (!NAME_PATTERN.test(projectName)) {
|
|
5303
5375
|
cancel4(
|
|
5304
5376
|
`Invalid project name "${projectName}". Use lowercase letters, digits, and hyphens (e.g. my-app).`
|
|
@@ -5325,6 +5397,17 @@ async function runInit(argv) {
|
|
|
5325
5397
|
s.stop(
|
|
5326
5398
|
`Scaffolded ${pc6.cyan(projectName)} ${pc6.dim("(apps/web \xB7 packages/ui \xB7 packages/tsconfig)")}`
|
|
5327
5399
|
);
|
|
5400
|
+
try {
|
|
5401
|
+
const config = await loadConfig(target);
|
|
5402
|
+
await saveConfig(target, { ...config, name: projectName });
|
|
5403
|
+
const entry = written.find((file) => file.target === CONFIG_FILE);
|
|
5404
|
+
if (entry) {
|
|
5405
|
+
entry.hash = hashContent(await readFile16(entry.path, "utf-8"));
|
|
5406
|
+
}
|
|
5407
|
+
} catch (error) {
|
|
5408
|
+
cancel4(`Couldn't record the project name: ${errorMessage(error)}`);
|
|
5409
|
+
return EXIT_FAILURE;
|
|
5410
|
+
}
|
|
5328
5411
|
try {
|
|
5329
5412
|
const manifest = await loadManifest(target);
|
|
5330
5413
|
const lock = await loadLock(target);
|
|
@@ -5888,11 +5971,11 @@ import { cancel as cancel7, intro as intro7, log as log5, note as note7, outro a
|
|
|
5888
5971
|
import pc9 from "picocolors";
|
|
5889
5972
|
|
|
5890
5973
|
// src/lib/updater.ts
|
|
5891
|
-
import { mkdir as mkdir8, readFile as
|
|
5974
|
+
import { mkdir as mkdir8, readFile as readFile18, rm as rmPath2, writeFile as writeFile11 } from "fs/promises";
|
|
5892
5975
|
import { dirname as dirname11, posix as posix9 } from "path";
|
|
5893
5976
|
|
|
5894
5977
|
// src/lib/remover.ts
|
|
5895
|
-
import { readdir as readdir5, readFile as
|
|
5978
|
+
import { readdir as readdir5, readFile as readFile17, rm as rmPath, writeFile as writeFile10 } from "fs/promises";
|
|
5896
5979
|
import { dirname as dirname10, join as join18, relative as relative5, sep as sep3 } from "path";
|
|
5897
5980
|
function classifyTrackedFile(content, manifestHash) {
|
|
5898
5981
|
if (content === void 0) {
|
|
@@ -5917,7 +6000,7 @@ async function buildRemovePlan(args) {
|
|
|
5917
6000
|
continue;
|
|
5918
6001
|
}
|
|
5919
6002
|
const targetAbs = resolveWithinRoot(root, target);
|
|
5920
|
-
const oldContent = await pathExists(targetAbs) ? await
|
|
6003
|
+
const oldContent = await pathExists(targetAbs) ? await readFile17(targetAbs, "utf-8") : void 0;
|
|
5921
6004
|
files.push({
|
|
5922
6005
|
module: name,
|
|
5923
6006
|
target,
|
|
@@ -5984,7 +6067,7 @@ async function previewPatchRemoval(root, entry) {
|
|
|
5984
6067
|
return { entry, action: "gone", diff: "" };
|
|
5985
6068
|
}
|
|
5986
6069
|
await assertNoSymlinkPath(root, fileAbs);
|
|
5987
|
-
const source = await
|
|
6070
|
+
const source = await readFile17(fileAbs, "utf-8");
|
|
5988
6071
|
const attempt = tryReversePatch(source, entry);
|
|
5989
6072
|
if (!attempt.ok) {
|
|
5990
6073
|
return { entry, action: "refused", diff: "", reason: attempt.reason };
|
|
@@ -6052,7 +6135,7 @@ async function recheckFile(file) {
|
|
|
6052
6135
|
if (file.oldContent === void 0) {
|
|
6053
6136
|
return "changed";
|
|
6054
6137
|
}
|
|
6055
|
-
const current = await
|
|
6138
|
+
const current = await readFile17(file.targetAbs, "utf-8");
|
|
6056
6139
|
return hashContent(current) === hashContent(file.oldContent) ? "unchanged" : "changed";
|
|
6057
6140
|
}
|
|
6058
6141
|
async function executeRemovePlan(plan, args) {
|
|
@@ -6104,7 +6187,7 @@ async function executeRemovePlan(plan, args) {
|
|
|
6104
6187
|
let reversed = false;
|
|
6105
6188
|
if (await pathExists(fileAbs)) {
|
|
6106
6189
|
await assertNoSymlinkPath(root, fileAbs);
|
|
6107
|
-
const source = await
|
|
6190
|
+
const source = await readFile17(fileAbs, "utf-8");
|
|
6108
6191
|
const attempt = tryReversePatch(source, entry);
|
|
6109
6192
|
if (!attempt.ok) {
|
|
6110
6193
|
patchRefusals.push({ patch: entry, reason: attempt.reason });
|
|
@@ -6244,6 +6327,7 @@ function baseLabel(version2, hash) {
|
|
|
6244
6327
|
}
|
|
6245
6328
|
function compareBase(args) {
|
|
6246
6329
|
const { lock, manifest, runningHash, runningVersion } = args;
|
|
6330
|
+
const missing = args.missingTargets ?? [];
|
|
6247
6331
|
const latest = baseLabel(runningVersion, runningHash);
|
|
6248
6332
|
if (!isBaseTracked(lock, manifest) || !lock.base) {
|
|
6249
6333
|
return {
|
|
@@ -6256,13 +6340,18 @@ function compareBase(args) {
|
|
|
6256
6340
|
detail: "not recorded \u2014 run `saasaloy update` to adopt the base at this CLI"
|
|
6257
6341
|
};
|
|
6258
6342
|
}
|
|
6343
|
+
const moved = lock.base.templateHash !== runningHash;
|
|
6259
6344
|
return {
|
|
6260
6345
|
name: BASE_MODULE,
|
|
6261
6346
|
source: BASE_SOURCE,
|
|
6262
6347
|
ref: BASE_REF,
|
|
6263
6348
|
current: baseLabel(lock.base.cliVersion, lock.base.templateHash),
|
|
6264
6349
|
latest,
|
|
6265
|
-
status:
|
|
6350
|
+
status: moved || missing.length > 0 ? "outdated" : "current",
|
|
6351
|
+
// Say why, or an unmoved hash beside `outdated` reads as a bug.
|
|
6352
|
+
...!moved && missing.length > 0 ? {
|
|
6353
|
+
detail: `${missing.length} tracked file${missing.length === 1 ? " is" : "s are"} missing from disk`
|
|
6354
|
+
} : {}
|
|
6266
6355
|
};
|
|
6267
6356
|
}
|
|
6268
6357
|
function recordRefRewrites(lock, comparisons) {
|
|
@@ -6381,18 +6470,21 @@ async function planOneModule(args) {
|
|
|
6381
6470
|
const baseFiles = base ? await listModuleFiles(base, aliasView, installedSet) : void 0;
|
|
6382
6471
|
const files = [];
|
|
6383
6472
|
for (const ref of theirsFiles.values()) {
|
|
6384
|
-
const theirsContent = await
|
|
6473
|
+
const theirsContent = await readFile18(ref.abs, "utf-8");
|
|
6385
6474
|
const baseRef = baseFiles?.get(ref.target);
|
|
6386
|
-
const baseContent = baseRef ? await
|
|
6475
|
+
const baseContent = baseRef ? await readFile18(baseRef.abs, "utf-8") : void 0;
|
|
6387
6476
|
const targetAbs = resolveWithinRoot(root, ref.target);
|
|
6388
6477
|
const managed = manifest.managed[ref.target];
|
|
6389
6478
|
const owned = managed?.module === name ? managed : void 0;
|
|
6390
6479
|
const mine = await readIfPresent(targetAbs);
|
|
6480
|
+
const projectOwned = input.isOwned?.(ref.target) ?? false;
|
|
6391
6481
|
const action = classifyUpdate(
|
|
6392
6482
|
baseContent,
|
|
6393
6483
|
theirsContent,
|
|
6394
6484
|
mine,
|
|
6395
|
-
owned?.hash
|
|
6485
|
+
owned?.hash,
|
|
6486
|
+
owned?.adopted ?? false,
|
|
6487
|
+
projectOwned
|
|
6396
6488
|
);
|
|
6397
6489
|
const patchedBy = patchersOf(manifest, ref.target);
|
|
6398
6490
|
files.push({
|
|
@@ -6401,6 +6493,8 @@ async function planOneModule(args) {
|
|
|
6401
6493
|
target: ref.target,
|
|
6402
6494
|
targetAbs,
|
|
6403
6495
|
action,
|
|
6496
|
+
...projectOwned ? { projectOwned: true } : {},
|
|
6497
|
+
...owned?.adopted ? { adopted: true } : {},
|
|
6404
6498
|
base: baseContent,
|
|
6405
6499
|
theirs: theirsContent,
|
|
6406
6500
|
mine,
|
|
@@ -6416,13 +6510,14 @@ async function planOneModule(args) {
|
|
|
6416
6510
|
const targetAbs = resolveWithinRoot(root, target);
|
|
6417
6511
|
const mine = await readIfPresent(targetAbs);
|
|
6418
6512
|
const baseRef = baseFiles?.get(target);
|
|
6513
|
+
const keep = mine !== void 0 && ((entry.adopted ?? false) || (input.isOwned?.(target) ?? false));
|
|
6419
6514
|
removals.push({
|
|
6420
6515
|
module: name,
|
|
6421
6516
|
from: entry.from ?? baseRef?.from ?? target,
|
|
6422
6517
|
target,
|
|
6423
6518
|
targetAbs,
|
|
6424
|
-
action: DELETE_ACTION[classifyTrackedFile(mine, entry.hash)],
|
|
6425
|
-
base: baseRef ? await
|
|
6519
|
+
action: keep ? "delete-drift" : DELETE_ACTION[classifyTrackedFile(mine, entry.hash)],
|
|
6520
|
+
base: baseRef ? await readFile18(baseRef.abs, "utf-8") : void 0,
|
|
6426
6521
|
mine
|
|
6427
6522
|
});
|
|
6428
6523
|
}
|
|
@@ -6543,7 +6638,7 @@ function newEnvVars(base, theirs) {
|
|
|
6543
6638
|
}
|
|
6544
6639
|
return out;
|
|
6545
6640
|
}
|
|
6546
|
-
function classifyUpdate(base, theirs, mine, managedHash) {
|
|
6641
|
+
function classifyUpdate(base, theirs, mine, managedHash, adopted, projectOwned) {
|
|
6547
6642
|
if (base !== void 0 && base === theirs) {
|
|
6548
6643
|
return "skip";
|
|
6549
6644
|
}
|
|
@@ -6556,6 +6651,9 @@ function classifyUpdate(base, theirs, mine, managedHash) {
|
|
|
6556
6651
|
if (managedHash === void 0) {
|
|
6557
6652
|
return "conflict";
|
|
6558
6653
|
}
|
|
6654
|
+
if (adopted || projectOwned) {
|
|
6655
|
+
return "drift";
|
|
6656
|
+
}
|
|
6559
6657
|
return hashContent(mine) === managedHash ? "overwrite" : "drift";
|
|
6560
6658
|
}
|
|
6561
6659
|
async function planLinks(root, theirs) {
|
|
@@ -6711,7 +6809,7 @@ async function executeUpdatePlan(plan, args) {
|
|
|
6711
6809
|
clean = false;
|
|
6712
6810
|
continue;
|
|
6713
6811
|
}
|
|
6714
|
-
const source = await
|
|
6812
|
+
const source = await readFile18(p.fileAbs, "utf-8");
|
|
6715
6813
|
const { content, changed } = applyPatch(source, p.patch, p.file);
|
|
6716
6814
|
if (!changed) {
|
|
6717
6815
|
continue;
|
|
@@ -6923,11 +7021,13 @@ async function runOutdated(argv) {
|
|
|
6923
7021
|
`${REGISTRY_ENV} is set, so every module reads as local \u2014 unset it to compare against the registry.`
|
|
6924
7022
|
);
|
|
6925
7023
|
}
|
|
7024
|
+
const templateDir = await baseTemplateDir();
|
|
6926
7025
|
const baseRow = compareBase({
|
|
6927
7026
|
lock,
|
|
6928
7027
|
manifest,
|
|
6929
|
-
runningHash: await templateHash(
|
|
6930
|
-
runningVersion: await readVersion()
|
|
7028
|
+
runningHash: await templateHash(templateDir),
|
|
7029
|
+
runningVersion: await readVersion(),
|
|
7030
|
+
missingTargets: await missingBaseTargets(root, manifest, templateDir)
|
|
6931
7031
|
});
|
|
6932
7032
|
const resolvers = /* @__PURE__ */ new Map();
|
|
6933
7033
|
const remote = (slug, ref) => {
|
|
@@ -7333,8 +7433,8 @@ ${pc10.dim("Hand-edited since \u2014 left on disk, now untracked.")}`
|
|
|
7333
7433
|
}
|
|
7334
7434
|
|
|
7335
7435
|
// src/commands/update.ts
|
|
7336
|
-
import { mkdir as
|
|
7337
|
-
import { basename as basename4, dirname as
|
|
7436
|
+
import { mkdir as mkdir10, realpath, writeFile as writeFile13 } from "fs/promises";
|
|
7437
|
+
import { basename as basename4, dirname as dirname13, isAbsolute as isAbsolute4, join as join20, resolve as resolve8 } from "path";
|
|
7338
7438
|
import {
|
|
7339
7439
|
cancel as cancel9,
|
|
7340
7440
|
confirm as confirm3,
|
|
@@ -7346,6 +7446,145 @@ import {
|
|
|
7346
7446
|
} from "@clack/prompts";
|
|
7347
7447
|
import pc11 from "picocolors";
|
|
7348
7448
|
|
|
7449
|
+
// src/lib/backup.ts
|
|
7450
|
+
import {
|
|
7451
|
+
copyFile,
|
|
7452
|
+
mkdir as mkdir9,
|
|
7453
|
+
readdir as readdir6,
|
|
7454
|
+
readFile as readFile19,
|
|
7455
|
+
rm as rmPath3,
|
|
7456
|
+
writeFile as writeFile12
|
|
7457
|
+
} from "fs/promises";
|
|
7458
|
+
import { dirname as dirname12, join as join19 } from "path";
|
|
7459
|
+
var BACKUP_DIR = join19(".saasaloy", "backups");
|
|
7460
|
+
var RECORD_FILE = "backup.json";
|
|
7461
|
+
var STATE_FILES = [
|
|
7462
|
+
"saasaloy.json",
|
|
7463
|
+
"saasaloy-lock.json",
|
|
7464
|
+
"package.json",
|
|
7465
|
+
join19(".saasaloy", "manifest.json")
|
|
7466
|
+
];
|
|
7467
|
+
function stampName(now) {
|
|
7468
|
+
return now.toISOString().replaceAll(/[:.]/g, "-");
|
|
7469
|
+
}
|
|
7470
|
+
async function writeBackup(args) {
|
|
7471
|
+
const { root, targets, cliVersion } = args;
|
|
7472
|
+
const createdAt = (args.now ?? /* @__PURE__ */ new Date()).toISOString();
|
|
7473
|
+
const rel = join19(BACKUP_DIR, stampName(args.now ?? new Date(createdAt)));
|
|
7474
|
+
const dir = join19(root, rel);
|
|
7475
|
+
const files = [];
|
|
7476
|
+
const absent = [];
|
|
7477
|
+
for (const target of /* @__PURE__ */ new Set([...targets, ...STATE_FILES])) {
|
|
7478
|
+
const source = resolveWithinRoot(root, target);
|
|
7479
|
+
if (!await pathExists(source)) {
|
|
7480
|
+
absent.push(target);
|
|
7481
|
+
continue;
|
|
7482
|
+
}
|
|
7483
|
+
const destination = join19(dir, target);
|
|
7484
|
+
await mkdir9(dirname12(destination), { recursive: true });
|
|
7485
|
+
await copyFile(source, destination);
|
|
7486
|
+
files.push(target);
|
|
7487
|
+
}
|
|
7488
|
+
const record = {
|
|
7489
|
+
createdAt,
|
|
7490
|
+
cliVersion,
|
|
7491
|
+
files: files.toSorted(),
|
|
7492
|
+
absent: absent.toSorted()
|
|
7493
|
+
};
|
|
7494
|
+
await mkdir9(dir, { recursive: true });
|
|
7495
|
+
await writeFile12(
|
|
7496
|
+
join19(dir, RECORD_FILE),
|
|
7497
|
+
`${JSON.stringify(record, null, 2)}
|
|
7498
|
+
`,
|
|
7499
|
+
"utf-8"
|
|
7500
|
+
);
|
|
7501
|
+
return rel;
|
|
7502
|
+
}
|
|
7503
|
+
async function listBackups(root) {
|
|
7504
|
+
const dir = join19(root, BACKUP_DIR);
|
|
7505
|
+
if (!await pathExists(dir)) {
|
|
7506
|
+
return [];
|
|
7507
|
+
}
|
|
7508
|
+
const entries = await readdir6(dir, { withFileTypes: true });
|
|
7509
|
+
return entries.filter((entry) => entry.isDirectory()).map((entry) => entry.name).toSorted();
|
|
7510
|
+
}
|
|
7511
|
+
async function restoreLatestBackup(root) {
|
|
7512
|
+
const backups = await listBackups(root);
|
|
7513
|
+
const newest = backups.at(-1);
|
|
7514
|
+
if (!newest) {
|
|
7515
|
+
throw new RefusalError(
|
|
7516
|
+
`No backup to restore \u2014 nothing under ${BACKUP_DIR}. \`saasaloy update\` writes one before it applies a plan.`
|
|
7517
|
+
);
|
|
7518
|
+
}
|
|
7519
|
+
const rel = join19(BACKUP_DIR, newest);
|
|
7520
|
+
const dir = join19(root, rel);
|
|
7521
|
+
const raw = await readIfPresent(join19(dir, RECORD_FILE));
|
|
7522
|
+
if (raw === void 0) {
|
|
7523
|
+
throw new RefusalError(
|
|
7524
|
+
`${join19(rel, RECORD_FILE)} is missing \u2014 that backup can't be restored safely.`
|
|
7525
|
+
);
|
|
7526
|
+
}
|
|
7527
|
+
let record;
|
|
7528
|
+
try {
|
|
7529
|
+
record = JSON.parse(raw);
|
|
7530
|
+
} catch (error) {
|
|
7531
|
+
throw new RefusalError(`${join19(rel, RECORD_FILE)} is invalid.`, {
|
|
7532
|
+
cause: error
|
|
7533
|
+
});
|
|
7534
|
+
}
|
|
7535
|
+
const restored = [];
|
|
7536
|
+
for (const target of record.files) {
|
|
7537
|
+
const source = join19(dir, target);
|
|
7538
|
+
const destination = resolveWithinRoot(root, target);
|
|
7539
|
+
if (!await pathExists(source)) {
|
|
7540
|
+
continue;
|
|
7541
|
+
}
|
|
7542
|
+
await mkdir9(dirname12(destination), { recursive: true });
|
|
7543
|
+
await copyFile(source, destination);
|
|
7544
|
+
restored.push(target);
|
|
7545
|
+
}
|
|
7546
|
+
const removed = [];
|
|
7547
|
+
for (const target of record.absent) {
|
|
7548
|
+
const destination = resolveWithinRoot(root, target);
|
|
7549
|
+
if (await pathExists(destination)) {
|
|
7550
|
+
await rmPath3(destination, { force: true });
|
|
7551
|
+
removed.push(target);
|
|
7552
|
+
}
|
|
7553
|
+
}
|
|
7554
|
+
return { from: rel, restored, removed };
|
|
7555
|
+
}
|
|
7556
|
+
|
|
7557
|
+
// src/lib/git-state.ts
|
|
7558
|
+
import { execFile } from "child_process";
|
|
7559
|
+
async function workingTreeState(root) {
|
|
7560
|
+
return new Promise((resolve9) => {
|
|
7561
|
+
execFile(
|
|
7562
|
+
"git",
|
|
7563
|
+
// `-- .` scopes the answer to the project. Without it a project nested inside a
|
|
7564
|
+
// larger repository reports every unrelated change in that repository as its own.
|
|
7565
|
+
["status", "--porcelain", "--untracked-files=no", "--", "."],
|
|
7566
|
+
{ cwd: root },
|
|
7567
|
+
(error, stdout) => {
|
|
7568
|
+
resolve9(error ? "unknown" : stdout.trim() === "" ? "clean" : "dirty");
|
|
7569
|
+
}
|
|
7570
|
+
);
|
|
7571
|
+
});
|
|
7572
|
+
}
|
|
7573
|
+
|
|
7574
|
+
// src/lib/project-name.ts
|
|
7575
|
+
import { basename as basename3 } from "path";
|
|
7576
|
+
async function resolveProjectName(root, config) {
|
|
7577
|
+
if (config.name) {
|
|
7578
|
+
return { name: config.name, source: "config" };
|
|
7579
|
+
}
|
|
7580
|
+
const pkg = await readRootPackageJson(root);
|
|
7581
|
+
const pkgName = typeof pkg?.name === "string" ? pkg.name : void 0;
|
|
7582
|
+
if (pkgName) {
|
|
7583
|
+
return { name: pkgName, source: "package.json" };
|
|
7584
|
+
}
|
|
7585
|
+
return { name: basename3(root), source: "directory" };
|
|
7586
|
+
}
|
|
7587
|
+
|
|
7349
7588
|
// src/lib/merge-plan.ts
|
|
7350
7589
|
function renderMergePlan(plan) {
|
|
7351
7590
|
const modules = plan.modules.filter((m) => m.needsMerge);
|
|
@@ -7460,6 +7699,17 @@ function renderFile(file) {
|
|
|
7460
7699
|
""
|
|
7461
7700
|
);
|
|
7462
7701
|
}
|
|
7702
|
+
if (file.projectOwned) {
|
|
7703
|
+
out.push(
|
|
7704
|
+
"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.",
|
|
7705
|
+
""
|
|
7706
|
+
);
|
|
7707
|
+
} else if (file.adopted) {
|
|
7708
|
+
out.push(
|
|
7709
|
+
"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.",
|
|
7710
|
+
""
|
|
7711
|
+
);
|
|
7712
|
+
}
|
|
7463
7713
|
if (file.patchedBy && file.patchedBy.length > 0) {
|
|
7464
7714
|
out.push(
|
|
7465
7715
|
`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.`,
|
|
@@ -7572,11 +7822,13 @@ var KNOWN_FLAGS9 = /* @__PURE__ */ new Set([
|
|
|
7572
7822
|
"--diff",
|
|
7573
7823
|
"--yes",
|
|
7574
7824
|
"-y",
|
|
7825
|
+
"--force",
|
|
7826
|
+
"--abort",
|
|
7575
7827
|
"--help",
|
|
7576
7828
|
"-h"
|
|
7577
7829
|
]);
|
|
7578
7830
|
var VALUE_FLAGS2 = /* @__PURE__ */ new Set(["--ref", "--out"]);
|
|
7579
|
-
var USAGE9 = "saasaloy update [<module>|base] [--ref <ref>] [--out <path>] [--dry-run] [--diff] [--yes]";
|
|
7831
|
+
var USAGE9 = "saasaloy update [<module>|base] [--ref <ref>] [--out <path>] [--dry-run] [--diff] [--force] [--yes] | saasaloy update --abort";
|
|
7580
7832
|
var HELP9 = {
|
|
7581
7833
|
name: "update",
|
|
7582
7834
|
describe: DESCRIPTIONS.update,
|
|
@@ -7586,6 +7838,8 @@ var HELP9 = {
|
|
|
7586
7838
|
"--out <path>": "write the merge plan to a file instead of stdout",
|
|
7587
7839
|
"--dry-run": "show the plan and write nothing",
|
|
7588
7840
|
"--diff": "show a per-file diff and write nothing",
|
|
7841
|
+
"--force": "apply even with uncommitted changes in the working tree",
|
|
7842
|
+
"--abort": "restore the files the last update replaced, then stop",
|
|
7589
7843
|
"-y, --yes": "skip the confirmation prompt"
|
|
7590
7844
|
}
|
|
7591
7845
|
};
|
|
@@ -7625,6 +7879,8 @@ function parseArgs9(argv) {
|
|
|
7625
7879
|
dryRun: argv.includes("--dry-run"),
|
|
7626
7880
|
diff: argv.includes("--diff"),
|
|
7627
7881
|
yes: argv.includes("--yes") || argv.includes("-y"),
|
|
7882
|
+
force: argv.includes("--force"),
|
|
7883
|
+
abort: argv.includes("--abort"),
|
|
7628
7884
|
unknown
|
|
7629
7885
|
};
|
|
7630
7886
|
}
|
|
@@ -7785,13 +8041,13 @@ function skipReason(comparison, preview = false) {
|
|
|
7785
8041
|
}
|
|
7786
8042
|
}
|
|
7787
8043
|
}
|
|
7788
|
-
var
|
|
8044
|
+
var STATE_FILES2 = [CONFIG_FILE, LOCK_FILE, MANIFEST_FILE];
|
|
7789
8045
|
async function stateFileRefusal(root, out) {
|
|
7790
8046
|
if (!out) {
|
|
7791
8047
|
return void 0;
|
|
7792
8048
|
}
|
|
7793
8049
|
const target = await canonicalPath(resolvePlanOut(out));
|
|
7794
|
-
for (const file of
|
|
8050
|
+
for (const file of STATE_FILES2) {
|
|
7795
8051
|
if (await canonicalPath(resolve8(root, file)) === target) {
|
|
7796
8052
|
return `Refusing to write the merge plan to ${pc11.cyan(out)} \u2014 that is Saasaloy's own ${pc11.cyan(file)}. Pick another path.`;
|
|
7797
8053
|
}
|
|
@@ -7803,7 +8059,7 @@ async function canonicalPath(path) {
|
|
|
7803
8059
|
return await realpath(path);
|
|
7804
8060
|
} catch {
|
|
7805
8061
|
try {
|
|
7806
|
-
return
|
|
8062
|
+
return join20(await realpath(dirname13(path)), basename4(path));
|
|
7807
8063
|
} catch {
|
|
7808
8064
|
return path;
|
|
7809
8065
|
}
|
|
@@ -7818,10 +8074,24 @@ async function emitMergePlan(document, out) {
|
|
|
7818
8074
|
return;
|
|
7819
8075
|
}
|
|
7820
8076
|
const target = resolvePlanOut(out);
|
|
7821
|
-
await
|
|
7822
|
-
await
|
|
8077
|
+
await mkdir10(dirname13(target), { recursive: true });
|
|
8078
|
+
await writeFile13(target, document, "utf-8");
|
|
7823
8079
|
log7.success(`Merge plan written to ${pc11.cyan(out)}`, TUI_ON_STDERR);
|
|
7824
8080
|
}
|
|
8081
|
+
function backupTargets(plan) {
|
|
8082
|
+
const targets = /* @__PURE__ */ new Set();
|
|
8083
|
+
for (const mod of plan.modules) {
|
|
8084
|
+
for (const file of [...mod.files, ...mod.removals]) {
|
|
8085
|
+
if (!QUIET.has(file.action) && file.action !== "skip") {
|
|
8086
|
+
targets.add(file.target);
|
|
8087
|
+
}
|
|
8088
|
+
}
|
|
8089
|
+
for (const patch of mod.patches) {
|
|
8090
|
+
targets.add(patch.file);
|
|
8091
|
+
}
|
|
8092
|
+
}
|
|
8093
|
+
return [...targets].toSorted();
|
|
8094
|
+
}
|
|
7825
8095
|
function splitSlug2(slug) {
|
|
7826
8096
|
const [owner, repo, ...rest] = slug.split("/");
|
|
7827
8097
|
if (!owner || !repo || rest.length > 0) {
|
|
@@ -7859,12 +8129,43 @@ async function runUpdate(argv) {
|
|
|
7859
8129
|
cancel9(formatFailure(error), TUI_ON_STDERR);
|
|
7860
8130
|
return exitCodeFor(error);
|
|
7861
8131
|
}
|
|
8132
|
+
if (opts.abort) {
|
|
8133
|
+
try {
|
|
8134
|
+
const restored = await restoreLatestBackup(root);
|
|
8135
|
+
for (const target of restored.restored) {
|
|
8136
|
+
log7.step(`${pc11.green("restore")} ${target}`, TUI_ON_STDERR);
|
|
8137
|
+
}
|
|
8138
|
+
for (const target of restored.removed) {
|
|
8139
|
+
log7.step(
|
|
8140
|
+
`${pc11.red("remove")} ${target} ${pc11.dim("(the update created it)")}`,
|
|
8141
|
+
TUI_ON_STDERR
|
|
8142
|
+
);
|
|
8143
|
+
}
|
|
8144
|
+
outro9(
|
|
8145
|
+
pc11.green(
|
|
8146
|
+
`Restored ${restored.restored.length} file${restored.restored.length === 1 ? "" : "s"} from ${restored.from}.`
|
|
8147
|
+
),
|
|
8148
|
+
TUI_ON_STDERR
|
|
8149
|
+
);
|
|
8150
|
+
return EXIT_OK;
|
|
8151
|
+
} catch (error) {
|
|
8152
|
+
cancel9(formatFailure(error), TUI_ON_STDERR);
|
|
8153
|
+
return exitCodeFor(error);
|
|
8154
|
+
}
|
|
8155
|
+
}
|
|
7862
8156
|
const outRefusal = await stateFileRefusal(root, opts.out);
|
|
7863
8157
|
if (outRefusal) {
|
|
7864
8158
|
cancel9(outRefusal, TUI_ON_STDERR);
|
|
7865
8159
|
return EXIT_REFUSED;
|
|
7866
8160
|
}
|
|
7867
8161
|
const preview = opts.dryRun || opts.diff;
|
|
8162
|
+
if (!preview && !opts.force && await workingTreeState(root) === "dirty") {
|
|
8163
|
+
cancel9(
|
|
8164
|
+
"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.",
|
|
8165
|
+
TUI_ON_STDERR
|
|
8166
|
+
);
|
|
8167
|
+
return EXIT_REFUSED;
|
|
8168
|
+
}
|
|
7868
8169
|
if (!opts.yes && !preview && !process.stdin.isTTY) {
|
|
7869
8170
|
cancel9(
|
|
7870
8171
|
"No terminal to confirm in \u2014 re-run with `--yes` to apply, or `--dry-run` to preview.",
|
|
@@ -7898,12 +8199,20 @@ async function runUpdate(argv) {
|
|
|
7898
8199
|
const cliVersion = await readVersion();
|
|
7899
8200
|
let baseComparison;
|
|
7900
8201
|
let templateDir;
|
|
8202
|
+
const project = await resolveProjectName(root, config);
|
|
7901
8203
|
if (wantsBase) {
|
|
7902
8204
|
templateDir = await baseTemplateDir();
|
|
8205
|
+
if (project.source === "directory") {
|
|
8206
|
+
log7.warn(
|
|
8207
|
+
`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.`,
|
|
8208
|
+
TUI_ON_STDERR
|
|
8209
|
+
);
|
|
8210
|
+
}
|
|
7903
8211
|
if (!isBaseTracked(lock, manifest)) {
|
|
7904
8212
|
const adoption = await adoptBase({
|
|
7905
8213
|
root,
|
|
7906
8214
|
cliVersion,
|
|
8215
|
+
projectName: project.name,
|
|
7907
8216
|
manifest,
|
|
7908
8217
|
lock,
|
|
7909
8218
|
templateDir,
|
|
@@ -7911,7 +8220,7 @@ async function runUpdate(argv) {
|
|
|
7911
8220
|
});
|
|
7912
8221
|
const count = adoption.adopted.length;
|
|
7913
8222
|
const lines = [
|
|
7914
|
-
`${preview ? "Would adopt" : "Adopted"} ${count} base file${count === 1 ? "" : "s"} at CLI ${pc11.cyan(cliVersion)}, each at the hash it has on disk
|
|
8223
|
+
`${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.`
|
|
7915
8224
|
];
|
|
7916
8225
|
if (adoption.absent.length > 0) {
|
|
7917
8226
|
lines.push(
|
|
@@ -7953,7 +8262,8 @@ async function runUpdate(argv) {
|
|
|
7953
8262
|
lock,
|
|
7954
8263
|
manifest,
|
|
7955
8264
|
runningHash: await templateHash(templateDir),
|
|
7956
|
-
runningVersion: cliVersion
|
|
8265
|
+
runningVersion: cliVersion,
|
|
8266
|
+
missingTargets: await missingBaseTargets(root, manifest, templateDir)
|
|
7957
8267
|
});
|
|
7958
8268
|
}
|
|
7959
8269
|
if (targets.length === 0 && !baseComparison) {
|
|
@@ -8026,6 +8336,7 @@ async function runUpdate(argv) {
|
|
|
8026
8336
|
lock,
|
|
8027
8337
|
templateDir,
|
|
8028
8338
|
cliVersion,
|
|
8339
|
+
projectName: project.name,
|
|
8029
8340
|
comparison: baseComparison
|
|
8030
8341
|
});
|
|
8031
8342
|
cleanups.push(handle.cleanup);
|
|
@@ -8210,6 +8521,15 @@ async function runUpdate(argv) {
|
|
|
8210
8521
|
return EXIT_OK;
|
|
8211
8522
|
}
|
|
8212
8523
|
}
|
|
8524
|
+
const backupDir = await writeBackup({
|
|
8525
|
+
root,
|
|
8526
|
+
targets: backupTargets(plan),
|
|
8527
|
+
cliVersion
|
|
8528
|
+
});
|
|
8529
|
+
log7.info(
|
|
8530
|
+
`Pre-update copies in ${pc11.cyan(backupDir)} ${pc11.dim("\u2014 `saasaloy update --abort` restores them")}`,
|
|
8531
|
+
TUI_ON_STDERR
|
|
8532
|
+
);
|
|
8213
8533
|
const outcome = await executeUpdatePlan(plan, {
|
|
8214
8534
|
root,
|
|
8215
8535
|
config,
|
|
@@ -8248,7 +8568,7 @@ async function runUpdate(argv) {
|
|
|
8248
8568
|
);
|
|
8249
8569
|
}
|
|
8250
8570
|
outro9(summarizeOutcome(plan, result), TUI_ON_STDERR);
|
|
8251
|
-
return EXIT_OK;
|
|
8571
|
+
return plan.needsMerge ? EXIT_MERGE_PENDING : EXIT_OK;
|
|
8252
8572
|
} catch (error) {
|
|
8253
8573
|
cancel9(formatFailure(error), TUI_ON_STDERR);
|
|
8254
8574
|
return exitCodeFor(error);
|