( state: SyncState, )
| 887 | * just did with no recourse. |
| 888 | */ |
| 889 | export async function pushTeamMemory( |
| 890 | state: SyncState, |
| 891 | ): Promise<TeamMemorySyncPushResult> { |
| 892 | const startTime = Date.now() |
| 893 | let conflictRetries = 0 |
| 894 | |
| 895 | if (!isUsingOAuth()) { |
| 896 | logPush(startTime, { success: false, errorType: 'no_oauth' }) |
| 897 | return { |
| 898 | success: false, |
| 899 | filesUploaded: 0, |
| 900 | error: 'OAuth not available', |
| 901 | errorType: 'no_oauth', |
| 902 | } |
| 903 | } |
| 904 | |
| 905 | const repoSlug = await getGithubRepo() |
| 906 | if (!repoSlug) { |
| 907 | logPush(startTime, { success: false, errorType: 'no_repo' }) |
| 908 | return { |
| 909 | success: false, |
| 910 | filesUploaded: 0, |
| 911 | error: 'No git remote found', |
| 912 | errorType: 'no_repo', |
| 913 | } |
| 914 | } |
| 915 | |
| 916 | // Read local entries once at the start. Conflict resolution does NOT re-read |
| 917 | // from disk — the delta computation against a refreshed serverChecksums naturally |
| 918 | // excludes server-origin content, so the user's local edit cannot be clobbered. |
| 919 | // Secret scanning (PSR M22174) happens here once — files with detected |
| 920 | // secrets are excluded from the upload set. |
| 921 | const localRead = await readLocalTeamMemory(state.serverMaxEntries) |
| 922 | const entries = localRead.entries |
| 923 | const skippedSecrets = localRead.skippedSecrets |
| 924 | if (skippedSecrets.length > 0) { |
| 925 | // Log a user-visible warning listing which files were skipped and why. |
| 926 | // Don't block the push — just exclude those files. The secret VALUE is |
| 927 | // never logged, only the type label. |
| 928 | const summary = skippedSecrets |
| 929 | .map(s => `"${s.path}" (${s.label})`) |
| 930 | .join(', ') |
| 931 | logForDebugging( |
| 932 | `team-memory-sync: ${skippedSecrets.length} file(s) skipped due to detected secrets: ${summary}. Remove the secret(s) to enable sync for these files.`, |
| 933 | { level: 'warn' }, |
| 934 | ) |
| 935 | logEvent('tengu_team_mem_secret_skipped', { |
| 936 | file_count: skippedSecrets.length, |
| 937 | // Only log gitleaks rule IDs (not values, not paths — paths could |
| 938 | // leak repo structure). Comma-joined for compact single-field analytics. |
| 939 | rule_ids: skippedSecrets |
| 940 | .map(s => s.ruleId) |
| 941 | .join( |
| 942 | ',', |
| 943 | ) as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, |
| 944 | }) |
| 945 | } |
| 946 |
no test coverage detected