@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/index.js CHANGED
@@ -7371,6 +7371,7 @@ var __webpack_exports__ = {};
7371
7371
  setConfig: ()=>setConfig,
7372
7372
  getAccountsForUrl: ()=>auth_getAccountsForUrl,
7373
7373
  AppIdWithContext: ()=>anvil_api_namespaceObject.AppIdWithContext,
7374
+ inspectCredentialHelperCommand: ()=>inspectCredentialHelperCommand,
7374
7375
  executeCheckout: ()=>executeCheckout,
7375
7376
  validateCheckoutDestination: ()=>validateCheckoutDestination,
7376
7377
  DeviceAuthorizationResponse: ()=>oauth_login_namespaceObject.DeviceAuthorizationResponse,
@@ -7386,7 +7387,9 @@ var __webpack_exports__ = {};
7386
7387
  verifyAndStoreTokens: ()=>auth_verifyAndStoreTokens,
7387
7388
  formatValidationPath: ()=>formatValidationPath,
7388
7389
  resolveAnvilUrl: ()=>resolveAnvilUrl,
7390
+ buildGitAuthDoctorReport: ()=>buildGitAuthDoctorReport,
7389
7391
  parseCheckoutInput: ()=>parseCheckoutInput,
7392
+ formatGitAuthDoctorReport: ()=>formatGitAuthDoctorReport,
7390
7393
  resetConfig: ()=>resetConfig,
7391
7394
  FormTemplateValidationResult: ()=>validators_namespaceObject.FormTemplateValidationResult,
7392
7395
  createWatchDiagnostic: ()=>createWatchDiagnostic,
@@ -7449,6 +7452,7 @@ var __webpack_exports__ = {};
7449
7452
  executeGitCredentialOperation: ()=>executeGitCredentialOperation,
7450
7453
  CheckoutResult: ()=>checkout_namespaceObject.CheckoutResult,
7451
7454
  ParsedCheckoutInput: ()=>checkout_namespaceObject.ParsedCheckoutInput,
7455
+ parseCredentialHelperCommand: ()=>parseCredentialHelperCommand,
7452
7456
  clearInMemoryTokens: ()=>clearInMemoryTokens,
7453
7457
  getDefaultDestinationDirectory: ()=>getDefaultDestinationDirectory,
7454
7458
  lookupByCommit: ()=>lookupByCommit,
@@ -13693,7 +13697,7 @@ Promise.resolve(executeGitCredentialOperation(process.argv[2] || "get", {
13693
13697
  }
13694
13698
  function getCredentialHelperModulePath(options) {
13695
13699
  const currentModulePath = options?.currentModulePath ?? __filename;
13696
- if (isAnvilCliPackageFile(currentModulePath)) return currentModulePath;
13700
+ if (isAnvilCliPackageFile(currentModulePath) && "cli.js" !== external_path_default().basename(currentModulePath)) return currentModulePath;
13697
13701
  return (options?.resolvePackageEntry ?? (()=>requireFromHere.resolve("@anvil-works/anvil-cli")))();
13698
13702
  }
13699
13703
  async function getRepositoryGitDir(repoPath, git = esm_default(repoPath)) {
@@ -14713,6 +14717,77 @@ Promise.resolve(executeGitCredentialOperation(process.argv[2] || "get", {
14713
14717
  throw errors_createGitError.commandFailed("status", e.message);
14714
14718
  }
14715
14719
  }
14720
+ async discardModeOnlyChanges(paths) {
14721
+ const discarded = [];
14722
+ for (const relativePath of paths)try {
14723
+ const headMode = await this.getHeadFileMode(relativePath);
14724
+ if ("100644" !== headMode && "100755" !== headMode) continue;
14725
+ const contentUnchanged = await this.hasSameContentAsHead(relativePath);
14726
+ if (!contentUnchanged) continue;
14727
+ await this.restoreExecutableBit(relativePath, "100755" === headMode);
14728
+ discarded.push(relativePath);
14729
+ } catch (e) {
14730
+ throw errors_createGitError.commandFailed("discard mode-only changes", e.message);
14731
+ }
14732
+ return discarded;
14733
+ }
14734
+ async discardStagedModeOnlyChanges(paths) {
14735
+ const discarded = [];
14736
+ for (const relativePath of paths)try {
14737
+ const headMode = await this.getHeadFileMode(relativePath);
14738
+ if ("100644" !== headMode && "100755" !== headMode) continue;
14739
+ const indexContentUnchanged = await this.hasSameIndexContentAsHead(relativePath);
14740
+ if (!indexContentUnchanged) continue;
14741
+ await this.git.raw([
14742
+ "reset",
14743
+ "-q",
14744
+ "HEAD",
14745
+ "--",
14746
+ relativePath
14747
+ ]);
14748
+ await this.restoreExecutableBit(relativePath, "100755" === headMode);
14749
+ discarded.push(relativePath);
14750
+ } catch (e) {
14751
+ throw errors_createGitError.commandFailed("discard staged mode-only changes", e.message);
14752
+ }
14753
+ return discarded;
14754
+ }
14755
+ async getHeadFileMode(relativePath) {
14756
+ const output = await this.git.raw([
14757
+ "ls-tree",
14758
+ "HEAD",
14759
+ "--",
14760
+ relativePath
14761
+ ]);
14762
+ const match = output.match(/^(\d{6})\s/);
14763
+ return match?.[1] ?? null;
14764
+ }
14765
+ async hasSameContentAsHead(relativePath) {
14766
+ const headHash = (await this.git.revparse([
14767
+ `HEAD:${relativePath}`
14768
+ ])).trim();
14769
+ const worktreeHash = (await this.git.raw([
14770
+ "hash-object",
14771
+ "--",
14772
+ relativePath
14773
+ ])).trim();
14774
+ return headHash === worktreeHash;
14775
+ }
14776
+ async hasSameIndexContentAsHead(relativePath) {
14777
+ const headHash = (await this.git.revparse([
14778
+ `HEAD:${relativePath}`
14779
+ ])).trim();
14780
+ const indexHash = (await this.git.revparse([
14781
+ `:${relativePath}`
14782
+ ])).trim();
14783
+ return headHash === indexHash;
14784
+ }
14785
+ async restoreExecutableBit(relativePath, executable) {
14786
+ const filePath = external_path_default().join(this.repoPath, relativePath);
14787
+ const stat = await external_fs_.promises.stat(filePath);
14788
+ const mode = executable ? 73 | stat.mode : -74 & stat.mode;
14789
+ if ((511 & stat.mode) !== (511 & mode)) await external_fs_.promises.chmod(filePath, mode);
14790
+ }
14716
14791
  async hasUncommittedChanges() {
14717
14792
  const status = await this.getStatus();
14718
14793
  return !status.isClean;
@@ -14852,6 +14927,32 @@ Promise.resolve(executeGitCredentialOperation(process.argv[2] || "get", {
14852
14927
  throw errors_createGitError.commandFailed("rebase --abort", e.message);
14853
14928
  }
14854
14929
  }
14930
+ async isAncestor(ancestorRef, descendantRef) {
14931
+ try {
14932
+ const mergeBase = (await this.git.raw([
14933
+ "merge-base",
14934
+ ancestorRef,
14935
+ descendantRef
14936
+ ])).trim();
14937
+ const ancestorCommit = (await this.git.revparse([
14938
+ ancestorRef
14939
+ ])).trim();
14940
+ return mergeBase === ancestorCommit;
14941
+ } catch {
14942
+ return false;
14943
+ }
14944
+ }
14945
+ async mergeFastForward(ref) {
14946
+ try {
14947
+ await this.git.raw([
14948
+ "merge",
14949
+ "--ff-only",
14950
+ ref
14951
+ ]);
14952
+ } catch (e) {
14953
+ throw errors_createGitError.commandFailed("merge --ff-only", e.message);
14954
+ }
14955
+ }
14855
14956
  async deleteRef(ref) {
14856
14957
  try {
14857
14958
  await this.git.raw([
@@ -14896,11 +14997,12 @@ Promise.resolve(executeGitCredentialOperation(process.argv[2] || "get", {
14896
14997
  } catch (error) {}
14897
14998
  }
14898
14999
  }
14899
- async function getStagedFileChanges(git) {
15000
+ async function getStagedFileChanges(git, gitService) {
14900
15001
  try {
14901
15002
  const status = await git.status();
14902
15003
  const renames = status.renamed || [];
14903
15004
  const stagedFiles = status.files.filter((f)=>" " !== f.index && "?" !== f.index);
15005
+ const stagedModeOnlyPaths = new Set(gitService ? await gitService.discardStagedModeOnlyChanges(stagedFiles.filter((f)=>"R" !== f.index && "D" !== f.index).map((f)=>f.path)) : []);
14904
15006
  const stagedRenames = [];
14905
15007
  for (const f of stagedFiles)if ("R" === f.index) {
14906
15008
  const rename = renames.find((r)=>r.to === f.path);
@@ -14913,19 +15015,23 @@ Promise.resolve(executeGitCredentialOperation(process.argv[2] || "get", {
14913
15015
  const stagedRenamedFromPaths = new Set(stagedRenames.map((r)=>r.from));
14914
15016
  const stagedRenamedToPaths = new Set(stagedRenames.map((r)=>r.path));
14915
15017
  const changes = [];
14916
- for (const f of stagedFiles)if ("R" !== f.index) {
14917
- if (!(stagedRenamedFromPaths.has(f.path) || stagedRenamedToPaths.has(f.path))) if ("D" === f.index) changes.push({
14918
- path: f.path,
14919
- type: "unlink"
14920
- });
14921
- else if ("A" === f.index || "?" === f.index) changes.push({
14922
- path: f.path,
14923
- type: "add"
14924
- });
14925
- else changes.push({
14926
- path: f.path,
14927
- type: "change"
14928
- });
15018
+ for (const f of stagedFiles){
15019
+ if ("R" !== f.index) {
15020
+ if (!stagedModeOnlyPaths.has(f.path)) {
15021
+ if (!(stagedRenamedFromPaths.has(f.path) || stagedRenamedToPaths.has(f.path))) if ("D" === f.index) changes.push({
15022
+ path: f.path,
15023
+ type: "unlink"
15024
+ });
15025
+ else if ("A" === f.index || "?" === f.index) changes.push({
15026
+ path: f.path,
15027
+ type: "add"
15028
+ });
15029
+ else changes.push({
15030
+ path: f.path,
15031
+ type: "change"
15032
+ });
15033
+ }
15034
+ }
14929
15035
  }
14930
15036
  changes.push(...stagedRenames);
14931
15037
  return changes;
@@ -17681,13 +17787,14 @@ Promise.resolve(executeGitCredentialOperation(process.argv[2] || "get", {
17681
17787
  }
17682
17788
  async getStagedChanges() {
17683
17789
  try {
17684
- return await getStagedFileChanges(this.config.gitService.getGit());
17790
+ return await getStagedFileChanges(this.config.gitService.getGit(), this.config.gitService);
17685
17791
  } catch (e) {
17686
17792
  logger_logger.error(external_chalk_default().red(`Error getting staged changes: ${errors_getErrorMessage(e)}`));
17687
17793
  return [];
17688
17794
  }
17689
17795
  }
17690
17796
  async getUnstagedChanges(status) {
17797
+ const modeOnlyChanges = new Set(await this.config.gitService.discardModeOnlyChanges(status.modified));
17691
17798
  const renames = status.renamed;
17692
17799
  const stagedRenamedFromPaths = new Set(renames.map((r)=>r.from));
17693
17800
  const stagedRenamedToPaths = new Set(renames.map((r)=>r.to));
@@ -17707,7 +17814,7 @@ Promise.resolve(executeGitCredentialOperation(process.argv[2] || "get", {
17707
17814
  const allRenamedFromPaths = new Set(allRenames.map((r)=>r.from));
17708
17815
  const allRenamedToPaths = new Set(allRenames.map((r)=>r.to));
17709
17816
  return [
17710
- ...status.modified.map((f)=>({
17817
+ ...status.modified.filter((f)=>!modeOnlyChanges.has(f)).map((f)=>({
17711
17818
  path: f,
17712
17819
  type: "change"
17713
17820
  })),
@@ -17915,7 +18022,7 @@ Promise.resolve(executeGitCredentialOperation(process.argv[2] || "get", {
17915
18022
  await this.handleConflict(json_resp, originalFilePaths, retryCount);
17916
18023
  return;
17917
18024
  }
17918
- if (200 === resp.status) return void await this.handleSuccess(json_resp);
18025
+ if (200 === resp.status) return void await this.handleSuccess(json_resp, originalFilePaths);
17919
18026
  logger_logger.error(`Unexpected response status: ${resp.status}`);
17920
18027
  throw {
17921
18028
  type: "server_error",
@@ -18018,13 +18125,20 @@ Promise.resolve(executeGitCredentialOperation(process.argv[2] || "get", {
18018
18125
  reason: "Unknown conflict"
18019
18126
  };
18020
18127
  }
18021
- async handleSuccess(json_resp) {
18128
+ async handleSuccess(json_resp, originalFilePaths) {
18022
18129
  const version = json_resp.version;
18023
- this.config.setCommitId(version);
18130
+ const oldCommitId = json_resp.previous_commit ?? null;
18131
+ const commitIdBeforePostSaveSync = this.config.getCommitId();
18132
+ this.emit("saved", {
18133
+ oldCommitId,
18134
+ newCommitId: version,
18135
+ branch: this.config.getCurrentBranch()
18136
+ });
18024
18137
  if (this.postSaveCallback) {
18025
18138
  logger_logger.progressUpdate("sync", "Syncing back");
18026
- await this.postSaveCallback();
18139
+ await this.postSaveCallback(new Set(originalFilePaths));
18027
18140
  }
18141
+ if (this.config.getCommitId() === commitIdBeforePostSaveSync) this.config.setCommitId(version);
18028
18142
  }
18029
18143
  async rerouteChanges(changes) {
18030
18144
  logger_logger.verbose(external_chalk_default().blue(`Re-routing ${changes.length} local change(s)...`));
@@ -18095,12 +18209,22 @@ Promise.resolve(executeGitCredentialOperation(process.argv[2] || "get", {
18095
18209
  try {
18096
18210
  const oldCommitId = this.config.getCommitId();
18097
18211
  let locallyChangedFiles;
18212
+ let initialStatus;
18098
18213
  if (knownLocalChanges) locallyChangedFiles = knownLocalChanges;
18099
18214
  else {
18100
- const status = await this.config.gitService.getStatus();
18101
- locallyChangedFiles = buildLocalChangesSet(status);
18215
+ initialStatus = await this.config.gitService.getStatus();
18216
+ locallyChangedFiles = buildLocalChangesSet(initialStatus);
18102
18217
  }
18103
- await this.fetchAndResetFromAnvil(locallyChangedFiles);
18218
+ let syncMode;
18219
+ if (!knownLocalChanges && initialStatus && this.isCleanForFastForward(initialStatus)) syncMode = await this.fetchAndMoveCleanTreeToAnvilHead(oldCommitId, locallyChangedFiles);
18220
+ else {
18221
+ await this.fetchAndResetFromAnvil(locallyChangedFiles);
18222
+ syncMode = "preservation";
18223
+ }
18224
+ if ("clean-move" === syncMode) return {
18225
+ localOnlyChanges: [],
18226
+ conflicts: []
18227
+ };
18104
18228
  const newCommitId = await this.config.gitService.getCommitId();
18105
18229
  this.config.setCommitId(newCommitId);
18106
18230
  const remoteChangedFiles = await detectRemoteChanges(this.config.gitService, oldCommitId, newCommitId);
@@ -18125,15 +18249,43 @@ Promise.resolve(executeGitCredentialOperation(process.argv[2] || "get", {
18125
18249
  }
18126
18250
  }
18127
18251
  async fetchAndResetFromAnvil(localChangesToPreserve = new Set()) {
18252
+ const oldCommitId = this.config.getCommitId();
18253
+ await this.withFetchedAnvilRef((tempRef)=>this.resetToFetchedAnvilRef(tempRef, oldCommitId, localChangesToPreserve));
18254
+ }
18255
+ async fetchAndMoveCleanTreeToAnvilHead(oldCommitId, locallyChangedFiles) {
18256
+ return this.withFetchedAnvilRef(async (tempRef)=>{
18257
+ const localHead = await this.config.gitService.getCommitId();
18258
+ if (await this.config.gitService.isAncestor(localHead, tempRef)) await this.config.gitService.mergeFastForward(tempRef);
18259
+ else {
18260
+ const statusBeforeHardReset = await this.config.gitService.getStatus();
18261
+ if (!this.isCleanForFastForward(statusBeforeHardReset)) {
18262
+ for (const filePath of buildLocalChangesSet(statusBeforeHardReset))locallyChangedFiles.add(filePath);
18263
+ await this.resetToFetchedAnvilRef(tempRef, oldCommitId, locallyChangedFiles);
18264
+ return "preservation";
18265
+ }
18266
+ await this.config.gitService.reset(tempRef, "hard");
18267
+ }
18268
+ const newCommitId = await this.config.gitService.getCommitId();
18269
+ this.config.setCommitId(newCommitId);
18270
+ await this.config.editorYaml.reload();
18271
+ return "clean-move";
18272
+ });
18273
+ }
18274
+ async withFetchedAnvilRef(callback) {
18128
18275
  const validToken = await auth_getValidAuthToken(this.config.anvilUrl, this.config.username);
18129
18276
  this.config.setAuthToken(validToken);
18130
18277
  const httpUrl = anvil_api_getGitFetchUrl(this.config.appId, this.config.getAuthToken(), this.config.anvilUrl);
18131
18278
  const currentBranch = this.config.getCurrentBranch();
18132
18279
  const tempRef = `anvil-sync-temp-${Date.now()}`;
18133
- const oldCommitId = this.config.getCommitId();
18134
- await this.config.gitService.fetch(httpUrl, `+${currentBranch}:${tempRef}`);
18280
+ try {
18281
+ await this.config.gitService.fetch(httpUrl, `+${currentBranch}:${tempRef}`);
18282
+ return await callback(tempRef);
18283
+ } finally{
18284
+ await this.config.gitService.deleteRef(`refs/heads/${tempRef}`);
18285
+ }
18286
+ }
18287
+ async resetToFetchedAnvilRef(tempRef, oldCommitId, localChangesToPreserve) {
18135
18288
  await this.config.gitService.reset(tempRef, "mixed");
18136
- await this.config.gitService.deleteRef(`refs/heads/${tempRef}`);
18137
18289
  const newCommitId = await this.config.gitService.getCommitId();
18138
18290
  await this.config.gitService.checkout([
18139
18291
  ".anvil_editor.yaml",
@@ -18143,6 +18295,9 @@ Promise.resolve(executeGitCredentialOperation(process.argv[2] || "get", {
18143
18295
  await this.deleteFilesRemovedOnAnvilLocally(oldCommitId, newCommitId, localChangesToPreserve);
18144
18296
  await this.discardFormattingOnlyYamlChanges();
18145
18297
  }
18298
+ isCleanForFastForward(status) {
18299
+ 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;
18300
+ }
18146
18301
  async deleteFilesRemovedOnAnvilLocally(oldCommitId, newCommitId, localChangesToPreserve) {
18147
18302
  try {
18148
18303
  const status = await this.config.gitService.getStatus();
@@ -18356,11 +18511,12 @@ Promise.resolve(executeGitCredentialOperation(process.argv[2] || "get", {
18356
18511
  isPaused: ()=>this.isPausedForUserInput
18357
18512
  });
18358
18513
  this.saveProcessor.setSyncCallback((filePaths)=>this.syncManager.syncRemoteChanges(filePaths));
18359
- this.saveProcessor.setPostSaveCallback(()=>this.syncManager.fetchAndResetFromAnvil());
18514
+ this.saveProcessor.setPostSaveCallback((filePaths)=>this.syncAfterSave(filePaths));
18360
18515
  this.saveProcessor.on("save-started", (data)=>this.emit("save-started", data));
18361
18516
  this.saveProcessor.on("save-succeeded", (data)=>this.emit("save-succeeded", data));
18362
18517
  this.saveProcessor.on("save-failed", (data)=>this.emit("save-failed", data));
18363
18518
  this.saveProcessor.on("save-complete", (data)=>this.emit("save-complete", data));
18519
+ this.saveProcessor.on("saved", (data)=>this.emit("saved", data));
18364
18520
  this.saveProcessor.on("anvil-yaml-dependencies-changed", ()=>{
18365
18521
  this.queueDependencyRefresh("anvil.yaml dependencies changed");
18366
18522
  });
@@ -18368,6 +18524,12 @@ Promise.resolve(executeGitCredentialOperation(process.argv[2] || "get", {
18368
18524
  this.saveProcessor.on("sync-conflict", (data)=>this.emit("sync-conflict", data));
18369
18525
  this.saveProcessor.on("max-retries-exceeded", (data)=>this.emit("max-retries-exceeded", data));
18370
18526
  }
18527
+ async syncAfterSave(savedFilePaths) {
18528
+ const status = await this.gitService.getStatus();
18529
+ const locallyChangedFiles = buildLocalChangesSet(status);
18530
+ if (!this.stagedOnly) for (const filePath of savedFilePaths)locallyChangedFiles.add(filePath);
18531
+ return this.syncManager.syncRemoteChanges(locallyChangedFiles);
18532
+ }
18371
18533
  async connectWebSocket() {
18372
18534
  this.wsClient = new WebSocketClient({
18373
18535
  appId: this.appId,
@@ -19000,6 +19162,367 @@ Promise.resolve(executeGitCredentialOperation(process.argv[2] || "get", {
19000
19162
  }
19001
19163
  require("inquirer");
19002
19164
  require("ora");
19165
+ const defaultGitAuthDoctorDeps = {
19166
+ getAppAuthBinding: git_auth_getAppAuthBinding,
19167
+ getAccountsForUrl: auth_getAccountsForUrl,
19168
+ hasTokensForUrl: auth_hasTokensForUrl,
19169
+ runCredentialFill: runGitCredentialFill
19170
+ };
19171
+ function getHighestStatus(statuses) {
19172
+ if (statuses.includes("error")) return "error";
19173
+ if (statuses.includes("warning")) return "warning";
19174
+ return "ok";
19175
+ }
19176
+ function finding(status, code, message) {
19177
+ return {
19178
+ status,
19179
+ code,
19180
+ message
19181
+ };
19182
+ }
19183
+ function redactSecrets(value) {
19184
+ 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>");
19185
+ }
19186
+ function splitShellCommand(command) {
19187
+ const tokens = [];
19188
+ let current = "";
19189
+ let state = "normal";
19190
+ for(let i = 0; i < command.length; i++){
19191
+ const ch = command[i];
19192
+ if ("single" === state) {
19193
+ if ("'" === ch) state = "normal";
19194
+ else current += ch;
19195
+ continue;
19196
+ }
19197
+ if ("double" === state) {
19198
+ if ("\"" === ch) state = "normal";
19199
+ else if ("\\" === ch && i + 1 < command.length && [
19200
+ "$",
19201
+ "`",
19202
+ "\"",
19203
+ "\\",
19204
+ "\n"
19205
+ ].includes(command[i + 1])) current += command[++i];
19206
+ else current += ch;
19207
+ continue;
19208
+ }
19209
+ if (/\s/.test(ch)) {
19210
+ if (current) {
19211
+ tokens.push(current);
19212
+ current = "";
19213
+ }
19214
+ } else if ("'" === ch) state = "single";
19215
+ else if ("\"" === ch) state = "double";
19216
+ else if ("\\" === ch && i + 1 < command.length) current += command[++i];
19217
+ else current += ch;
19218
+ }
19219
+ if ("normal" !== state) throw new Error("Unterminated quote in helper command");
19220
+ if (current) tokens.push(current);
19221
+ return tokens;
19222
+ }
19223
+ function parseCredentialHelperCommand(command) {
19224
+ const trimmed = command.trim();
19225
+ if (!trimmed || !trimmed.startsWith("!")) return {};
19226
+ const tokens = splitShellCommand(trimmed.slice(1).trim());
19227
+ return {
19228
+ execPath: tokens[0],
19229
+ helperPath: tokens[1]
19230
+ };
19231
+ }
19232
+ function readHelperModulePath(helperPath) {
19233
+ try {
19234
+ const helperSource = external_fs_default().readFileSync(helperPath, "utf8");
19235
+ const match = helperSource.match(/executeGitCredentialOperation\s*}\s*=\s*require\((["'])(.*?)\1\)/);
19236
+ return match?.[2];
19237
+ } catch {
19238
+ return;
19239
+ }
19240
+ }
19241
+ function inspectCredentialHelperCommand(raw) {
19242
+ const details = {
19243
+ raw
19244
+ };
19245
+ try {
19246
+ const parsed = parseCredentialHelperCommand(raw);
19247
+ details.execPath = parsed.execPath;
19248
+ details.helperPath = parsed.helperPath;
19249
+ } catch (e) {
19250
+ details.parseError = e.message;
19251
+ return details;
19252
+ }
19253
+ if (details.execPath) details.execExists = external_fs_default().existsSync(details.execPath);
19254
+ if (details.helperPath) {
19255
+ details.helperExists = external_fs_default().existsSync(details.helperPath);
19256
+ if (details.helperExists) {
19257
+ try {
19258
+ const stat = external_fs_default().statSync(details.helperPath);
19259
+ details.helperExecutable = (73 & stat.mode) !== 0;
19260
+ } catch {
19261
+ details.helperExecutable = false;
19262
+ }
19263
+ details.modulePath = readHelperModulePath(details.helperPath);
19264
+ if (details.modulePath) details.moduleExists = external_fs_default().existsSync(details.modulePath);
19265
+ }
19266
+ }
19267
+ return details;
19268
+ }
19269
+ function detectAnvilRemote(remote) {
19270
+ const fetchUrl = remote.refs.fetch;
19271
+ const out = {
19272
+ name: remote.name,
19273
+ fetchUrl
19274
+ };
19275
+ if (!fetchUrl) return out;
19276
+ const httpMatch = fetchUrl.match(/^(https?):\/\/(?:[^@/?#]+@)?([^/?#]+)\/git\/([A-Z0-9]+)\.git(?:[?#].*)?$/);
19277
+ if (httpMatch) {
19278
+ const [, protocol, host, appId] = httpMatch;
19279
+ return {
19280
+ ...out,
19281
+ appId,
19282
+ anvilUrl: config_normalizeAnvilUrl(`${protocol}://${host}`),
19283
+ transport: "https"
19284
+ };
19285
+ }
19286
+ const sshMatch = fetchUrl.match(/^ssh:\/\/(?:([^@]+)@)?([^:\/]+)(?::\d+)?\/(?:git\/)?([A-Z0-9]+)\.git(?:[?#].*)?$/);
19287
+ if (sshMatch) {
19288
+ const [, , host, appId] = sshMatch;
19289
+ return {
19290
+ ...out,
19291
+ appId,
19292
+ anvilUrl: config_normalizeAnvilUrl(host),
19293
+ transport: "ssh"
19294
+ };
19295
+ }
19296
+ return out;
19297
+ }
19298
+ async function getLocalConfigValues(repoPath, key) {
19299
+ try {
19300
+ const output = await esm_default(repoPath).raw([
19301
+ "config",
19302
+ "--local",
19303
+ "--get-all",
19304
+ key
19305
+ ]);
19306
+ const values = output.split(/\r?\n/);
19307
+ if ("" === values[values.length - 1]) values.pop();
19308
+ return values;
19309
+ } catch {
19310
+ return [];
19311
+ }
19312
+ }
19313
+ function buildCredentialInput(anvilUrl, appId) {
19314
+ const url = new URL(anvilUrl);
19315
+ return `protocol=${url.protocol.replace(/:$/, "")}\nhost=${url.host}\npath=git/${appId}.git\n\n`;
19316
+ }
19317
+ async function runGitCredentialFill(repoPath, input) {
19318
+ return new Promise((resolve)=>{
19319
+ const child = (0, external_child_process_namespaceObject.spawn)("git", [
19320
+ "credential",
19321
+ "fill"
19322
+ ], {
19323
+ cwd: repoPath,
19324
+ env: {
19325
+ ...process.env,
19326
+ GIT_TERMINAL_PROMPT: "0"
19327
+ },
19328
+ stdio: [
19329
+ "pipe",
19330
+ "pipe",
19331
+ "pipe"
19332
+ ]
19333
+ });
19334
+ let stdout = "";
19335
+ let stderr = "";
19336
+ child.stdout.setEncoding("utf8");
19337
+ child.stderr.setEncoding("utf8");
19338
+ child.stdout.on("data", (chunk)=>{
19339
+ stdout += chunk;
19340
+ });
19341
+ child.stderr.on("data", (chunk)=>{
19342
+ stderr += chunk;
19343
+ });
19344
+ child.on("error", (error)=>{
19345
+ resolve({
19346
+ exitCode: null,
19347
+ stdout: redactSecrets(stdout),
19348
+ stderr: redactSecrets(stderr),
19349
+ error: redactSecrets(error.message)
19350
+ });
19351
+ });
19352
+ child.on("close", (code)=>{
19353
+ resolve({
19354
+ exitCode: code,
19355
+ stdout: redactSecrets(stdout),
19356
+ stderr: redactSecrets(stderr)
19357
+ });
19358
+ });
19359
+ child.stdin.end(input);
19360
+ });
19361
+ }
19362
+ async function buildAppReport(repoPath, remote, deps) {
19363
+ const appId = remote.appId;
19364
+ const anvilUrl = remote.anvilUrl;
19365
+ const url = new URL(anvilUrl);
19366
+ const credentialScope = `${url.protocol}//${url.host}`;
19367
+ const helperConfigKey = `credential.${credentialScope}.helper`;
19368
+ const helperValues = await getLocalConfigValues(repoPath, helperConfigKey);
19369
+ const nonEmptyHelperValues = helperValues.filter((value)=>"" !== value.trim());
19370
+ const helperDetails = nonEmptyHelperValues.map(inspectCredentialHelperCommand);
19371
+ const findings = [];
19372
+ if (0 === helperValues.length) findings.push(finding("error", "missing_helper_config", `No local ${helperConfigKey} entries found.`));
19373
+ else if (0 === nonEmptyHelperValues.length) findings.push(finding("error", "missing_helper_command", `${helperConfigKey} only resets helpers; no Anvil helper command is configured.`));
19374
+ for (const helper of helperDetails){
19375
+ if (helper.parseError) findings.push(finding("error", "helper_parse_failed", `Could not parse helper command: ${helper.parseError}`));
19376
+ if (helper.execPath && false === helper.execExists) findings.push(finding("error", "helper_exec_missing", `Credential helper Node executable does not exist: ${helper.execPath}`));
19377
+ if (helper.helperPath) {
19378
+ if (false === helper.helperExists) findings.push(finding("error", "helper_file_missing", `Credential helper script does not exist: ${helper.helperPath}`));
19379
+ else if (false === helper.helperExecutable) findings.push(finding("warning", "helper_not_executable", `Credential helper script is not executable: ${helper.helperPath}`));
19380
+ } else findings.push(finding("error", "helper_path_missing", "Credential helper command does not include a helper script path."));
19381
+ if (helper.modulePath && false === helper.moduleExists) findings.push(finding("error", "helper_module_missing", `Generated helper points at a missing module: ${helper.modulePath}`));
19382
+ 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}`));
19383
+ }
19384
+ const binding = await deps.getAppAuthBinding(repoPath, appId);
19385
+ if (!binding.url) findings.push(finding("warning", "missing_app_binding_url", `No anvil.auth.${appId}.url binding is configured.`));
19386
+ if (!binding.username) findings.push(finding("warning", "missing_app_binding_username", `No anvil.auth.${appId}.username binding is configured.`));
19387
+ const authUrl = binding.url ? config_normalizeAnvilUrl(binding.url) : anvilUrl;
19388
+ let accounts = [];
19389
+ let authUsername = binding.username;
19390
+ let hasUsableAuth = false;
19391
+ try {
19392
+ accounts = await deps.getAccountsForUrl(authUrl);
19393
+ if (authUsername || 1 !== accounts.length) {
19394
+ 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.`));
19395
+ } else authUsername = accounts[0];
19396
+ hasUsableAuth = authUsername ? await deps.hasTokensForUrl(authUrl, authUsername) : accounts.length <= 1 && await deps.hasTokensForUrl(authUrl);
19397
+ 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}.`));
19398
+ } catch (e) {
19399
+ findings.push(finding("error", "auth_lookup_failed", `Auth lookup failed: ${redactSecrets(e.message)}`));
19400
+ }
19401
+ const fillResult = await deps.runCredentialFill(repoPath, buildCredentialInput(anvilUrl, appId));
19402
+ const credentialSimulation = {
19403
+ status: 0 === fillResult.exitCode && /^password=/im.test(fillResult.stdout) ? "ok" : "error",
19404
+ exitCode: fillResult.exitCode,
19405
+ stdout: redactSecrets(fillResult.stdout),
19406
+ stderr: redactSecrets(fillResult.stderr),
19407
+ error: fillResult.error ? redactSecrets(fillResult.error) : void 0
19408
+ };
19409
+ if ("ok" !== credentialSimulation.status) findings.push(finding("error", "credential_fill_failed", credentialSimulation.error || credentialSimulation.stderr.trim() || "git credential fill did not return a password."));
19410
+ const report = {
19411
+ appId,
19412
+ anvilUrl,
19413
+ remoteName: remote.name,
19414
+ remoteUrl: remote.fetchUrl,
19415
+ credentialScope,
19416
+ helperConfigKey,
19417
+ helperValues,
19418
+ helperDetails,
19419
+ binding: {
19420
+ url: binding.url,
19421
+ username: binding.username,
19422
+ hasUrl: !!binding.url,
19423
+ hasUsername: !!binding.username
19424
+ },
19425
+ auth: {
19426
+ url: authUrl,
19427
+ username: authUsername,
19428
+ accounts,
19429
+ hasUsableAuth
19430
+ },
19431
+ credentialSimulation,
19432
+ findings
19433
+ };
19434
+ return report;
19435
+ }
19436
+ async function buildGitAuthDoctorReport(repoPathInput = process.cwd(), options = {}) {
19437
+ const deps = options.deps ?? defaultGitAuthDoctorDeps;
19438
+ const inputPath = external_path_default().resolve(repoPathInput);
19439
+ const git = esm_default(inputPath);
19440
+ const repoPath = (await git.raw([
19441
+ "rev-parse",
19442
+ "--show-toplevel"
19443
+ ])).trim();
19444
+ const rawGitDir = (await git.raw([
19445
+ "rev-parse",
19446
+ "--git-dir"
19447
+ ])).trim();
19448
+ const gitDir = external_path_default().isAbsolute(rawGitDir) ? rawGitDir : external_path_default().resolve(inputPath, rawGitDir);
19449
+ auth_setRepoContext(repoPath);
19450
+ const remotes = (await git.getRemotes(true)).map(detectAnvilRemote);
19451
+ const anvilRemotes = remotes.filter((remote)=>remote.appId && remote.anvilUrl && "https" === remote.transport);
19452
+ const findings = [];
19453
+ if (0 === anvilRemotes.length) findings.push(finding("error", "missing_anvil_https_remote", "No HTTPS Anvil git remotes were detected."));
19454
+ const apps = await Promise.all(anvilRemotes.map((remote)=>buildAppReport(repoPath, remote, deps)));
19455
+ const status = getHighestStatus([
19456
+ ...findings.map((item)=>item.status),
19457
+ ...apps.flatMap((app)=>app.findings.map((item)=>item.status))
19458
+ ]);
19459
+ return {
19460
+ repoPath,
19461
+ gitDir,
19462
+ remotes,
19463
+ apps,
19464
+ findings,
19465
+ status
19466
+ };
19467
+ }
19468
+ function formatStatus(status) {
19469
+ if ("ok" === status) return external_chalk_default().green("[ok]");
19470
+ if ("warning" === status) return external_chalk_default().yellow("[warn]");
19471
+ return external_chalk_default().red("[error]");
19472
+ }
19473
+ function formatGitAuthDoctorReport(report) {
19474
+ const lines = [];
19475
+ lines.push(external_chalk_default().bold("Git auth doctor"));
19476
+ lines.push(`Repository: ${report.repoPath}`);
19477
+ lines.push(`Git dir: ${report.gitDir}`);
19478
+ lines.push(`Overall: ${formatStatus(report.status)}`);
19479
+ lines.push("");
19480
+ lines.push(external_chalk_default().bold("Detected remotes:"));
19481
+ if (0 === report.remotes.length) lines.push(" (none)");
19482
+ else for (const remote of report.remotes){
19483
+ const appText = remote.appId && remote.anvilUrl ? ` -> ${remote.appId} on ${remote.anvilUrl}` : "";
19484
+ lines.push(` - ${remote.name}: ${remote.fetchUrl || "(no fetch URL)"}${appText}`);
19485
+ }
19486
+ for (const app of report.apps){
19487
+ lines.push("");
19488
+ lines.push(external_chalk_default().bold(`App ${app.appId} (${app.remoteName})`));
19489
+ lines.push(` URL: ${app.anvilUrl}`);
19490
+ lines.push(` Credential scope: ${app.credentialScope}`);
19491
+ lines.push(` Helper config: ${app.helperConfigKey}`);
19492
+ if (0 === app.helperValues.length) lines.push(" (none)");
19493
+ else for (const value of app.helperValues)lines.push(` - ${value || "(reset helper list)"}`);
19494
+ for (const helper of app.helperDetails){
19495
+ lines.push(` Helper command: ${helper.raw}`);
19496
+ lines.push(` Node: ${helper.execPath || "(missing)"} ${false === helper.execExists ? "[missing]" : ""}`.trimEnd());
19497
+ lines.push(` Script: ${helper.helperPath || "(missing)"} ${false === helper.helperExists ? "[missing]" : ""}`.trimEnd());
19498
+ if (helper.modulePath) lines.push(` Module: ${helper.modulePath} ${false === helper.moduleExists ? "[missing]" : ""}`.trimEnd());
19499
+ }
19500
+ lines.push(` Binding URL: ${app.binding.url || "(missing)"}`);
19501
+ lines.push(` Binding user: ${app.binding.username || "(missing)"}`);
19502
+ lines.push(` Auth tokens: ${app.auth.hasUsableAuth ? "available" : "not available"}${app.auth.username ? ` for ${app.auth.username}` : ""}`);
19503
+ if (app.credentialSimulation) {
19504
+ lines.push(` Credential fill: ${formatStatus(app.credentialSimulation.status)} exit=${app.credentialSimulation.exitCode}`);
19505
+ if (app.credentialSimulation.stdout.trim()) {
19506
+ lines.push(" stdout:");
19507
+ for (const line of app.credentialSimulation.stdout.trim().split(/\r?\n/))lines.push(` ${line}`);
19508
+ }
19509
+ if (app.credentialSimulation.stderr.trim()) {
19510
+ lines.push(" stderr:");
19511
+ for (const line of app.credentialSimulation.stderr.trim().split(/\r?\n/))lines.push(` ${line}`);
19512
+ }
19513
+ }
19514
+ if (app.findings.length > 0) {
19515
+ lines.push(" Findings:");
19516
+ for (const item of app.findings)lines.push(` ${formatStatus(item.status)} ${item.code}: ${item.message}`);
19517
+ }
19518
+ }
19519
+ if (report.findings.length > 0) {
19520
+ lines.push("");
19521
+ lines.push(external_chalk_default().bold("Repository findings:"));
19522
+ for (const item of report.findings)lines.push(` ${formatStatus(item.status)} ${item.code}: ${item.message}`);
19523
+ }
19524
+ return lines.join("\n");
19525
+ }
19003
19526
  const gitCredential_defaultDeps = {
19004
19527
  getValidAuthToken: auth_getValidAuthToken,
19005
19528
  getAccountsForUrl: auth_getAccountsForUrl,
@@ -19457,6 +19980,7 @@ exports.ValidationIssue = __webpack_exports__.ValidationIssue;
19457
19980
  exports.ValidationTarget = __webpack_exports__.ValidationTarget;
19458
19981
  exports.WatchSession = __webpack_exports__.WatchSession;
19459
19982
  exports.branchFromEnvironmentVersion = __webpack_exports__.branchFromEnvironmentVersion;
19983
+ exports.buildGitAuthDoctorReport = __webpack_exports__.buildGitAuthDoctorReport;
19460
19984
  exports.checkUncommittedChanges = __webpack_exports__.checkUncommittedChanges;
19461
19985
  exports.clearInMemoryTokens = __webpack_exports__.clearInMemoryTokens;
19462
19986
  exports.createWatchDiagnostic = __webpack_exports__.createWatchDiagnostic;
@@ -19468,6 +19992,7 @@ exports.executeCheckout = __webpack_exports__.executeCheckout;
19468
19992
  exports.executeGitCredentialOperation = __webpack_exports__.executeGitCredentialOperation;
19469
19993
  exports.filterCandidates = __webpack_exports__.filterCandidates;
19470
19994
  exports.formatCandidateLabel = __webpack_exports__.formatCandidateLabel;
19995
+ exports.formatGitAuthDoctorReport = __webpack_exports__.formatGitAuthDoctorReport;
19471
19996
  exports.formatValidationPath = __webpack_exports__.formatValidationPath;
19472
19997
  exports.getAccountsForUrl = __webpack_exports__.getAccountsForUrl;
19473
19998
  exports.getAllConfig = __webpack_exports__.getAllConfig;
@@ -19483,6 +20008,7 @@ exports.getSettableConfigKeys = __webpack_exports__.getSettableConfigKeys;
19483
20008
  exports.getValidAuthToken = __webpack_exports__.getValidAuthToken;
19484
20009
  exports.getWebSocketUrl = __webpack_exports__.getWebSocketUrl;
19485
20010
  exports.hasTokensForUrl = __webpack_exports__.hasTokensForUrl;
20011
+ exports.inspectCredentialHelperCommand = __webpack_exports__.inspectCredentialHelperCommand;
19486
20012
  exports.isCommandAvailable = __webpack_exports__.isCommandAvailable;
19487
20013
  exports.isDirectoryNonEmpty = __webpack_exports__.isDirectoryNonEmpty;
19488
20014
  exports.isPathInsideGitRepo = __webpack_exports__.isPathInsideGitRepo;
@@ -19493,6 +20019,7 @@ exports.lookupRemoteInfoForAppId = __webpack_exports__.lookupRemoteInfoForAppId;
19493
20019
  exports.normalizeAnvilUrl = __webpack_exports__.normalizeAnvilUrl;
19494
20020
  exports.parseCheckoutInput = __webpack_exports__.parseCheckoutInput;
19495
20021
  exports.parseConfigSetValue = __webpack_exports__.parseConfigSetValue;
20022
+ exports.parseCredentialHelperCommand = __webpack_exports__.parseCredentialHelperCommand;
19496
20023
  exports.pollDeviceAuthorization = __webpack_exports__.pollDeviceAuthorization;
19497
20024
  exports.preferredEditors = __webpack_exports__.preferredEditors;
19498
20025
  exports.readEnvironmentPid = __webpack_exports__.readEnvironmentPid;
@@ -19544,6 +20071,7 @@ for(var __rspack_i in __webpack_exports__)if (-1 === [
19544
20071
  "ValidationTarget",
19545
20072
  "WatchSession",
19546
20073
  "branchFromEnvironmentVersion",
20074
+ "buildGitAuthDoctorReport",
19547
20075
  "checkUncommittedChanges",
19548
20076
  "clearInMemoryTokens",
19549
20077
  "createWatchDiagnostic",
@@ -19555,6 +20083,7 @@ for(var __rspack_i in __webpack_exports__)if (-1 === [
19555
20083
  "executeGitCredentialOperation",
19556
20084
  "filterCandidates",
19557
20085
  "formatCandidateLabel",
20086
+ "formatGitAuthDoctorReport",
19558
20087
  "formatValidationPath",
19559
20088
  "getAccountsForUrl",
19560
20089
  "getAllConfig",
@@ -19570,6 +20099,7 @@ for(var __rspack_i in __webpack_exports__)if (-1 === [
19570
20099
  "getValidAuthToken",
19571
20100
  "getWebSocketUrl",
19572
20101
  "hasTokensForUrl",
20102
+ "inspectCredentialHelperCommand",
19573
20103
  "isCommandAvailable",
19574
20104
  "isDirectoryNonEmpty",
19575
20105
  "isPathInsideGitRepo",
@@ -19580,6 +20110,7 @@ for(var __rspack_i in __webpack_exports__)if (-1 === [
19580
20110
  "normalizeAnvilUrl",
19581
20111
  "parseCheckoutInput",
19582
20112
  "parseConfigSetValue",
20113
+ "parseCredentialHelperCommand",
19583
20114
  "pollDeviceAuthorization",
19584
20115
  "preferredEditors",
19585
20116
  "readEnvironmentPid",