Files
spark-store/AGENTS.md

894 lines
23 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# AI Coding Guidance for APM App Store
**Repository:** elysia-best/apm-app-store
**Project Type:** Electron + Vue 3 + Vite Desktop Application
**Purpose:** Desktop app store client for APM (AmberPM) package manager
**License:** MulanPSL-2.0
---
If you are an AI coding agent working on this repo, make sure to follow the guidelines below:
## 🏗️ Project Architecture Overview
### Technology Stack
- **Frontend Framework:** Vue 3 with Composition API (`<script setup>`)
- **Build Tool:** Vite 6.4.1
- **Desktop Framework:** Electron 40.0.0
- **UI Framework:** Tailwind CSS 4.1.18
- **Language:** TypeScript (strict mode enabled)
- **State Management:** Vue reactivity system (ref, computed)
- **HTTP Client:** Axios 1.13.2
- **Logging:** Pino logger
### Directory Structure
```
apm-app-store/
├── electron/ # Electron main process
│ ├── main/
│ │ ├── backend/ # Backend logic (e.g. install manager)
│ │ │ └── install-manager.ts # Core installation/package management
│ │ ├── deeplink.ts # Deep link protocol handling
│ │ ├── handle-url-scheme.ts # URL scheme handler
│ │ └── index.ts # Main process entry point
│ ├── preload/
│ │ └── index.ts # Preload script (IPC bridge)
│ └── global.ts # Shared state between processes
├── src/ # Vue renderer process
│ ├── components/ # Vue components (modals, cards, grids)
│ ├── global/ # Global config and state
│ │ ├── downloadStatus.ts # Download queue management
│ │ ├── storeConfig.ts # API config and shared state
│ │ └── typedefinition.ts # TypeScript type definitions
│ ├── modules/ # Business logic modules
│ │ └── processInstall.ts # Install/uninstall logic
│ ├── assets/ # CSS/images
│ ├── App.vue # Root component
│ └── main.ts # Renderer entry point
├── extras/ # Shell scripts and policy files
├── icons/ # Application icons
├── scripts/ # Maintenance scripts
├── public/ # Public assets
├── electron-builder.yml # Build configuration
├── vite.config.ts # Vite configuration
├── tsconfig.json # TypeScript configuration
├── eslint.config.ts # ESLint configuration
└── package.json # Dependencies and scripts
```
---
## 🎯 Core Concepts
### 1. APM Package Manager Integration
The app acts as a GUI frontend for the APM CLI tool (`/opt/spark-store/extras/shell-caller.sh`).
**Key Operations:**
- `apm install -y <pkgname>` - Install package
- `apm remove -y <pkgname>` - Uninstall package
- `apm list --installed` - List installed packages
- `apm list --upgradable` - List upgradable packages
**Important:** All APM operations require privilege escalation via `pkexec` on Linux.
### 2. IPC Communication Pattern
**Main Process (Node.js) ⟷ Renderer Process (Browser)**
```typescript
// In Renderer (Vue):
window.ipcRenderer.send('queue-install', JSON.stringify(download));
window.ipcRenderer.invoke('list-installed');
window.ipcRenderer.on('install-complete', (event, result) => { /* ... */ });
// In Main Process (Electron):
ipcMain.on('queue-install', async (event, download_json) => { /* ... */ });
ipcMain.handle('list-installed', async () => { /* ... */ });
event.sender.send('install-complete', { id, success, ... });
```
**Exposed APIs in Preload:**
- `window.ipcRenderer.on/off/send/invoke` - IPC communication
- `window.apm_store.arch` - System architecture detection (amd64-store, arm64-store)
### 3. Installation Queue System
**Location:** `electron/main/backend/install-manager.ts`
**Key Features:**
- Single-task sequential processing (only one installation at a time)
- Task queue managed via `Map<number, InstallTask>`
- Real-time progress streaming via IPC events
- Automatic privilege escalation detection
**Task Lifecycle:**
1. Renderer sends `queue-install` with task ID and package name
2. Main process checks for duplicate tasks
3. Task added to queue with status `queued`
4. `processNextInQueue()` spawns child process
5. stdout/stderr streamed to renderer via `install-log`
6. Completion signaled via `install-complete` with exit code
**Critical Pattern:**
```typescript
// Always check if idle before processing
if (idle) processNextInQueue(0);
// After task completion, check for more tasks
tasks.delete(task.id);
idle = true;
if (tasks.size > 0) processNextInQueue(0);
```
---
## 📝 Type System Guidelines
Important: DO NOT use any in the code!
### Core Types (src/global/typedefinition.ts)
#### App Data Structure
```typescript
// Raw JSON from API (PascalCase)
interface AppJson {
Name: string;
Pkgname: string;
Version: string;
Filename: string;
// ... (follows upstream API format)
}
// Normalized app data (camelCase)
interface App {
name: string;
pkgname: string; // Primary identifier
version: string;
category: string; // Added by frontend
currentStatus: "not-installed" | "installed";
flags?: string; // e.g., "automatic" for dependencies
arch?: string; // e.g., "amd64", "arm64"
// ... (full spec in typedefinition.ts)
}
```
#### Download/Installation Task
```typescript
interface DownloadItem {
id: number; // Unique task ID
pkgname: string; // Package identifier
status: DownloadItemStatus; // "queued" | "installing" | "completed" | "failed" | "paused"
progress: number; // 0-100
logs: Array<{ time: number; message: string }>;
retry: boolean; // Retry flag
upgradeOnly?: boolean; // For upgrade operations
// ... (see full spec)
}
```
#### IPC Payloads
```typescript
interface InstallLog {
id: number;
time: number;
message: string;
}
interface DownloadResult extends InstallLog {
success: boolean;
exitCode: number | null;
}
```
---
## 🎨 Vue Component Patterns
### Composition API Best Practices
```typescript
<script setup lang="ts">
import { ref, computed, onMounted } from 'vue';
import type { Ref } from 'vue';
// Reactive state
const apps: Ref<App[]> = ref([]);
const loading = ref(true);
// Computed properties
const filteredApps = computed(() => {
return apps.value.filter(/* ... */);
});
// Lifecycle hooks
onMounted(async () => {
await loadCategories();
await loadApps();
});
// Methods (use arrow functions or function declarations)
const handleInstall = () => {
// Implementation
};
</script>
```
### Props and Events Pattern
```typescript
// Props definition
const props = defineProps<{
app: App | null;
show: boolean;
}>();
// Emits definition
const emit = defineEmits<{
close: [];
install: [];
remove: [];
}>();
// Usage
emit('install');
```
### IPC Event Listeners in Vue
**Always use in `onMounted` for proper cleanup:**
```typescript
onMounted(() => {
window.ipcRenderer.on('install-complete', (_event: IpcRendererEvent, result: DownloadResult) => {
// Handle event
});
window.ipcRenderer.on('install-log', (_event: IpcRendererEvent, log: InstallLog) => {
// Handle log
});
});
```
---
## 🔧 Main Process Patterns
### Spawning APM Commands
```typescript
import { spawn } from 'node:child_process';
// Check for privilege escalation
const superUserCmd = await checkSuperUserCommand();
const execCommand = superUserCmd.length > 0 ? superUserCmd : SHELL_CALLER_PATH;
const execParams = superUserCmd.length > 0
? [SHELL_CALLER_PATH, 'apm', 'install', '-y', pkgname]
: ['apm', 'install', '-y', pkgname];
// Spawn process
const child = spawn(execCommand, execParams, {
shell: true,
env: process.env,
});
// Stream output
child.stdout.on('data', (data) => {
webContents.send('install-log', { id, time: Date.now(), message: data.toString() });
});
// Handle completion
child.on('close', (code) => {
const success = code === 0;
webContents.send('install-complete', { id, success, exitCode: code, /* ... */ });
});
```
### Parsing APM Output
**APM outputs are text-based with specific formats:**
```typescript
// Installed packages format: "pkgname/repo,version arch [flags]"
// Example: "code/stable,1.108.2 amd64 [installed]"
const parseInstalledList = (output: string) => {
const apps: InstalledAppInfo[] = [];
const lines = output.split('\n');
for (const line of lines) {
const match = line.trim().match(/^(\S+)\/\S+,\S+\s+(\S+)\s+(\S+)\s+\[(.+)\]$/);
if (match) {
apps.push({
pkgname: match[1],
version: match[2],
arch: match[3],
flags: match[4],
raw: line.trim(),
});
}
}
return apps;
};
```
---
## 🌐 API Integration
### Base Configuration
```typescript
// src/global/storeConfig.ts
export const APM_STORE_BASE_URL = 'https://erotica.spark-app.store';
// URL structure:
// /{arch}/{category}/applist.json - App list
// /{arch}/{category}/{pkgname}/icon.png - App icon
// /{arch}/{category}/{pkgname}/screen_N.png - Screenshots (1-5)
// /{arch}/categories.json - Categories mapping
### Home ()
The store may provide a special `home` directory under `{arch}` for a localized homepage. Two JSON files are expected:
- `homelinks.json` :
```json
{
"name": "交流反馈",
"more": "前往论坛交流讨论",
"imgUrl": "/home/links/bbs.png",
"type": "_blank",
"url": "https://bbs.spark-app.store/"
}
```
- `homelist.json` — 描述若干推荐应用列表,每项引用一个 JSON 列表(`jsonUrl`:
```json
[
{ "name":"装机必备", "type":"appList", "jsonUrl":"/home/lists/NecessaryforInstallation.json" }
]
```
Parsing rules used by the app:
- Resolve `imgUrl` by prefixing: `${APM_STORE_BASE_URL}/{arch}${imgUrl}`.
- `type: _blank` → 使用系统浏览器打开链接;`type: _self` → 在当前页面打开。
- For `homelist.json` entries with `type: "appList"`, fetch the referenced `jsonUrl` and map each item to the app shape used by the UI:
- `Name``name`
- `Pkgname``pkgname`
- `Category``category`
- `More``more`
Where to implement:
- Renderer: `src/App.vue` loads and normalizes `homelinks.json` and `homelist.json` on selecting the `home` category and exposes data to a new `HomeView` component.
- Component: `src/components/HomeView.vue` renders link cards and recommended app sections (re-uses `AppCard.vue`).
Notes:
- The `home` directory path is: `/{arch}/home/` under the configured `APM_STORE_BASE_URL`.
- Missing or partially invalid files are handled gracefully — individual failures don't block showing other home sections.
```
### Axios Usage
```typescript
const axiosInstance = axios.create({
baseURL: APM_STORE_BASE_URL,
timeout: 1000, // Note: Very short timeout!
});
// Loading apps by category
const response = await axiosInstance.get<AppJson[]>(
`/${window.apm_store.arch}/${category}/applist.json`
);
```
**Development Proxy (vite.config.ts):**
```typescript
server: {
proxy: {
'/local_amd64-store': {
target: 'https://erotica.spark-app.store',
changeOrigin: true,
rewrite: (path) => path.replace(/^\/local_amd64-store/, ''),
}
}
}
```
---
## 🎯 Deep Link Protocol (SPK URI Scheme)
**URL Scheme:** `spk://`
### Supported SPK URI Format
Format: `spk://search/{pkgname}`
**Examples:**
- `spk://search/code` - Search for and open "code" app
- `spk://search/steam` - Search for and open "steam" app
- `spk://search/store.spark-app.hmcl` - Search for and open "HMCL" game
### Implementation Pattern
```typescript
// electron/main/deeplink.ts - Parse command line and route
export function handleCommandLine(commandLine: string[]) {
const deeplinkUrl = commandLine.find((arg) =>
arg.startsWith('spk://')
);
if (!deeplinkUrl) return;
try {
const url = new URL(deeplinkUrl);
const action = url.hostname; // 'search'
if (action === 'search') {
// Format: spk://search/pkgname
// url.pathname will be '/pkgname'
const pkgname = url.pathname.split('/').filter(Boolean)[0];
if (pkgname) {
listeners.emit('search', { pkgname });
}
}
} catch (error) {
logger.error({ err: error }, 'Error parsing SPK URI');
}
}
// src/App.vue - Handle in renderer
window.ipcRenderer.on(
'deep-link-search',
(_event: IpcRendererEvent, data: { pkgname: string }) => {
// Trigger search with the pkgname
searchQuery.value = data.pkgname;
}
);
```
---
## 🛡️ Security Considerations
### Privilege Escalation
**Always check for `pkexec` availability:**
```typescript
const checkSuperUserCommand = async (): Promise<string> => {
if (process.getuid && process.getuid() !== 0) {
const { stdout } = await execAsync('which /usr/bin/pkexec');
return stdout.trim().length > 0 ? '/usr/bin/pkexec' : '';
}
return '';
};
```
### Context Isolation
**Current Status:** Context isolation is **enabled** (default Electron behavior).
**IPC Exposed via Preload (Safe):**
```typescript
// electron/preload/index.ts
contextBridge.exposeInMainWorld('ipcRenderer', {
on: (...args) => ipcRenderer.on(...args),
send: (...args) => ipcRenderer.send(...args),
invoke: (...args) => ipcRenderer.invoke(...args),
});
```
**⚠️ Do NOT enable nodeIntegration or disable contextIsolation!**
---
## 🎨 UI/UX Patterns
### Tailwind CSS Usage
**Dark Mode Support:**
```vue
<div class="bg-slate-50 dark:bg-slate-950 text-slate-900 dark:text-slate-100">
<!-- Content -->
</div>
```
**Theme Toggle:**
```typescript
const isDarkTheme = ref(false);
watch(isDarkTheme, (newVal) => {
localStorage.setItem('theme', newVal ? 'dark' : 'light');
document.documentElement.classList.toggle('dark', newVal);
});
```
### Modal Pattern
```vue
<template>
<div v-if="show" class="fixed inset-0 z-50 flex items-center justify-center">
<div class="absolute inset-0 bg-black/50" @click="closeModal"></div>
<div class="modal-panel relative z-10 bg-white dark:bg-slate-900">
<!-- Modal content -->
</div>
</div>
</template>
```
### Loading States
```typescript
const loading = ref(true);
// In template
<div v-if="loading">Loading...</div>
<div v-else>{{ apps.length }} apps</div>
```
---
## 🧪 Testing & Quality
### ESLint Configuration
```typescript
// eslint.config.ts
export default defineConfig([
globalIgnores(['**/3rdparty/**', '**/node_modules/**', '**/dist/**', '**/dist-electron/**']),
tseslint.configs.recommended,
pluginVue.configs['flat/essential'],
eslintConfigPrettier,
eslintPluginPrettierRecommended,
]);
```
### TypeScript Configuration
```json
{
"compilerOptions": {
"strict": true, // Strict mode enabled
"noEmit": true, // No emit (Vite handles build)
"module": "ESNext",
"target": "ESNext",
"jsx": "preserve", // Vue JSX
"resolveJsonModule": true // Import JSON files
}
}
```
### Code Quality Commands
```bash
npm run lint # Run ESLint
npm run lint:fix # Auto-fix issues
npm run format # Format with Prettier
```
---
## 🚀 Build & Development
### Development Mode
```bash
npm run dev # Start dev server (Vite + Electron)
```
**Dev Server:** `http://127.0.0.1:3344/` (port from package.json)
### Production Build
```bash
npm run build # Build all (deb + rpm)
npm run build:deb # Build Debian package only
npm run build:rpm # Build RPM package only
```
**Build Output:**
- `dist-electron/` - Compiled Electron code
- `dist/` - Compiled renderer assets
- Packaged app in project root
### Build Configuration
**electron-builder.yml:**
- App ID: `cn.eu.org.simplelinux.apmstore`
- Linux targets: deb, rpm
- Includes extras/ directory in resources
- Auto-update disabled (Linux package manager handles updates)
---
## 📦 Important Files to Understand
### 1. electron/main/backend/install-manager.ts
**Purpose:** Core package management logic
**Key Responsibilities:**
- Task queue management
- APM command spawning
- Progress reporting
- Installed/upgradable list parsing
**Critical Functions:**
- `processNextInQueue()` - Task processor
- `parseInstalledList()` - Parse APM output
- `checkSuperUserCommand()` - Privilege escalation
### 2. src/App.vue
**Purpose:** Root component
**Key Responsibilities:**
- App state management
- Category/app loading
- Modal orchestration
- Deep link handling
### 3. src/global/downloadStatus.ts
**Purpose:** Download queue state
**Key Features:**
- Reactive download list
- Download item CRUD operations
- Change watchers for UI updates
### 4. electron/preload/index.ts
**Purpose:** Renderer-Main bridge
**Key Features:**
- IPC API exposure
- Architecture detection
- Loading animation
### 5. vite.config.ts
**Purpose:** Build configuration
**Key Features:**
- Electron plugin setup
- Dev server proxy
- Tailwind integration
---
## 🐛 Common Pitfalls & Solutions
### 1. Duplicate Task Handling
**Problem:** User clicks install multiple times
**Solution:**
```typescript
if (tasks.has(id) && !download.retry) {
logger.warn('Task already exists, ignoring duplicate');
return;
}
```
### 2. Window Close Behavior
**Problem:** Closing window while tasks are running
**Solution:**
```typescript
win.on('close', (event) => {
event.preventDefault();
if (tasks.size > 0) {
win.hide(); // Hide instead of closing
win.setSkipTaskbar(true);
} else {
win.destroy(); // Allow close if no tasks
}
});
```
### 3. App Data Normalization
**Problem:** API returns PascalCase, app uses camelCase
**Solution:**
```typescript
const normalizedApp: App = {
name: appJson.Name,
pkgname: appJson.Pkgname,
version: appJson.Version,
// ... map all fields
};
```
### 4. Screenshot Loading
**Problem:** Not all apps have 5 screenshots
**Solution:**
```typescript
for (let i = 1; i <= 5; i++) {
const img = new Image();
img.src = screenshotUrl;
img.onload = () => screenshots.value.push(screenshotUrl);
// No onerror handler - silently skip missing images
}
```
---
## 📚 Logging Best Practices
### Pino Logger Usage
```typescript
import pino from 'pino';
const logger = pino({ name: 'module-name' });
// Levels: trace, debug, info, warn, error, fatal
logger.info('Application started');
logger.error({ err }, 'Failed to load apps');
logger.warn(`Package ${pkgname} not found`);
```
### Log Locations
**Development:** Console with `pino-pretty`
**Production:** Structured JSON to stdout
---
## 🔄 State Management
### Global State (src/global/storeConfig.ts)
```typescript
export const currentApp = ref<App | null>(null);
export const currentAppIsInstalled = ref(false);
```
**Usage Pattern:**
```typescript
import { currentApp, currentAppIsInstalled } from '@/global/storeConfig';
// Set current app
currentApp.value = selectedApp;
// Check installation status
window.ipcRenderer.invoke('check-installed', app.pkgname)
.then((isInstalled: boolean) => {
currentAppIsInstalled.value = isInstalled;
});
```
### Download Queue (src/global/downloadStatus.ts)
```typescript
export const downloads = ref<DownloadItem[]>([]);
// Add download
downloads.value.push(newDownload);
// Remove download
export const removeDownloadItem = (pkgname: string) => {
const index = downloads.value.findIndex(d => d.pkgname === pkgname);
if (index !== -1) downloads.value.splice(index, 1);
};
// Watch changes
export const watchDownloadsChange = (callback: () => void) => {
watch(downloads, callback, { deep: true });
};
```
---
## 🎯 Contribution Guidelines
### When Adding New Features
1. **Add TypeScript types first** (src/global/typedefinition.ts)
2. **Update IPC handlers** if main-renderer communication needed
3. **Follow existing component patterns** (props, emits, setup)
4. **Test with actual APM commands** (don't mock in development)
5. **Update README TODO list** when completing tasks
### Code Style
- **Use TypeScript strict mode** - no `any` types without `eslint-disable`
- **Avoid used of eslint-disable directly** - use `undefined` instead if you really do not know its type.
- **Prefer Composition API** - `<script setup lang="ts">`
- **Use arrow functions** for methods in setup
- **Destructure imports** - `import { ref } from 'vue'`
- **Follow naming conventions:**
- Components: PascalCase (AppCard.vue)
- Functions: camelCase (handleInstall)
- Constants: UPPER_SNAKE_CASE (SHELL_CALLER_PATH)
### Commit Message Format
```
type(scope): subject
Examples:
feat(install): add retry mechanism for failed installations
fix(ui): correct dark mode toggle persistence
refactor(ipc): simplify install manager event handling
docs(readme): update build instructions
```
Add sign off to the commit is recommended.
---
## 🔗 Related Resources
- **APM Project:** https://gitee.com/spark-store-project/AmberPM
- **Electron Docs:** https://www.electronjs.org/docs
- **Vue 3 Docs:** https://vuejs.org/
- **Vite Docs:** https://vitejs.dev/
- **Tailwind CSS:** https://tailwindcss.com/
---
## ⚠️ Known Issues & TODOs
See README.md for more details.
## 🎓 Learning Path for New Contributors
### Phase 1: Understand the Stack
1. Read Vue 3 Composition API docs
2. Review Electron IPC communication patterns
3. Understand APM package manager basics
### Phase 2: Explore the Code
1. Start with `src/App.vue` - see how app state flows
2. Study `electron/main/backend/install-manager.ts` - understand task queue
3. Review `src/global/typedefinition.ts` - learn data structures
### Phase 3: Make Your First Change
1. Pick an item from TODO list (prefer UI improvements first)
2. Create a feature branch
3. Follow code style guidelines
4. Test with actual APM commands
5. Submit PR with clear description
---
## 🤖 AI Agent-Specific Instructions
### When Generating Code
1. **Always check existing patterns first** - search for similar implementations
2. **Maintain type safety** - no implicit any, use explicit types
3. **Follow IPC naming conventions:**
- Main → Renderer: `kebab-case` (e.g., `install-complete`)
- Handlers: descriptive names (e.g., `queue-install`, `list-installed`)
4. **Error handling:**
- Always catch promises
- Log errors with context
- Send user-friendly messages to renderer
5. **State updates:**
- Use Vue reactivity (ref, reactive)
- Avoid direct array mutations, use spread operator
- Update UI before async operations complete
### When Fixing Bugs
1. **Reproduce first** - understand the context
2. **Check IPC communication** - many bugs are timing issues
3. **Verify APM output parsing** - format may change
4. **Test edge cases:**
- Missing packages
- Network failures
- Privilege escalation failures
- Rapid button clicks
### When Refactoring
1. **Don't break IPC contracts** - keep event names and payloads compatible
2. **Preserve type safety** - update types before code
3. **Test installation flow end-to-end**
4. **Update comments and docs**
### MUST DO
1. Lint code
2. Format code
3. Build vite project to check for errors.
---
**Document Version:** 1.0
**Last Updated:** 2026-02-14
**Generated for:** AI Coding Agents working on elysia-best/apm-app-store
---
This document should be updated as the codebase evolves. When in doubt, refer to the actual source code and prioritize type safety and user experience.