<!--
hoody-app Subskill (sdk)
Auto-generated by Hoody Skills Generator
Generated: 2026-05-06T20:05:38.188Z
Model: mimo-v2.5-pro
Mode: sdk


Tokens: 9291

DO NOT EDIT MANUALLY - Changes will be overwritten on next generation
-->

# hoody-app Subskill

## Overview

**hoody-app** is the application execution and package management service in the Hoody Kit ecosystem. It provides a unified interface for searching, resolving, and running applications from multiple package sources, with support for user profiles, saved recipes, and batch operations.

### When to Use

- **Application Discovery**: Search for applications across configured package sources (npm, pip, brew, custom registries)
- **Application Execution**: Resolve an application selector to a runnable command and optionally execute it
- **Source Management**: Configure and manage package sources that applications are discovered from
- **Profile Management**: Create user profiles with default preferences and source overrides
- **Recipe Management**: Save reusable selector templates for common application launches
- **Batch Operations**: Process multiple search or run requests in a single call
- **Async Jobs**: Queue long-running searches and poll for completion

### How It Fits Into Hoody Philosophy

hoody-app embodies Hoody's "run anything, anywhere" philosophy by abstracting away the complexity of multi-platform application management. It decouples *what* you want to run from *where* it comes from, enabling portable, reproducible application execution across environments. Profiles allow per-user customization while recipes enable team-wide standardization of common workflows.

---

## Common Workflows

### 1. Health Check

Verify the service is running and responsive.

```
const health = await client.app.health.check();
console.log(health.status); // "ok"
```

### 2. Search for Applications

Find available applications matching a query across all enabled sources.

```
// Basic search
const results = await client.app.execution.searchCandidates({ app: 'node' });
console.log(results.candidates); // Array of matching applications

// Search with filters
const filtered = await client.app.execution.searchCandidates('python', {
  os: 'linux',
  kind: 'runtime',
  channel: 'stable',
  limit: 10
});
```

### 3. Paginated Search for Large Result Sets

When result sets are large, use cursor-based pagination.

```
// First page
const page1 = await client.app.execution.searchCandidatesPaged({
  selector: { app: 'docker' },
  limit: 20
});

// Next page using cursor
if (page1.next_cursor) {
  const page2 = await client.app.execution.searchCandidatesPaged({
    selector: { app: 'docker' },
    limit: 20,
    cursor: page1.next_cursor
  });
}

// Or collect all pages automatically
const allResults = await client.app.execution.searchCandidatesPagedAll({
  selector: { app: 'docker' },
  limit: 20
});
```

### 4. Preflight a Run Request

Resolve and validate an execution plan without actually running it.

```
const plan = await client.app.execution.preflight({
  app: 'node',
  version: '20',
  os: 'linux'
});
console.log(plan.command); // The resolved command
console.log(plan.candidate); // The selected candidate details
```

### 5. Run an Application

Resolve an application and get the exact shell command to execute.

```
// GET-based with query parameters
const result = await client.app.execution.runAppGet('node', {
  version: '20',
  os: 'linux',
  dry_run: true
});

// POST-based with full selector body
const result2 = await client.app.execution.runAppPost({
  app: 'python',
  version: '3.12',
  os: 'linux',
  channel: 'stable'
});
```

### 6. Path-Based Application Resolution

Use clean, bookmarkable URLs for application resolution.

```
// Positional path segments
const result = await client.app.execution.runPathBased({ rest: 'linux/node' });

// With terminal anchoring
const terminalResult = await client.app.execution.runTerminalAnchored(
  1,
  'linux/node'
);
```

### 7. Async Search with Job Polling

Queue a search in the background and poll for completion.

```
// Start async search
await client.app.jobs.createSearch({ app: 'rust' });

// Poll for completion (long-poll with wait=done)
const job = await client.app.jobs.getStatus('job_abc123', {
  wait: 'done',
  timeout_ms: 30000
});
console.log(job.status); // "completed"
console.log(job.result); // Search results
```

### 8. Batch Operations

Process multiple requests in a single call.

```
const batch = await client.app.execution.runBatch({
  items: [
    {
      request_id: 'req-1',
      mode: 'search',
      selector: { app: 'node' }
    },
    {
      request_id: 'req-2',
      mode: 'search',
      selector: { app: 'python' }
    }
  ]
});

// Each item has its own success/error payload
batch.items.forEach(item => {
  if (item.status === 'ok') {
    console.log(item.result);
  } else {
    console.error(item.error);
  }
});
```

### 9. Manage Package Sources

Configure where applications are discovered from.

```
// List all sources
const sources = await client.app.sources.list();

// Add a new source
const newSource = await client.app.sources.create({
  source_id: 'custom-registry',
  enabled: true,
  priority: 10,
  provider: 'npm',
  source_type: 'registry',
  pin: { url: 'https://registry.example.com' }
});

// Update a source
await client.app.sources.update('custom-registry', {
  enabled: false,
  priority: 5
});

// Sync a specific source
await client.app.sources.sync({ source_id: 'custom-registry' });

// Sync all sources
await client.app.sources.syncAll();

// Get diagnostics
const diag = await client.app.sources.getDiagnostics({ source_id: 'custom-registry' });

// Delete a source
await client.app.sources.delete({ source_id: 'custom-registry' });
```

### 10. Manage User Profiles

Create and switch between user profiles with different defaults.

```
// List profiles
const profiles = await client.app.profiles.list();

// Create a profile
await client.app.profiles.create({
  name: 'frontend-dev',
  sources: [
    { source_id: 'npm-registry' }
  ]
});

// Update a profile
await client.app.profiles.update('frontend-dev', {
  sources_mode: 'override'
});

// Select active profile
await client.app.profiles.select({ profile: 'frontend-dev' });

// Delete a profile
await client.app.profiles.delete({ profile: 'frontend-dev' });
```

### 11. Manage Recipes

Save and reuse common application selectors.

```
// List recipes
const recipes = await client.app.recipes.list();

// Create a recipe
await client.app.recipes.create({
  name: 'node-lts',
  selector: {
    app: 'node',
    channel: 'lts',
    os: 'linux'
  }
});

// Get a recipe
const recipe = await client.app.recipes.get({ name: 'node-lts' });

// Update a recipe
await client.app.recipes.update('node-lts', {
  selector: {
    app: 'node',
    channel: 'lts',
    os: 'linux',
    version: '20'
  }
});

// Search using a recipe
const searchResults = await client.app.recipes.search('node-lts', {});

// Run using a recipe
const runResult = await client.app.recipes.run('node-lts', {});

// Delete a recipe
await client.app.recipes.delete({ name: 'node-lts' });
```

### 12. Get Runtime Configuration

Retrieve the full persisted configuration.

```
const config = await client.app.configuration.get();
console.log(config.sources);
console.log(config.profiles);
console.log(config.selected_profile);
```

---

## Advanced Operations

### Multi-Step: Discover, Preflight, and Run

A complete workflow from discovery to execution.

```
// Step 1: Search for candidates
const search = await client.app.execution.searchCandidates('terraform', {
  os: 'linux',
  kind: 'tool'
});

if (!search.candidates || search.candidates.length === 0) {
  throw new Error('No candidates found');
}

// Step 2: Preflight the top candidate
const plan = await client.app.execution.preflight({
  app: 'terraform',
  os: 'linux',
  kind: 'tool',
  pick_index: 0,
  set_id: search.set_id
});

// Step 3: Execute with the resolved plan
const run = await client.app.execution.runAppPost({
  app: 'terraform',
  os: 'linux',
  kind: 'tool',
  pick_index: 0,
  set_id: search.set_id
});
console.log(run.command);
```

### Multi-Step: Profile-Based Workflow

Set up a profile and use it for consistent application resolution.

```
// Step 1: Create a profile with source preferences
await client.app.profiles.create({
  name: 'data-science',
  sources: [
    { source_id: 'conda-forge' },
    { source_id: 'pypi' }
  ]
});

// Step 2: Activate the profile
await client.app.profiles.select({ profile: 'data-science' });

// Step 3: Search with profile defaults applied
const results = await client.app.execution.searchCandidates('jupyter', {
  profile: 'data-science'
});

// Step 4: Run with profile context
const run = await client.app.execution.runAppPost({
  app: 'jupyter',
  profile: 'data-science'
});
```

### Multi-Step: Recipe-Based Team Workflow

Create a shared recipe and use it for standardized launches.

```
// Step 1: Create a reusable recipe
await client.app.recipes.create({
  name: 'project-setup',
  selector: {
    app: 'node',
    version: '20',
    channel: 'lts',
    os: 'linux'
  }
});

// Step 2: Team member searches using the recipe
const candidates = await client.app.recipes.search('project-setup', {});

// Step 3: Team member runs with optional overrides
const result = await client.app.recipes.run('project-setup', {
  overrides: { version: '22' }
});
```

### Error Recovery: Source Sync and Retry

Handle cases where searches fail due to stale source data.

```
try {
  const results = await client.app.execution.searchCandidates({ app: 'my-app' });
  if (!results.candidates || results.candidates.length === 0) {
    throw new Error('No results');
  }
} catch (error) {
  // Step 1: Sync all sources to refresh data
  await client.app.sources.syncAll();

  // Step 2: Poll for sync completion
  const job = await client.app.jobs.getStatus('sync_job_id', {
    wait: 'done',
    timeout_ms: 60000
  });

  // Step 3: Retry search
  const results = await client.app.execution.searchCandidates({ app: 'my-app' });
}
```

### Error Recovery: Source Diagnostics

Investigate source health when searches return unexpected results.

```
const sources = await client.app.sources.list();

for (const source of sources) {
  const diag = await client.app.sources.getDiagnostics(source.source_id);
  if (diag.failures && diag.failures.length > 0) {
    console.warn(`Source ${source.source_id} has failures:`, diag.failures);
  }
}
```

### Performance: Batch Multiple Searches

Reduce round-trips by batching independent searches.

```
const batch = await client.app.execution.runBatch({
  items: [
    {
      request_id: 'search-node',
      mode: 'search',
      selector: { app: 'node', limit: 5 }
    },
    {
      request_id: 'search-python',
      mode: 'search',
      selector: { app: 'python', limit: 5 }
    },
    {
      request_id: 'search-go',
      mode: 'search',
      selector: { app: 'go', limit: 5 }
    }
  ]
});

const results = {};
batch.items.forEach(item => {
  results[item.request_id] = item.result;
});
```

### Performance: Async Search for Long Operations

Use background jobs for searches that may take time.

```
// Queue multiple async searches
await client.app.jobs.createSearch({ app: 'large-package-1' });
await client.app.jobs.createSearch({ app: 'large-package-2' });

// Poll with long-polling to avoid busy-waiting
const job1 = await client.app.jobs.getStatus('job_1', {
  wait: 'done',
  timeout_ms: 30000
});
const job2 = await client.app.jobs.getStatus('job_2', {
  wait: 'done',
  timeout_ms: 30000
});
```

---

## Quick Reference

### Most Common Endpoints

| Operation | SDK Method | HTTP |
|-----------|-----------|------|
| Health check | `client.app.health.check()` | GET `/api/v1/run/health` |
| Search apps | `client.app.execution.searchCandidates(app)` | GET `/api/v1/run/search` |
| Run app | `client.app.execution.runAppPost(selector)` | POST `/api/v1/run/run` |
| Preflight | `client.app.execution.preflight(selector)` | POST `/api/v1/run/preflight` |
| List sources | `client.app.sources.list()` | GET `/api/v1/run/sources` |
| List profiles | `client.app.profiles.list()` | GET `/api/v1/run/profiles` |
| List recipes | `client.app.recipes.list()` | GET `/api/v1/run/recipes` |
| Get config | `client.app.configuration.get()` | GET `/api/v1/run/config` |

### Essential Parameters

**Selector Fields** (used in search, run, preflight, recipes):
- `app` (required): Application name or query
- `os`: Target operating system
- `version`: Version constraint
- `channel`: Release channel (stable, beta, nightly)
- `kind`: Application kind (runtime, tool, library)
- `profile`: Profile name for defaults
- `pick_index`: Select specific candidate by index
- `set_id`: Reuse a previous search result set

### Typical Response Formats

**Search Response** (`app_SearchResponse`):
```
{
  "set_id": "set_abc123",
  "candidates": [
    {
      "id": "candidate_1",
      "name": "node",
      "version": "20.11.0",
      "source": "npm-registry"
    }
  ]
}
```

**Run Response** (`app_RunResponse`):
```
{
  "command": "npx node@20.11.0",
  "candidate": {
    "id": "candidate_1",
    "name": "node",
    "version": "20.11.0"
  }
}
```

**Job Response** (`app_Job`):
```
{
  "job_id": "job_abc123",
  "status": "completed",
  "result": {}
}
```

### Base URL Pattern

```
https://{projectId}-{containerId}-app-{serviceId}.{node}.containers.hoody.icu
```

All paths are prefixed with `/api/v1/run/`. See the core SKILL.md for container creation and service discovery.