Files
spark-store/AGENTS.md

21 KiB

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
│   ├── modeuls/                 # 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/apm-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)

// 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-apm, arm64-apm)

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:

// 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

Core Types (src/global/typedefinition.ts)

App Data Structure

// 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

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

interface InstallLog {
  id: number;
  time: number;
  message: string;
}

interface DownloadResult extends InstallLog {
  success: boolean;
  exitCode: number | null;
}

🎨 Vue Component Patterns

Composition API Best Practices

<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

// 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:

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

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:

// 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

// 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

Axios Usage

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):

server: {
  proxy: {
    '/local_amd64-apm': {
      target: 'https://erotica.spark-app.store',
      changeOrigin: true,
      rewrite: (path) => path.replace(/^\/local_amd64-apm/, ''),
    }
  }
}

URL Scheme: apmstore://

Supported Actions

  1. Install App: apmstore://install?pkg=<pkgname>
  2. Show Updates: apmstore://update
  3. Show Installed: apmstore://installed

Implementation Pattern

// electron/main/deeplink.ts - Parse command line
export function handleCommandLine(argv: string[]) {
  const deeplinkUrl = argv.find((arg) => arg.startsWith('apmstore://'));
  if (!deeplinkUrl) return;
  
  const url = new URL(deeplinkUrl);
  if (url.hostname === 'install') {
    const pkg = url.searchParams.get('pkg');
    sendToRenderer('deep-link-install', pkg);
  }
}

// src/App.vue - Handle in renderer
window.ipcRenderer.on('deep-link-install', (_event, pkgname: string) => {
  const target = apps.value.find((a) => a.pkgname === pkgname);
  if (target) openDetail(target);
});

🛡️ Security Considerations

Privilege Escalation

Always check for pkexec availability:

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):

// 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:

<div class="bg-slate-50 dark:bg-slate-950 text-slate-900 dark:text-slate-100">
  <!-- Content -->
</div>

Theme Toggle:

const isDarkTheme = ref(false);

watch(isDarkTheme, (newVal) => {
  localStorage.setItem('theme', newVal ? 'dark' : 'light');
  document.documentElement.classList.toggle('dark', newVal);
});

Modal Pattern

<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

const loading = ref(true);

// In template
<div v-if="loading">Loading...</div>
<div v-else>{{ apps.length }} apps</div>

🧪 Testing & Quality

ESLint Configuration

// eslint.config.ts
export default defineConfig([
  globalIgnores(['**/3rdparty/**', '**/node_modules/**', '**/dist/**', '**/dist-electron/**']),
  tseslint.configs.recommended,
  pluginVue.configs['flat/essential'],
  eslintConfigPrettier,
  eslintPluginPrettierRecommended,
]);

TypeScript Configuration

{
  "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

npm run lint         # Run ESLint
npm run lint:fix     # Auto-fix issues
npm run format       # Format with Prettier

🚀 Build & Development

Development Mode

npm run dev          # Start dev server (Vite + Electron)

Dev Server: http://127.0.0.1:3344/ (port from package.json)

Production Build

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:

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:

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:

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:

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

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)

export const currentApp = ref<App | null>(null);
export const currentAppIsInstalled = ref(false);

Usage Pattern:

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)

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
  • 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


⚠️ 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

Document Version: 1.0
Last Updated: 2026-02-12
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.