@anvil-works/anvil-cli 0.7.0-canary.11 → 0.7.0-canary.13
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/WatchSession.d.ts +6 -0
- package/dist/WatchSession.d.ts.map +1 -1
- package/dist/api.d.ts +2 -0
- package/dist/api.d.ts.map +1 -1
- package/dist/cli.js +576 -29
- package/dist/commands/gitAuth.d.ts +89 -0
- package/dist/commands/gitAuth.d.ts.map +1 -0
- package/dist/commands/gitCredential.d.ts.map +1 -1
- package/dist/commands/index.d.ts +1 -0
- package/dist/commands/index.d.ts.map +1 -1
- package/dist/index.js +559 -28
- package/dist/program.d.ts.map +1 -1
- package/dist/services/git-auth.d.ts.map +1 -1
- package/dist/services/git.d.ts +25 -1
- package/dist/services/git.d.ts.map +1 -1
- package/dist/watch/SaveProcessor.d.ts +6 -1
- package/dist/watch/SaveProcessor.d.ts.map +1 -1
- package/dist/watch/SyncManager.d.ts +4 -0
- package/dist/watch/SyncManager.d.ts.map +1 -1
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -13718,7 +13718,7 @@ Promise.resolve(executeGitCredentialOperation(process.argv[2] || "get", {
|
|
|
13718
13718
|
}
|
|
13719
13719
|
function getCredentialHelperModulePath(options) {
|
|
13720
13720
|
const currentModulePath = options?.currentModulePath ?? __filename;
|
|
13721
|
-
if (isAnvilCliPackageFile(currentModulePath)) return currentModulePath;
|
|
13721
|
+
if (isAnvilCliPackageFile(currentModulePath) && "cli.js" !== external_path_default().basename(currentModulePath)) return currentModulePath;
|
|
13722
13722
|
return (options?.resolvePackageEntry ?? (()=>requireFromHere.resolve("@anvil-works/anvil-cli")))();
|
|
13723
13723
|
}
|
|
13724
13724
|
async function getRepositoryGitDir(repoPath, git = esm_default(repoPath)) {
|
|
@@ -14689,6 +14689,77 @@ Promise.resolve(executeGitCredentialOperation(process.argv[2] || "get", {
|
|
|
14689
14689
|
throw errors_createGitError.commandFailed("status", e.message);
|
|
14690
14690
|
}
|
|
14691
14691
|
}
|
|
14692
|
+
async discardModeOnlyChanges(paths) {
|
|
14693
|
+
const discarded = [];
|
|
14694
|
+
for (const relativePath of paths)try {
|
|
14695
|
+
const headMode = await this.getHeadFileMode(relativePath);
|
|
14696
|
+
if ("100644" !== headMode && "100755" !== headMode) continue;
|
|
14697
|
+
const contentUnchanged = await this.hasSameContentAsHead(relativePath);
|
|
14698
|
+
if (!contentUnchanged) continue;
|
|
14699
|
+
await this.restoreExecutableBit(relativePath, "100755" === headMode);
|
|
14700
|
+
discarded.push(relativePath);
|
|
14701
|
+
} catch (e) {
|
|
14702
|
+
throw errors_createGitError.commandFailed("discard mode-only changes", e.message);
|
|
14703
|
+
}
|
|
14704
|
+
return discarded;
|
|
14705
|
+
}
|
|
14706
|
+
async discardStagedModeOnlyChanges(paths) {
|
|
14707
|
+
const discarded = [];
|
|
14708
|
+
for (const relativePath of paths)try {
|
|
14709
|
+
const headMode = await this.getHeadFileMode(relativePath);
|
|
14710
|
+
if ("100644" !== headMode && "100755" !== headMode) continue;
|
|
14711
|
+
const indexContentUnchanged = await this.hasSameIndexContentAsHead(relativePath);
|
|
14712
|
+
if (!indexContentUnchanged) continue;
|
|
14713
|
+
await this.git.raw([
|
|
14714
|
+
"reset",
|
|
14715
|
+
"-q",
|
|
14716
|
+
"HEAD",
|
|
14717
|
+
"--",
|
|
14718
|
+
relativePath
|
|
14719
|
+
]);
|
|
14720
|
+
await this.restoreExecutableBit(relativePath, "100755" === headMode);
|
|
14721
|
+
discarded.push(relativePath);
|
|
14722
|
+
} catch (e) {
|
|
14723
|
+
throw errors_createGitError.commandFailed("discard staged mode-only changes", e.message);
|
|
14724
|
+
}
|
|
14725
|
+
return discarded;
|
|
14726
|
+
}
|
|
14727
|
+
async getHeadFileMode(relativePath) {
|
|
14728
|
+
const output = await this.git.raw([
|
|
14729
|
+
"ls-tree",
|
|
14730
|
+
"HEAD",
|
|
14731
|
+
"--",
|
|
14732
|
+
relativePath
|
|
14733
|
+
]);
|
|
14734
|
+
const match = output.match(/^(\d{6})\s/);
|
|
14735
|
+
return match?.[1] ?? null;
|
|
14736
|
+
}
|
|
14737
|
+
async hasSameContentAsHead(relativePath) {
|
|
14738
|
+
const headHash = (await this.git.revparse([
|
|
14739
|
+
`HEAD:${relativePath}`
|
|
14740
|
+
])).trim();
|
|
14741
|
+
const worktreeHash = (await this.git.raw([
|
|
14742
|
+
"hash-object",
|
|
14743
|
+
"--",
|
|
14744
|
+
relativePath
|
|
14745
|
+
])).trim();
|
|
14746
|
+
return headHash === worktreeHash;
|
|
14747
|
+
}
|
|
14748
|
+
async hasSameIndexContentAsHead(relativePath) {
|
|
14749
|
+
const headHash = (await this.git.revparse([
|
|
14750
|
+
`HEAD:${relativePath}`
|
|
14751
|
+
])).trim();
|
|
14752
|
+
const indexHash = (await this.git.revparse([
|
|
14753
|
+
`:${relativePath}`
|
|
14754
|
+
])).trim();
|
|
14755
|
+
return headHash === indexHash;
|
|
14756
|
+
}
|
|
14757
|
+
async restoreExecutableBit(relativePath, executable) {
|
|
14758
|
+
const filePath = external_path_default().join(this.repoPath, relativePath);
|
|
14759
|
+
const stat = await external_fs_.promises.stat(filePath);
|
|
14760
|
+
const mode = executable ? 73 | stat.mode : -74 & stat.mode;
|
|
14761
|
+
if ((511 & stat.mode) !== (511 & mode)) await external_fs_.promises.chmod(filePath, mode);
|
|
14762
|
+
}
|
|
14692
14763
|
async hasUncommittedChanges() {
|
|
14693
14764
|
const status = await this.getStatus();
|
|
14694
14765
|
return !status.isClean;
|
|
@@ -14828,6 +14899,32 @@ Promise.resolve(executeGitCredentialOperation(process.argv[2] || "get", {
|
|
|
14828
14899
|
throw errors_createGitError.commandFailed("rebase --abort", e.message);
|
|
14829
14900
|
}
|
|
14830
14901
|
}
|
|
14902
|
+
async isAncestor(ancestorRef, descendantRef) {
|
|
14903
|
+
try {
|
|
14904
|
+
const mergeBase = (await this.git.raw([
|
|
14905
|
+
"merge-base",
|
|
14906
|
+
ancestorRef,
|
|
14907
|
+
descendantRef
|
|
14908
|
+
])).trim();
|
|
14909
|
+
const ancestorCommit = (await this.git.revparse([
|
|
14910
|
+
ancestorRef
|
|
14911
|
+
])).trim();
|
|
14912
|
+
return mergeBase === ancestorCommit;
|
|
14913
|
+
} catch {
|
|
14914
|
+
return false;
|
|
14915
|
+
}
|
|
14916
|
+
}
|
|
14917
|
+
async mergeFastForward(ref) {
|
|
14918
|
+
try {
|
|
14919
|
+
await this.git.raw([
|
|
14920
|
+
"merge",
|
|
14921
|
+
"--ff-only",
|
|
14922
|
+
ref
|
|
14923
|
+
]);
|
|
14924
|
+
} catch (e) {
|
|
14925
|
+
throw errors_createGitError.commandFailed("merge --ff-only", e.message);
|
|
14926
|
+
}
|
|
14927
|
+
}
|
|
14831
14928
|
async deleteRef(ref) {
|
|
14832
14929
|
try {
|
|
14833
14930
|
await this.git.raw([
|
|
@@ -14872,11 +14969,12 @@ Promise.resolve(executeGitCredentialOperation(process.argv[2] || "get", {
|
|
|
14872
14969
|
} catch (error) {}
|
|
14873
14970
|
}
|
|
14874
14971
|
}
|
|
14875
|
-
async function getStagedFileChanges(git) {
|
|
14972
|
+
async function getStagedFileChanges(git, gitService) {
|
|
14876
14973
|
try {
|
|
14877
14974
|
const status = await git.status();
|
|
14878
14975
|
const renames = status.renamed || [];
|
|
14879
14976
|
const stagedFiles = status.files.filter((f)=>" " !== f.index && "?" !== f.index);
|
|
14977
|
+
const stagedModeOnlyPaths = new Set(gitService ? await gitService.discardStagedModeOnlyChanges(stagedFiles.filter((f)=>"R" !== f.index && "D" !== f.index).map((f)=>f.path)) : []);
|
|
14880
14978
|
const stagedRenames = [];
|
|
14881
14979
|
for (const f of stagedFiles)if ("R" === f.index) {
|
|
14882
14980
|
const rename = renames.find((r)=>r.to === f.path);
|
|
@@ -14889,19 +14987,23 @@ Promise.resolve(executeGitCredentialOperation(process.argv[2] || "get", {
|
|
|
14889
14987
|
const stagedRenamedFromPaths = new Set(stagedRenames.map((r)=>r.from));
|
|
14890
14988
|
const stagedRenamedToPaths = new Set(stagedRenames.map((r)=>r.path));
|
|
14891
14989
|
const changes = [];
|
|
14892
|
-
for (const f of stagedFiles)
|
|
14893
|
-
if (
|
|
14894
|
-
|
|
14895
|
-
|
|
14896
|
-
|
|
14897
|
-
|
|
14898
|
-
|
|
14899
|
-
|
|
14900
|
-
|
|
14901
|
-
|
|
14902
|
-
|
|
14903
|
-
|
|
14904
|
-
|
|
14990
|
+
for (const f of stagedFiles){
|
|
14991
|
+
if ("R" !== f.index) {
|
|
14992
|
+
if (!stagedModeOnlyPaths.has(f.path)) {
|
|
14993
|
+
if (!(stagedRenamedFromPaths.has(f.path) || stagedRenamedToPaths.has(f.path))) if ("D" === f.index) changes.push({
|
|
14994
|
+
path: f.path,
|
|
14995
|
+
type: "unlink"
|
|
14996
|
+
});
|
|
14997
|
+
else if ("A" === f.index || "?" === f.index) changes.push({
|
|
14998
|
+
path: f.path,
|
|
14999
|
+
type: "add"
|
|
15000
|
+
});
|
|
15001
|
+
else changes.push({
|
|
15002
|
+
path: f.path,
|
|
15003
|
+
type: "change"
|
|
15004
|
+
});
|
|
15005
|
+
}
|
|
15006
|
+
}
|
|
14905
15007
|
}
|
|
14906
15008
|
changes.push(...stagedRenames);
|
|
14907
15009
|
return changes;
|
|
@@ -17657,13 +17759,14 @@ Promise.resolve(executeGitCredentialOperation(process.argv[2] || "get", {
|
|
|
17657
17759
|
}
|
|
17658
17760
|
async getStagedChanges() {
|
|
17659
17761
|
try {
|
|
17660
|
-
return await getStagedFileChanges(this.config.gitService.getGit());
|
|
17762
|
+
return await getStagedFileChanges(this.config.gitService.getGit(), this.config.gitService);
|
|
17661
17763
|
} catch (e) {
|
|
17662
17764
|
logger_logger.error(external_chalk_default().red(`Error getting staged changes: ${errors_getErrorMessage(e)}`));
|
|
17663
17765
|
return [];
|
|
17664
17766
|
}
|
|
17665
17767
|
}
|
|
17666
17768
|
async getUnstagedChanges(status) {
|
|
17769
|
+
const modeOnlyChanges = new Set(await this.config.gitService.discardModeOnlyChanges(status.modified));
|
|
17667
17770
|
const renames = status.renamed;
|
|
17668
17771
|
const stagedRenamedFromPaths = new Set(renames.map((r)=>r.from));
|
|
17669
17772
|
const stagedRenamedToPaths = new Set(renames.map((r)=>r.to));
|
|
@@ -17683,7 +17786,7 @@ Promise.resolve(executeGitCredentialOperation(process.argv[2] || "get", {
|
|
|
17683
17786
|
const allRenamedFromPaths = new Set(allRenames.map((r)=>r.from));
|
|
17684
17787
|
const allRenamedToPaths = new Set(allRenames.map((r)=>r.to));
|
|
17685
17788
|
return [
|
|
17686
|
-
...status.modified.map((f)=>({
|
|
17789
|
+
...status.modified.filter((f)=>!modeOnlyChanges.has(f)).map((f)=>({
|
|
17687
17790
|
path: f,
|
|
17688
17791
|
type: "change"
|
|
17689
17792
|
})),
|
|
@@ -17891,7 +17994,7 @@ Promise.resolve(executeGitCredentialOperation(process.argv[2] || "get", {
|
|
|
17891
17994
|
await this.handleConflict(json_resp, originalFilePaths, retryCount);
|
|
17892
17995
|
return;
|
|
17893
17996
|
}
|
|
17894
|
-
if (200 === resp.status) return void await this.handleSuccess(json_resp);
|
|
17997
|
+
if (200 === resp.status) return void await this.handleSuccess(json_resp, originalFilePaths);
|
|
17895
17998
|
logger_logger.error(`Unexpected response status: ${resp.status}`);
|
|
17896
17999
|
throw {
|
|
17897
18000
|
type: "server_error",
|
|
@@ -17994,13 +18097,20 @@ Promise.resolve(executeGitCredentialOperation(process.argv[2] || "get", {
|
|
|
17994
18097
|
reason: "Unknown conflict"
|
|
17995
18098
|
};
|
|
17996
18099
|
}
|
|
17997
|
-
async handleSuccess(json_resp) {
|
|
18100
|
+
async handleSuccess(json_resp, originalFilePaths) {
|
|
17998
18101
|
const version = json_resp.version;
|
|
17999
|
-
|
|
18102
|
+
const oldCommitId = json_resp.previous_commit ?? null;
|
|
18103
|
+
const commitIdBeforePostSaveSync = this.config.getCommitId();
|
|
18104
|
+
this.emit("saved", {
|
|
18105
|
+
oldCommitId,
|
|
18106
|
+
newCommitId: version,
|
|
18107
|
+
branch: this.config.getCurrentBranch()
|
|
18108
|
+
});
|
|
18000
18109
|
if (this.postSaveCallback) {
|
|
18001
18110
|
logger_logger.progressUpdate("sync", "Syncing back");
|
|
18002
|
-
await this.postSaveCallback();
|
|
18111
|
+
await this.postSaveCallback(new Set(originalFilePaths));
|
|
18003
18112
|
}
|
|
18113
|
+
if (this.config.getCommitId() === commitIdBeforePostSaveSync) this.config.setCommitId(version);
|
|
18004
18114
|
}
|
|
18005
18115
|
async rerouteChanges(changes) {
|
|
18006
18116
|
logger_logger.verbose(external_chalk_default().blue(`Re-routing ${changes.length} local change(s)...`));
|
|
@@ -18071,12 +18181,22 @@ Promise.resolve(executeGitCredentialOperation(process.argv[2] || "get", {
|
|
|
18071
18181
|
try {
|
|
18072
18182
|
const oldCommitId = this.config.getCommitId();
|
|
18073
18183
|
let locallyChangedFiles;
|
|
18184
|
+
let initialStatus;
|
|
18074
18185
|
if (knownLocalChanges) locallyChangedFiles = knownLocalChanges;
|
|
18075
18186
|
else {
|
|
18076
|
-
|
|
18077
|
-
locallyChangedFiles = buildLocalChangesSet(
|
|
18187
|
+
initialStatus = await this.config.gitService.getStatus();
|
|
18188
|
+
locallyChangedFiles = buildLocalChangesSet(initialStatus);
|
|
18078
18189
|
}
|
|
18079
|
-
|
|
18190
|
+
let syncMode;
|
|
18191
|
+
if (!knownLocalChanges && initialStatus && this.isCleanForFastForward(initialStatus)) syncMode = await this.fetchAndMoveCleanTreeToAnvilHead(oldCommitId, locallyChangedFiles);
|
|
18192
|
+
else {
|
|
18193
|
+
await this.fetchAndResetFromAnvil(locallyChangedFiles);
|
|
18194
|
+
syncMode = "preservation";
|
|
18195
|
+
}
|
|
18196
|
+
if ("clean-move" === syncMode) return {
|
|
18197
|
+
localOnlyChanges: [],
|
|
18198
|
+
conflicts: []
|
|
18199
|
+
};
|
|
18080
18200
|
const newCommitId = await this.config.gitService.getCommitId();
|
|
18081
18201
|
this.config.setCommitId(newCommitId);
|
|
18082
18202
|
const remoteChangedFiles = await detectRemoteChanges(this.config.gitService, oldCommitId, newCommitId);
|
|
@@ -18101,15 +18221,43 @@ Promise.resolve(executeGitCredentialOperation(process.argv[2] || "get", {
|
|
|
18101
18221
|
}
|
|
18102
18222
|
}
|
|
18103
18223
|
async fetchAndResetFromAnvil(localChangesToPreserve = new Set()) {
|
|
18224
|
+
const oldCommitId = this.config.getCommitId();
|
|
18225
|
+
await this.withFetchedAnvilRef((tempRef)=>this.resetToFetchedAnvilRef(tempRef, oldCommitId, localChangesToPreserve));
|
|
18226
|
+
}
|
|
18227
|
+
async fetchAndMoveCleanTreeToAnvilHead(oldCommitId, locallyChangedFiles) {
|
|
18228
|
+
return this.withFetchedAnvilRef(async (tempRef)=>{
|
|
18229
|
+
const localHead = await this.config.gitService.getCommitId();
|
|
18230
|
+
if (await this.config.gitService.isAncestor(localHead, tempRef)) await this.config.gitService.mergeFastForward(tempRef);
|
|
18231
|
+
else {
|
|
18232
|
+
const statusBeforeHardReset = await this.config.gitService.getStatus();
|
|
18233
|
+
if (!this.isCleanForFastForward(statusBeforeHardReset)) {
|
|
18234
|
+
for (const filePath of buildLocalChangesSet(statusBeforeHardReset))locallyChangedFiles.add(filePath);
|
|
18235
|
+
await this.resetToFetchedAnvilRef(tempRef, oldCommitId, locallyChangedFiles);
|
|
18236
|
+
return "preservation";
|
|
18237
|
+
}
|
|
18238
|
+
await this.config.gitService.reset(tempRef, "hard");
|
|
18239
|
+
}
|
|
18240
|
+
const newCommitId = await this.config.gitService.getCommitId();
|
|
18241
|
+
this.config.setCommitId(newCommitId);
|
|
18242
|
+
await this.config.editorYaml.reload();
|
|
18243
|
+
return "clean-move";
|
|
18244
|
+
});
|
|
18245
|
+
}
|
|
18246
|
+
async withFetchedAnvilRef(callback) {
|
|
18104
18247
|
const validToken = await auth_getValidAuthToken(this.config.anvilUrl, this.config.username);
|
|
18105
18248
|
this.config.setAuthToken(validToken);
|
|
18106
18249
|
const httpUrl = getGitFetchUrl(this.config.appId, this.config.getAuthToken(), this.config.anvilUrl);
|
|
18107
18250
|
const currentBranch = this.config.getCurrentBranch();
|
|
18108
18251
|
const tempRef = `anvil-sync-temp-${Date.now()}`;
|
|
18109
|
-
|
|
18110
|
-
|
|
18252
|
+
try {
|
|
18253
|
+
await this.config.gitService.fetch(httpUrl, `+${currentBranch}:${tempRef}`);
|
|
18254
|
+
return await callback(tempRef);
|
|
18255
|
+
} finally{
|
|
18256
|
+
await this.config.gitService.deleteRef(`refs/heads/${tempRef}`);
|
|
18257
|
+
}
|
|
18258
|
+
}
|
|
18259
|
+
async resetToFetchedAnvilRef(tempRef, oldCommitId, localChangesToPreserve) {
|
|
18111
18260
|
await this.config.gitService.reset(tempRef, "mixed");
|
|
18112
|
-
await this.config.gitService.deleteRef(`refs/heads/${tempRef}`);
|
|
18113
18261
|
const newCommitId = await this.config.gitService.getCommitId();
|
|
18114
18262
|
await this.config.gitService.checkout([
|
|
18115
18263
|
".anvil_editor.yaml",
|
|
@@ -18119,6 +18267,9 @@ Promise.resolve(executeGitCredentialOperation(process.argv[2] || "get", {
|
|
|
18119
18267
|
await this.deleteFilesRemovedOnAnvilLocally(oldCommitId, newCommitId, localChangesToPreserve);
|
|
18120
18268
|
await this.discardFormattingOnlyYamlChanges();
|
|
18121
18269
|
}
|
|
18270
|
+
isCleanForFastForward(status) {
|
|
18271
|
+
return status.isClean && 0 === status.modified.length && 0 === status.notAdded.length && 0 === status.created.length && 0 === status.deleted.length && 0 === status.staged.length && 0 === status.renamed.length;
|
|
18272
|
+
}
|
|
18122
18273
|
async deleteFilesRemovedOnAnvilLocally(oldCommitId, newCommitId, localChangesToPreserve) {
|
|
18123
18274
|
try {
|
|
18124
18275
|
const status = await this.config.gitService.getStatus();
|
|
@@ -18332,11 +18483,12 @@ Promise.resolve(executeGitCredentialOperation(process.argv[2] || "get", {
|
|
|
18332
18483
|
isPaused: ()=>this.isPausedForUserInput
|
|
18333
18484
|
});
|
|
18334
18485
|
this.saveProcessor.setSyncCallback((filePaths)=>this.syncManager.syncRemoteChanges(filePaths));
|
|
18335
|
-
this.saveProcessor.setPostSaveCallback(()=>this.
|
|
18486
|
+
this.saveProcessor.setPostSaveCallback((filePaths)=>this.syncAfterSave(filePaths));
|
|
18336
18487
|
this.saveProcessor.on("save-started", (data)=>this.emit("save-started", data));
|
|
18337
18488
|
this.saveProcessor.on("save-succeeded", (data)=>this.emit("save-succeeded", data));
|
|
18338
18489
|
this.saveProcessor.on("save-failed", (data)=>this.emit("save-failed", data));
|
|
18339
18490
|
this.saveProcessor.on("save-complete", (data)=>this.emit("save-complete", data));
|
|
18491
|
+
this.saveProcessor.on("saved", (data)=>this.emit("saved", data));
|
|
18340
18492
|
this.saveProcessor.on("anvil-yaml-dependencies-changed", ()=>{
|
|
18341
18493
|
this.queueDependencyRefresh("anvil.yaml dependencies changed");
|
|
18342
18494
|
});
|
|
@@ -18344,6 +18496,12 @@ Promise.resolve(executeGitCredentialOperation(process.argv[2] || "get", {
|
|
|
18344
18496
|
this.saveProcessor.on("sync-conflict", (data)=>this.emit("sync-conflict", data));
|
|
18345
18497
|
this.saveProcessor.on("max-retries-exceeded", (data)=>this.emit("max-retries-exceeded", data));
|
|
18346
18498
|
}
|
|
18499
|
+
async syncAfterSave(savedFilePaths) {
|
|
18500
|
+
const status = await this.gitService.getStatus();
|
|
18501
|
+
const locallyChangedFiles = buildLocalChangesSet(status);
|
|
18502
|
+
if (!this.stagedOnly) for (const filePath of savedFilePaths)locallyChangedFiles.add(filePath);
|
|
18503
|
+
return this.syncManager.syncRemoteChanges(locallyChangedFiles);
|
|
18504
|
+
}
|
|
18347
18505
|
async connectWebSocket() {
|
|
18348
18506
|
this.wsClient = new WebSocketClient({
|
|
18349
18507
|
appId: this.appId,
|
|
@@ -20170,12 +20328,396 @@ Promise.resolve(executeGitCredentialOperation(process.argv[2] || "get", {
|
|
|
20170
20328
|
});
|
|
20171
20329
|
checkoutCommand.addHelpText("after", "\n" + external_chalk_default().bold("Examples:") + '\n anvil checkout http://localhost:3000/build/apps/APPID/code/assets/theme.css\n anvil checkout https://anvil.works/git/APPID.git\n anvil checkout APPID --url anvil.works\n anvil checkout APPID --branch master --depth 1 --single-branch\n anvil checkout APPID -O # open editor/file browser after checkout\n anvil checkout APPID my-local-folder --force\n anvil checkout # interactive search/select app list\n anvil checkout -Q "dashboard" # preseed picker search\n');
|
|
20172
20330
|
}
|
|
20331
|
+
const defaultGitAuthDoctorDeps = {
|
|
20332
|
+
getAppAuthBinding: getAppAuthBinding,
|
|
20333
|
+
getAccountsForUrl: auth_getAccountsForUrl,
|
|
20334
|
+
hasTokensForUrl: hasTokensForUrl,
|
|
20335
|
+
runCredentialFill: runGitCredentialFill
|
|
20336
|
+
};
|
|
20337
|
+
function getHighestStatus(statuses) {
|
|
20338
|
+
if (statuses.includes("error")) return "error";
|
|
20339
|
+
if (statuses.includes("warning")) return "warning";
|
|
20340
|
+
return "ok";
|
|
20341
|
+
}
|
|
20342
|
+
function finding(status, code, message) {
|
|
20343
|
+
return {
|
|
20344
|
+
status,
|
|
20345
|
+
code,
|
|
20346
|
+
message
|
|
20347
|
+
};
|
|
20348
|
+
}
|
|
20349
|
+
function redactSecrets(value) {
|
|
20350
|
+
return value.replace(/^password=.*$/gim, "password=<redacted>").replace(/(https?:\/\/git:)[^@\s]+@/g, "$1<redacted>@").replace(/(access[_-]?token=)[^&\s]+/gi, "$1<redacted>").replace(/(refresh[_-]?token=)[^&\s]+/gi, "$1<redacted>");
|
|
20351
|
+
}
|
|
20352
|
+
function splitShellCommand(command) {
|
|
20353
|
+
const tokens = [];
|
|
20354
|
+
let current = "";
|
|
20355
|
+
let state = "normal";
|
|
20356
|
+
for(let i = 0; i < command.length; i++){
|
|
20357
|
+
const ch = command[i];
|
|
20358
|
+
if ("single" === state) {
|
|
20359
|
+
if ("'" === ch) state = "normal";
|
|
20360
|
+
else current += ch;
|
|
20361
|
+
continue;
|
|
20362
|
+
}
|
|
20363
|
+
if ("double" === state) {
|
|
20364
|
+
if ("\"" === ch) state = "normal";
|
|
20365
|
+
else if ("\\" === ch && i + 1 < command.length && [
|
|
20366
|
+
"$",
|
|
20367
|
+
"`",
|
|
20368
|
+
"\"",
|
|
20369
|
+
"\\",
|
|
20370
|
+
"\n"
|
|
20371
|
+
].includes(command[i + 1])) current += command[++i];
|
|
20372
|
+
else current += ch;
|
|
20373
|
+
continue;
|
|
20374
|
+
}
|
|
20375
|
+
if (/\s/.test(ch)) {
|
|
20376
|
+
if (current) {
|
|
20377
|
+
tokens.push(current);
|
|
20378
|
+
current = "";
|
|
20379
|
+
}
|
|
20380
|
+
} else if ("'" === ch) state = "single";
|
|
20381
|
+
else if ("\"" === ch) state = "double";
|
|
20382
|
+
else if ("\\" === ch && i + 1 < command.length) current += command[++i];
|
|
20383
|
+
else current += ch;
|
|
20384
|
+
}
|
|
20385
|
+
if ("normal" !== state) throw new Error("Unterminated quote in helper command");
|
|
20386
|
+
if (current) tokens.push(current);
|
|
20387
|
+
return tokens;
|
|
20388
|
+
}
|
|
20389
|
+
function parseCredentialHelperCommand(command) {
|
|
20390
|
+
const trimmed = command.trim();
|
|
20391
|
+
if (!trimmed || !trimmed.startsWith("!")) return {};
|
|
20392
|
+
const tokens = splitShellCommand(trimmed.slice(1).trim());
|
|
20393
|
+
return {
|
|
20394
|
+
execPath: tokens[0],
|
|
20395
|
+
helperPath: tokens[1]
|
|
20396
|
+
};
|
|
20397
|
+
}
|
|
20398
|
+
function readHelperModulePath(helperPath) {
|
|
20399
|
+
try {
|
|
20400
|
+
const helperSource = external_fs_default().readFileSync(helperPath, "utf8");
|
|
20401
|
+
const match = helperSource.match(/executeGitCredentialOperation\s*}\s*=\s*require\((["'])(.*?)\1\)/);
|
|
20402
|
+
return match?.[2];
|
|
20403
|
+
} catch {
|
|
20404
|
+
return;
|
|
20405
|
+
}
|
|
20406
|
+
}
|
|
20407
|
+
function inspectCredentialHelperCommand(raw) {
|
|
20408
|
+
const details = {
|
|
20409
|
+
raw
|
|
20410
|
+
};
|
|
20411
|
+
try {
|
|
20412
|
+
const parsed = parseCredentialHelperCommand(raw);
|
|
20413
|
+
details.execPath = parsed.execPath;
|
|
20414
|
+
details.helperPath = parsed.helperPath;
|
|
20415
|
+
} catch (e) {
|
|
20416
|
+
details.parseError = e.message;
|
|
20417
|
+
return details;
|
|
20418
|
+
}
|
|
20419
|
+
if (details.execPath) details.execExists = external_fs_default().existsSync(details.execPath);
|
|
20420
|
+
if (details.helperPath) {
|
|
20421
|
+
details.helperExists = external_fs_default().existsSync(details.helperPath);
|
|
20422
|
+
if (details.helperExists) {
|
|
20423
|
+
try {
|
|
20424
|
+
const stat = external_fs_default().statSync(details.helperPath);
|
|
20425
|
+
details.helperExecutable = (73 & stat.mode) !== 0;
|
|
20426
|
+
} catch {
|
|
20427
|
+
details.helperExecutable = false;
|
|
20428
|
+
}
|
|
20429
|
+
details.modulePath = readHelperModulePath(details.helperPath);
|
|
20430
|
+
if (details.modulePath) details.moduleExists = external_fs_default().existsSync(details.modulePath);
|
|
20431
|
+
}
|
|
20432
|
+
}
|
|
20433
|
+
return details;
|
|
20434
|
+
}
|
|
20435
|
+
function detectAnvilRemote(remote) {
|
|
20436
|
+
const fetchUrl = remote.refs.fetch;
|
|
20437
|
+
const out = {
|
|
20438
|
+
name: remote.name,
|
|
20439
|
+
fetchUrl
|
|
20440
|
+
};
|
|
20441
|
+
if (!fetchUrl) return out;
|
|
20442
|
+
const httpMatch = fetchUrl.match(/^(https?):\/\/(?:[^@/?#]+@)?([^/?#]+)\/git\/([A-Z0-9]+)\.git(?:[?#].*)?$/);
|
|
20443
|
+
if (httpMatch) {
|
|
20444
|
+
const [, protocol, host, appId] = httpMatch;
|
|
20445
|
+
return {
|
|
20446
|
+
...out,
|
|
20447
|
+
appId,
|
|
20448
|
+
anvilUrl: normalizeAnvilUrl(`${protocol}://${host}`),
|
|
20449
|
+
transport: "https"
|
|
20450
|
+
};
|
|
20451
|
+
}
|
|
20452
|
+
const sshMatch = fetchUrl.match(/^ssh:\/\/(?:([^@]+)@)?([^:\/]+)(?::\d+)?\/(?:git\/)?([A-Z0-9]+)\.git(?:[?#].*)?$/);
|
|
20453
|
+
if (sshMatch) {
|
|
20454
|
+
const [, , host, appId] = sshMatch;
|
|
20455
|
+
return {
|
|
20456
|
+
...out,
|
|
20457
|
+
appId,
|
|
20458
|
+
anvilUrl: normalizeAnvilUrl(host),
|
|
20459
|
+
transport: "ssh"
|
|
20460
|
+
};
|
|
20461
|
+
}
|
|
20462
|
+
return out;
|
|
20463
|
+
}
|
|
20464
|
+
async function getLocalConfigValues(repoPath, key) {
|
|
20465
|
+
try {
|
|
20466
|
+
const output = await esm_default(repoPath).raw([
|
|
20467
|
+
"config",
|
|
20468
|
+
"--local",
|
|
20469
|
+
"--get-all",
|
|
20470
|
+
key
|
|
20471
|
+
]);
|
|
20472
|
+
const values = output.split(/\r?\n/);
|
|
20473
|
+
if ("" === values[values.length - 1]) values.pop();
|
|
20474
|
+
return values;
|
|
20475
|
+
} catch {
|
|
20476
|
+
return [];
|
|
20477
|
+
}
|
|
20478
|
+
}
|
|
20479
|
+
function buildCredentialInput(anvilUrl, appId) {
|
|
20480
|
+
const url = new URL(anvilUrl);
|
|
20481
|
+
return `protocol=${url.protocol.replace(/:$/, "")}\nhost=${url.host}\npath=git/${appId}.git\n\n`;
|
|
20482
|
+
}
|
|
20483
|
+
async function runGitCredentialFill(repoPath, input) {
|
|
20484
|
+
return new Promise((resolve)=>{
|
|
20485
|
+
const child = (0, external_child_process_namespaceObject.spawn)("git", [
|
|
20486
|
+
"credential",
|
|
20487
|
+
"fill"
|
|
20488
|
+
], {
|
|
20489
|
+
cwd: repoPath,
|
|
20490
|
+
env: {
|
|
20491
|
+
...process.env,
|
|
20492
|
+
GIT_TERMINAL_PROMPT: "0"
|
|
20493
|
+
},
|
|
20494
|
+
stdio: [
|
|
20495
|
+
"pipe",
|
|
20496
|
+
"pipe",
|
|
20497
|
+
"pipe"
|
|
20498
|
+
]
|
|
20499
|
+
});
|
|
20500
|
+
let stdout = "";
|
|
20501
|
+
let stderr = "";
|
|
20502
|
+
child.stdout.setEncoding("utf8");
|
|
20503
|
+
child.stderr.setEncoding("utf8");
|
|
20504
|
+
child.stdout.on("data", (chunk)=>{
|
|
20505
|
+
stdout += chunk;
|
|
20506
|
+
});
|
|
20507
|
+
child.stderr.on("data", (chunk)=>{
|
|
20508
|
+
stderr += chunk;
|
|
20509
|
+
});
|
|
20510
|
+
child.on("error", (error)=>{
|
|
20511
|
+
resolve({
|
|
20512
|
+
exitCode: null,
|
|
20513
|
+
stdout: redactSecrets(stdout),
|
|
20514
|
+
stderr: redactSecrets(stderr),
|
|
20515
|
+
error: redactSecrets(error.message)
|
|
20516
|
+
});
|
|
20517
|
+
});
|
|
20518
|
+
child.on("close", (code)=>{
|
|
20519
|
+
resolve({
|
|
20520
|
+
exitCode: code,
|
|
20521
|
+
stdout: redactSecrets(stdout),
|
|
20522
|
+
stderr: redactSecrets(stderr)
|
|
20523
|
+
});
|
|
20524
|
+
});
|
|
20525
|
+
child.stdin.end(input);
|
|
20526
|
+
});
|
|
20527
|
+
}
|
|
20528
|
+
async function buildAppReport(repoPath, remote, deps) {
|
|
20529
|
+
const appId = remote.appId;
|
|
20530
|
+
const anvilUrl = remote.anvilUrl;
|
|
20531
|
+
const url = new URL(anvilUrl);
|
|
20532
|
+
const credentialScope = `${url.protocol}//${url.host}`;
|
|
20533
|
+
const helperConfigKey = `credential.${credentialScope}.helper`;
|
|
20534
|
+
const helperValues = await getLocalConfigValues(repoPath, helperConfigKey);
|
|
20535
|
+
const nonEmptyHelperValues = helperValues.filter((value)=>"" !== value.trim());
|
|
20536
|
+
const helperDetails = nonEmptyHelperValues.map(inspectCredentialHelperCommand);
|
|
20537
|
+
const findings = [];
|
|
20538
|
+
if (0 === helperValues.length) findings.push(finding("error", "missing_helper_config", `No local ${helperConfigKey} entries found.`));
|
|
20539
|
+
else if (0 === nonEmptyHelperValues.length) findings.push(finding("error", "missing_helper_command", `${helperConfigKey} only resets helpers; no Anvil helper command is configured.`));
|
|
20540
|
+
for (const helper of helperDetails){
|
|
20541
|
+
if (helper.parseError) findings.push(finding("error", "helper_parse_failed", `Could not parse helper command: ${helper.parseError}`));
|
|
20542
|
+
if (helper.execPath && false === helper.execExists) findings.push(finding("error", "helper_exec_missing", `Credential helper Node executable does not exist: ${helper.execPath}`));
|
|
20543
|
+
if (helper.helperPath) {
|
|
20544
|
+
if (false === helper.helperExists) findings.push(finding("error", "helper_file_missing", `Credential helper script does not exist: ${helper.helperPath}`));
|
|
20545
|
+
else if (false === helper.helperExecutable) findings.push(finding("warning", "helper_not_executable", `Credential helper script is not executable: ${helper.helperPath}`));
|
|
20546
|
+
} else findings.push(finding("error", "helper_path_missing", "Credential helper command does not include a helper script path."));
|
|
20547
|
+
if (helper.modulePath && false === helper.moduleExists) findings.push(finding("error", "helper_module_missing", `Generated helper points at a missing module: ${helper.modulePath}`));
|
|
20548
|
+
if (helper.modulePath && "cli.js" === external_path_default().basename(helper.modulePath)) findings.push(finding("error", "helper_module_cli_entry", `Generated helper points at the CLI entry instead of the API entry: ${helper.modulePath}`));
|
|
20549
|
+
}
|
|
20550
|
+
const binding = await deps.getAppAuthBinding(repoPath, appId);
|
|
20551
|
+
if (!binding.url) findings.push(finding("warning", "missing_app_binding_url", `No anvil.auth.${appId}.url binding is configured.`));
|
|
20552
|
+
if (!binding.username) findings.push(finding("warning", "missing_app_binding_username", `No anvil.auth.${appId}.username binding is configured.`));
|
|
20553
|
+
const authUrl = binding.url ? normalizeAnvilUrl(binding.url) : anvilUrl;
|
|
20554
|
+
let accounts = [];
|
|
20555
|
+
let authUsername = binding.username;
|
|
20556
|
+
let hasUsableAuth = false;
|
|
20557
|
+
try {
|
|
20558
|
+
accounts = await deps.getAccountsForUrl(authUrl);
|
|
20559
|
+
if (authUsername || 1 !== accounts.length) {
|
|
20560
|
+
if (!authUsername && accounts.length > 1) findings.push(finding("error", "multiple_accounts_without_binding", `Multiple accounts are logged in for ${authUrl}; bind one with anvil checkout --user.`));
|
|
20561
|
+
} else authUsername = accounts[0];
|
|
20562
|
+
hasUsableAuth = authUsername ? await deps.hasTokensForUrl(authUrl, authUsername) : accounts.length <= 1 && await deps.hasTokensForUrl(authUrl);
|
|
20563
|
+
if (!hasUsableAuth) findings.push(finding("error", "missing_usable_auth", authUsername ? `No usable auth tokens found for ${authUrl} as ${authUsername}.` : `No usable auth tokens found for ${authUrl}.`));
|
|
20564
|
+
} catch (e) {
|
|
20565
|
+
findings.push(finding("error", "auth_lookup_failed", `Auth lookup failed: ${redactSecrets(e.message)}`));
|
|
20566
|
+
}
|
|
20567
|
+
const fillResult = await deps.runCredentialFill(repoPath, buildCredentialInput(anvilUrl, appId));
|
|
20568
|
+
const credentialSimulation = {
|
|
20569
|
+
status: 0 === fillResult.exitCode && /^password=/im.test(fillResult.stdout) ? "ok" : "error",
|
|
20570
|
+
exitCode: fillResult.exitCode,
|
|
20571
|
+
stdout: redactSecrets(fillResult.stdout),
|
|
20572
|
+
stderr: redactSecrets(fillResult.stderr),
|
|
20573
|
+
error: fillResult.error ? redactSecrets(fillResult.error) : void 0
|
|
20574
|
+
};
|
|
20575
|
+
if ("ok" !== credentialSimulation.status) findings.push(finding("error", "credential_fill_failed", credentialSimulation.error || credentialSimulation.stderr.trim() || "git credential fill did not return a password."));
|
|
20576
|
+
const report = {
|
|
20577
|
+
appId,
|
|
20578
|
+
anvilUrl,
|
|
20579
|
+
remoteName: remote.name,
|
|
20580
|
+
remoteUrl: remote.fetchUrl,
|
|
20581
|
+
credentialScope,
|
|
20582
|
+
helperConfigKey,
|
|
20583
|
+
helperValues,
|
|
20584
|
+
helperDetails,
|
|
20585
|
+
binding: {
|
|
20586
|
+
url: binding.url,
|
|
20587
|
+
username: binding.username,
|
|
20588
|
+
hasUrl: !!binding.url,
|
|
20589
|
+
hasUsername: !!binding.username
|
|
20590
|
+
},
|
|
20591
|
+
auth: {
|
|
20592
|
+
url: authUrl,
|
|
20593
|
+
username: authUsername,
|
|
20594
|
+
accounts,
|
|
20595
|
+
hasUsableAuth
|
|
20596
|
+
},
|
|
20597
|
+
credentialSimulation,
|
|
20598
|
+
findings
|
|
20599
|
+
};
|
|
20600
|
+
return report;
|
|
20601
|
+
}
|
|
20602
|
+
async function buildGitAuthDoctorReport(repoPathInput = process.cwd(), options = {}) {
|
|
20603
|
+
const deps = options.deps ?? defaultGitAuthDoctorDeps;
|
|
20604
|
+
const inputPath = external_path_default().resolve(repoPathInput);
|
|
20605
|
+
const git = esm_default(inputPath);
|
|
20606
|
+
const repoPath = (await git.raw([
|
|
20607
|
+
"rev-parse",
|
|
20608
|
+
"--show-toplevel"
|
|
20609
|
+
])).trim();
|
|
20610
|
+
const rawGitDir = (await git.raw([
|
|
20611
|
+
"rev-parse",
|
|
20612
|
+
"--git-dir"
|
|
20613
|
+
])).trim();
|
|
20614
|
+
const gitDir = external_path_default().isAbsolute(rawGitDir) ? rawGitDir : external_path_default().resolve(inputPath, rawGitDir);
|
|
20615
|
+
setRepoContext(repoPath);
|
|
20616
|
+
const remotes = (await git.getRemotes(true)).map(detectAnvilRemote);
|
|
20617
|
+
const anvilRemotes = remotes.filter((remote)=>remote.appId && remote.anvilUrl && "https" === remote.transport);
|
|
20618
|
+
const findings = [];
|
|
20619
|
+
if (0 === anvilRemotes.length) findings.push(finding("error", "missing_anvil_https_remote", "No HTTPS Anvil git remotes were detected."));
|
|
20620
|
+
const apps = await Promise.all(anvilRemotes.map((remote)=>buildAppReport(repoPath, remote, deps)));
|
|
20621
|
+
const status = getHighestStatus([
|
|
20622
|
+
...findings.map((item)=>item.status),
|
|
20623
|
+
...apps.flatMap((app)=>app.findings.map((item)=>item.status))
|
|
20624
|
+
]);
|
|
20625
|
+
return {
|
|
20626
|
+
repoPath,
|
|
20627
|
+
gitDir,
|
|
20628
|
+
remotes,
|
|
20629
|
+
apps,
|
|
20630
|
+
findings,
|
|
20631
|
+
status
|
|
20632
|
+
};
|
|
20633
|
+
}
|
|
20634
|
+
function formatStatus(status) {
|
|
20635
|
+
if ("ok" === status) return external_chalk_default().green("[ok]");
|
|
20636
|
+
if ("warning" === status) return external_chalk_default().yellow("[warn]");
|
|
20637
|
+
return external_chalk_default().red("[error]");
|
|
20638
|
+
}
|
|
20639
|
+
function formatGitAuthDoctorReport(report) {
|
|
20640
|
+
const lines = [];
|
|
20641
|
+
lines.push(external_chalk_default().bold("Git auth doctor"));
|
|
20642
|
+
lines.push(`Repository: ${report.repoPath}`);
|
|
20643
|
+
lines.push(`Git dir: ${report.gitDir}`);
|
|
20644
|
+
lines.push(`Overall: ${formatStatus(report.status)}`);
|
|
20645
|
+
lines.push("");
|
|
20646
|
+
lines.push(external_chalk_default().bold("Detected remotes:"));
|
|
20647
|
+
if (0 === report.remotes.length) lines.push(" (none)");
|
|
20648
|
+
else for (const remote of report.remotes){
|
|
20649
|
+
const appText = remote.appId && remote.anvilUrl ? ` -> ${remote.appId} on ${remote.anvilUrl}` : "";
|
|
20650
|
+
lines.push(` - ${remote.name}: ${remote.fetchUrl || "(no fetch URL)"}${appText}`);
|
|
20651
|
+
}
|
|
20652
|
+
for (const app of report.apps){
|
|
20653
|
+
lines.push("");
|
|
20654
|
+
lines.push(external_chalk_default().bold(`App ${app.appId} (${app.remoteName})`));
|
|
20655
|
+
lines.push(` URL: ${app.anvilUrl}`);
|
|
20656
|
+
lines.push(` Credential scope: ${app.credentialScope}`);
|
|
20657
|
+
lines.push(` Helper config: ${app.helperConfigKey}`);
|
|
20658
|
+
if (0 === app.helperValues.length) lines.push(" (none)");
|
|
20659
|
+
else for (const value of app.helperValues)lines.push(` - ${value || "(reset helper list)"}`);
|
|
20660
|
+
for (const helper of app.helperDetails){
|
|
20661
|
+
lines.push(` Helper command: ${helper.raw}`);
|
|
20662
|
+
lines.push(` Node: ${helper.execPath || "(missing)"} ${false === helper.execExists ? "[missing]" : ""}`.trimEnd());
|
|
20663
|
+
lines.push(` Script: ${helper.helperPath || "(missing)"} ${false === helper.helperExists ? "[missing]" : ""}`.trimEnd());
|
|
20664
|
+
if (helper.modulePath) lines.push(` Module: ${helper.modulePath} ${false === helper.moduleExists ? "[missing]" : ""}`.trimEnd());
|
|
20665
|
+
}
|
|
20666
|
+
lines.push(` Binding URL: ${app.binding.url || "(missing)"}`);
|
|
20667
|
+
lines.push(` Binding user: ${app.binding.username || "(missing)"}`);
|
|
20668
|
+
lines.push(` Auth tokens: ${app.auth.hasUsableAuth ? "available" : "not available"}${app.auth.username ? ` for ${app.auth.username}` : ""}`);
|
|
20669
|
+
if (app.credentialSimulation) {
|
|
20670
|
+
lines.push(` Credential fill: ${formatStatus(app.credentialSimulation.status)} exit=${app.credentialSimulation.exitCode}`);
|
|
20671
|
+
if (app.credentialSimulation.stdout.trim()) {
|
|
20672
|
+
lines.push(" stdout:");
|
|
20673
|
+
for (const line of app.credentialSimulation.stdout.trim().split(/\r?\n/))lines.push(` ${line}`);
|
|
20674
|
+
}
|
|
20675
|
+
if (app.credentialSimulation.stderr.trim()) {
|
|
20676
|
+
lines.push(" stderr:");
|
|
20677
|
+
for (const line of app.credentialSimulation.stderr.trim().split(/\r?\n/))lines.push(` ${line}`);
|
|
20678
|
+
}
|
|
20679
|
+
}
|
|
20680
|
+
if (app.findings.length > 0) {
|
|
20681
|
+
lines.push(" Findings:");
|
|
20682
|
+
for (const item of app.findings)lines.push(` ${formatStatus(item.status)} ${item.code}: ${item.message}`);
|
|
20683
|
+
}
|
|
20684
|
+
}
|
|
20685
|
+
if (report.findings.length > 0) {
|
|
20686
|
+
lines.push("");
|
|
20687
|
+
lines.push(external_chalk_default().bold("Repository findings:"));
|
|
20688
|
+
for (const item of report.findings)lines.push(` ${formatStatus(item.status)} ${item.code}: ${item.message}`);
|
|
20689
|
+
}
|
|
20690
|
+
return lines.join("\n");
|
|
20691
|
+
}
|
|
20692
|
+
function registerGitAuthCommand(program) {
|
|
20693
|
+
const gitAuthCommand = program.command("git-auth").description("Diagnose Anvil Git authentication");
|
|
20694
|
+
gitAuthCommand.command("doctor").description("Diagnose why plain Git commands cannot authenticate to Anvil").option("--repo <PATH>", "Repository path to diagnose", process.cwd()).action(async (options)=>{
|
|
20695
|
+
try {
|
|
20696
|
+
const report = await buildGitAuthDoctorReport(options.repo || process.cwd());
|
|
20697
|
+
if (getGlobalOutputConfig().jsonMode) logJsonResult("error" !== report.status, {
|
|
20698
|
+
data: report
|
|
20699
|
+
});
|
|
20700
|
+
else logger_logger.info(formatGitAuthDoctorReport(report));
|
|
20701
|
+
if ("error" === report.status) process.exitCode = 1;
|
|
20702
|
+
} catch (e) {
|
|
20703
|
+
const message = redactSecrets(e.message);
|
|
20704
|
+
if (getGlobalOutputConfig().jsonMode) logJsonResult(false, {
|
|
20705
|
+
error: message
|
|
20706
|
+
});
|
|
20707
|
+
else logger_logger.error(`Git auth doctor failed: ${message}`);
|
|
20708
|
+
process.exitCode = 1;
|
|
20709
|
+
}
|
|
20710
|
+
});
|
|
20711
|
+
}
|
|
20173
20712
|
const gitCredential_defaultDeps = {
|
|
20174
20713
|
getValidAuthToken: auth_getValidAuthToken,
|
|
20175
20714
|
getAccountsForUrl: auth_getAccountsForUrl,
|
|
20176
20715
|
getAppAuthBinding: getAppAuthBinding,
|
|
20177
20716
|
cwd: ()=>process.cwd()
|
|
20178
20717
|
};
|
|
20718
|
+
function sanitizeCredentialError(message) {
|
|
20719
|
+
return message.replace(/^password=.*$/gim, "password=<redacted>").replace(/(https?:\/\/git:)[^@\s]+@/g, "$1<redacted>@").replace(/(access[_-]?token=)[^&\s]+/gi, "$1<redacted>").replace(/(refresh[_-]?token=)[^&\s]+/gi, "$1<redacted>");
|
|
20720
|
+
}
|
|
20179
20721
|
function parseGitCredentialRequest(rawInput) {
|
|
20180
20722
|
const out = {};
|
|
20181
20723
|
const lines = rawInput.split(/\r?\n/);
|
|
@@ -20236,7 +20778,11 @@ Promise.resolve(executeGitCredentialOperation(process.argv[2] || "get", {
|
|
|
20236
20778
|
}).description("Internal helper command for Git HTTPS authentication").action(async (operation)=>{
|
|
20237
20779
|
try {
|
|
20238
20780
|
await executeGitCredentialOperation(operation);
|
|
20239
|
-
} catch
|
|
20781
|
+
} catch (e) {
|
|
20782
|
+
if ("1" === process.env["ANVIL_GIT_CREDENTIAL_DEBUG"]) {
|
|
20783
|
+
const message = e instanceof Error ? e.message : String(e);
|
|
20784
|
+
process.stderr.write(`anvil git-credential failed: ${sanitizeCredentialError(message)}\n`);
|
|
20785
|
+
}
|
|
20240
20786
|
process.exit(1);
|
|
20241
20787
|
}
|
|
20242
20788
|
});
|
|
@@ -21945,6 +22491,7 @@ Promise.resolve(executeGitCredentialOperation(process.argv[2] || "get", {
|
|
|
21945
22491
|
});
|
|
21946
22492
|
const watchCommand = registerWatchCommand(program);
|
|
21947
22493
|
registerCheckoutCommand(program);
|
|
22494
|
+
registerGitAuthCommand(program);
|
|
21948
22495
|
registerGitCredentialCommand(program);
|
|
21949
22496
|
registerLoginCommand(program);
|
|
21950
22497
|
registerLogoutCommand(program);
|