Skip to content

Commit 14ea213

Browse files
authored
Merge pull request #14 from 0xtbug/dev
feat(providers): add bulk refresh token management & improve artifact handling
2 parents cef0b4d + deed565 commit 14ea213

File tree

13 files changed

+425
-28
lines changed

13 files changed

+425
-28
lines changed

.github/workflows/build-release.yml

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -439,7 +439,7 @@ jobs:
439439
run: |
440440
VERSION="${{ needs.create-release.outputs.version }}"
441441
442-
echo "Listing build output directory:"
442+
echo "=== Listing build output directory ==="
443443
ls -R src-tauri/target/release/bundle/
444444
445445
# Debian (using wildcard for safety)
@@ -451,15 +451,19 @@ jobs:
451451
# AppImage (using wildcard for safety)
452452
find src-tauri/target/release/bundle/appimage/ -name "*_${VERSION}_amd64.AppImage" -exec mv {} "ZeroLimit_${VERSION}_Linux_x64.AppImage" \;
453453
454-
# AppImage.tar.gz (optional updater artifact)
455-
find src-tauri/target/release/bundle/appimage/ -name "*_${VERSION}_amd64.AppImage.tar.gz" -exec mv {} "ZeroLimit_${VERSION}_Linux_x64.AppImage.tar.gz" \;
456-
find src-tauri/target/release/bundle/appimage/ -name "*_${VERSION}_amd64.AppImage.tar.gz.sig" -exec mv {} "ZeroLimit_${VERSION}_Linux_x64.AppImage.tar.gz.sig" \;
454+
# AppImage.tar.gz and signature (updater artifacts)
455+
find src-tauri/target/release/bundle/appimage/ -name "*.AppImage.tar.gz" ! -name "*.sig" -exec mv {} "ZeroLimit_${VERSION}_Linux_x64.AppImage.tar.gz" \;
456+
find src-tauri/target/release/bundle/appimage/ -name "*.AppImage.tar.gz.sig" -exec mv {} "ZeroLimit_${VERSION}_Linux_x64.AppImage.tar.gz.sig" \;
457+
458+
echo "=== Prepared artifacts in workspace ==="
459+
ls -la ZeroLimit_* || echo "No artifacts found!"
457460
458461
- name: Upload artifacts to release
459462
uses: softprops/action-gh-release@v2
460463
with:
461464
tag_name: v${{ needs.create-release.outputs.version }}
462465
draft: true
466+
fail_on_unmatched_files: false
463467
files: |
464468
ZeroLimit_*_Linux_x64.deb
465469
ZeroLimit_*_Linux_x64.rpm
@@ -472,6 +476,7 @@ jobs:
472476
with:
473477
name: signatures-linux
474478
path: ZeroLimit_*_Linux_x64.AppImage.tar.gz.sig
479+
if-no-files-found: warn
475480

476481
# ===========================================
477482
# 7. Publish Release + Generate latest.json

AGENTS.md

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
# Repository Guidelines
2+
3+
## Project Structure & Module Organization
4+
- `src/` contains the React + TypeScript frontend, organized by feature slices: `features/`, `shared/`, `layouts/`, `router/`, `i18n/`, `services/`, `types/`, `constants/`.
5+
- `src-tauri/` contains the Rust backend and Tauri configuration.
6+
- `public/` holds static assets used by the web view (icons, etc.).
7+
- `docs/` contains user documentation (see `docs/USAGE.md`).
8+
- `scripts/` includes maintenance utilities such as `scripts/bump-version.sh`.
9+
- Build outputs land in `dist/` (frontend) and `src-tauri/target/` (Tauri).
10+
11+
## Build, Test, and Development Commands
12+
- `pnpm install` installs dependencies (Node >= 20, pnpm 10).
13+
- `pnpm run dev` starts the Vite dev server for the frontend only.
14+
- `pnpm run build` runs TypeScript checks and builds the frontend bundle.
15+
- `pnpm run preview` serves the built frontend for local verification.
16+
- `pnpm run tauri dev` runs the full desktop app in development mode.
17+
- `pnpm run tauri build` builds the production desktop application.
18+
19+
## Coding Style & Naming Conventions
20+
- Indentation: 2 spaces, LF line endings, UTF-8, trim trailing whitespace (see `.editorconfig`).
21+
- Formatting: Prettier with Tailwind plugin, no semicolons, single quotes (see `.prettierrc`).
22+
- Prefer clear, descriptive names; keep React components in `PascalCase` and hooks in `camelCase` (e.g., `useQuotaStore`).
23+
24+
## Testing Guidelines
25+
- There is no dedicated automated test suite yet.
26+
- Before submitting changes, run `pnpm run build` and perform a manual smoke test via `pnpm run tauri dev`.
27+
- If you add tests, co-locate them near the feature and use a `*.test.ts(x)` naming pattern.
28+
29+
## Commit & Pull Request Guidelines
30+
- Commit messages follow a Conventional Commit style seen in history: `feat(scope): ...`, `fix: ...`, `docs(README): ...`, `build(release): ...`.
31+
- Keep commits focused and scoped; prefer multiple small commits over a single large one.
32+
- Pull requests should include a concise summary, steps to verify, and screenshots for UI changes.
33+
- Link related issues or discussions when applicable.
34+
35+
## Security & Configuration Notes
36+
- The app depends on CLIProxyAPI and external provider auth; avoid committing secrets or tokens.
37+
- If you touch OAuth flows or updater logic, test on all supported platforms where possible.

src/features/providers/ProvidersPage.tsx

Lines changed: 151 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,17 @@ import {
1515
Eye,
1616
EyeOff,
1717
ChevronDown,
18-
ChevronRight
18+
ChevronRight,
19+
Key
1920
} from 'lucide-react';
21+
import {
22+
Dialog,
23+
DialogContent,
24+
DialogDescription,
25+
DialogFooter,
26+
DialogHeader,
27+
DialogTitle,
28+
} from '@/shared/components/ui/dialog';
2029
import {
2130
AlertDialog,
2231
AlertDialogAction,
@@ -62,6 +71,20 @@ export function ProvidersPage() {
6271
isPrivacyMode,
6372
togglePrivacyMode,
6473
openInBrowser,
74+
isDeletingAll,
75+
executeDeleteAll,
76+
showDeleteAllConfirmation,
77+
setShowDeleteAllConfirmation,
78+
copyRefreshToken,
79+
80+
// Copy All
81+
showCopyAllModal,
82+
setShowCopyAllModal,
83+
copyingAll,
84+
selectedProvidersForCopy,
85+
openCopyAllModal,
86+
toggleCopyProvider,
87+
executeCopyAll,
6588
} = useProvidersPresenter();
6689

6790
if (!isAuthenticated) {
@@ -99,6 +122,37 @@ export function ProvidersPage() {
99122
</AlertDialogContent>
100123
</AlertDialog>
101124

125+
<AlertDialog open={showDeleteAllConfirmation} onOpenChange={setShowDeleteAllConfirmation}>
126+
<AlertDialogContent className="border-border/50">
127+
<AlertDialogHeader>
128+
<AlertDialogTitle>{t('common.confirm', 'Are you sure?')}</AlertDialogTitle>
129+
<AlertDialogDescription>
130+
{t('providers.deleteAllConfirm', 'This will permanently delete all connected accounts. This action cannot be undone.')}
131+
</AlertDialogDescription>
132+
</AlertDialogHeader>
133+
<AlertDialogFooter>
134+
<AlertDialogCancel disabled={isDeletingAll}>{t('common.cancel')}</AlertDialogCancel>
135+
<AlertDialogAction
136+
onClick={(e) => {
137+
e.preventDefault();
138+
executeDeleteAll();
139+
}}
140+
className="bg-red-500 hover:bg-red-600 text-white"
141+
disabled={isDeletingAll}
142+
>
143+
{isDeletingAll ? (
144+
<>
145+
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
146+
{t('common.deleting', 'Deleting...')}
147+
</>
148+
) : (
149+
t('common.deleteAll', 'Delete All')
150+
)}
151+
</AlertDialogAction>
152+
</AlertDialogFooter>
153+
</AlertDialogContent>
154+
</AlertDialog>
155+
102156
<div className="flex items-center justify-between">
103157
<h1 className="text-3xl font-bold">{t('providers.title')}</h1>
104158

@@ -119,6 +173,28 @@ export function ProvidersPage() {
119173
<CheckCircle className="h-5 w-5" />
120174
{t('providers.connectedAccounts')} ({files.length})
121175
</h2>
176+
{files.length > 0 && (
177+
<div className="flex items-center gap-2">
178+
<Button
179+
variant="outline"
180+
size="sm"
181+
onClick={openCopyAllModal}
182+
className="h-8 text-xs"
183+
>
184+
<Key className="mr-2 h-3.5 w-3.5" />
185+
{t('common.copyAll', 'Copy All')}
186+
</Button>
187+
<Button
188+
variant="outline"
189+
size="sm"
190+
onClick={() => setShowDeleteAllConfirmation(true)}
191+
className="h-8 text-xs bg-red-500/10 text-red-500 hover:bg-red-500/20 shadow-none border border-red-500/20"
192+
>
193+
<Trash2 className="mr-2 h-3.5 w-3.5" />
194+
{t('common.deleteAll', 'Delete All')}
195+
</Button>
196+
</div>
197+
)}
122198
</div>
123199

124200
{filesError && (
@@ -216,15 +292,26 @@ export function ProvidersPage() {
216292
</div>
217293
</div>
218294

219-
<Button
220-
size="icon"
221-
variant="ghost"
222-
className="h-8 w-8 text-destructive hover:bg-destructive/10 opacity-80 group-hover:opacity-100"
223-
onClick={() => setFileToDelete(file.id)}
224-
title={t('common.delete')}
225-
>
226-
<Trash2 className="h-4 w-4" />
227-
</Button>
295+
<div className="flex items-center gap-1">
296+
<Button
297+
size="icon"
298+
variant="ghost"
299+
className="h-8 w-8 text-primary hover:bg-primary/10 opacity-80 group-hover:opacity-100"
300+
onClick={() => copyRefreshToken(file.name || file.filename || file.id)}
301+
title={t('common.copyRefreshToken', 'Copy Refresh Token')}
302+
>
303+
<Key className="h-4 w-4" />
304+
</Button>
305+
<Button
306+
size="icon"
307+
variant="ghost"
308+
className="h-8 w-8 text-destructive hover:bg-destructive/10 opacity-80 group-hover:opacity-100"
309+
onClick={() => setFileToDelete(file.id)}
310+
title={t('common.delete')}
311+
>
312+
<Trash2 className="h-4 w-4" />
313+
</Button>
314+
</div>
228315
</div>
229316
);
230317
})}
@@ -416,6 +503,60 @@ export function ProvidersPage() {
416503
})}
417504
</div>
418505
</section>
506+
507+
<Dialog open={showCopyAllModal} onOpenChange={setShowCopyAllModal}>
508+
<DialogContent className="sm:max-w-[425px]">
509+
<DialogHeader>
510+
<DialogTitle>{t('providers.copyAllTitle', 'Copy All Refresh Tokens')}</DialogTitle>
511+
<DialogDescription>
512+
{t('providers.selectProviders', 'Select the providers you want to include in the copy.')}
513+
</DialogDescription>
514+
</DialogHeader>
515+
<div className="py-4">
516+
<div className="space-y-4">
517+
{groupedFiles.map(([providerId, group]) => (
518+
<div key={providerId} className="flex items-center space-x-2">
519+
<input
520+
type="checkbox"
521+
id={`copy-${providerId}`}
522+
checked={selectedProvidersForCopy.includes(providerId)}
523+
onChange={() => toggleCopyProvider(providerId)}
524+
className="h-4 w-4 rounded border-gray-300 text-primary focus:ring-primary accent-primary"
525+
/>
526+
<label
527+
htmlFor={`copy-${providerId}`}
528+
className="text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70 flex items-center gap-2 cursor-pointer select-none"
529+
>
530+
<img
531+
src={group.iconInfo.path}
532+
alt={group.displayName}
533+
className={`h-4 w-4 object-contain ${group.iconInfo.needsInvert ? 'invert-on-dark' : ''}`}
534+
/>
535+
{group.displayName}
536+
<span className="text-xs text-muted-foreground">({group.files.length})</span>
537+
</label>
538+
</div>
539+
))}
540+
</div>
541+
</div>
542+
<DialogFooter>
543+
<Button variant="outline" onClick={() => setShowCopyAllModal(false)}>{t('common.cancel')}</Button>
544+
<Button onClick={executeCopyAll} disabled={copyingAll || selectedProvidersForCopy.length === 0}>
545+
{copyingAll ? (
546+
<>
547+
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
548+
{t('common.copying', 'Copying...')}
549+
</>
550+
) : (
551+
<>
552+
<Key className="mr-2 h-4 w-4" />
553+
{t('common.copy', 'Copy')}
554+
</>
555+
)}
556+
</Button>
557+
</DialogFooter>
558+
</DialogContent>
559+
</Dialog>
419560
</div>
420561
);
421562
}

0 commit comments

Comments
 (0)