Skill: akellacom/static-app (version 1.0.4)

--- name: Static Website Hosting - Static.app description: Deploy static websites to Static.app hosting. Use when the user wants to deploy, upload, or host a static site on Static.app. Triggers on phrases like "deploy to static.app", "upload to static", "host on static.app", "static.app deploy", or when working with the Static.app hosting service. --- # Static.app Deployment Skill Deploy static websites and applications to [Static.app](https://static.app) hosting directly from OpenClaw. ## Workspace Structure All Static.app operations in your workspace use a dedicated folder structure: ``` workspace/ └── staticapp/ # Main folder for all Static.app operations ├── new-site/ # New sites created locally └── {pid}/ # Downloaded existing sites (by PID) ``` - **New sites**: Created in `staticapp/` subfolders before deployment - **Downloaded sites**: Extracted to `staticapp/{pid}/` for editing ## How Static.app Handles Files Static.app automatically creates clean URLs from your filenames: | File | URL | |------|-----| | `index.html` | `/` (homepage) | | `about.html` | `/about` | | `portfolio.html` | `/portfolio` | | `contact.html` | `/contact` | **No subdirectories needed!** Just create `.html` files in the root folder. ## Project Structure ### Simple Multi-Page Site ``` my-site/ ├── index.html # Homepage → / ├── about.html # About page → /about ├── portfolio.html # Portfolio → /portfolio ├── contact.html # Contact → /contact ├── style.css # Stylesheet ├── js/ # JavaScript files │ ├── main.js │ └── utils.js └── images/ # Images folder ├── logo.png └── photo.jpg ``` ### JavaScript App (React, Vue, etc.) For JS apps, **build first**, then deploy the `dist` (or `build`) folder: ```bash # Build your app npm run build # Deploy the dist folder node scripts/deploy.js ./dist ``` ## Prerequisites 1. **Get API Key**: Go to https://static.app/account/api and create an API key (starts with `sk_`) 2. **Set Environment Variable**: Store the API key in `STATIC_APP_API_KEY` env var ## Usage ### Deploy Multi-Page Site ```bash # Create your pages echo '<h1>Home</h1>' > index.html echo '<h1>About</h1>' > about.html echo '<h1>Portfolio</h1>' > portfolio.html # Deploy node scripts/deploy.js ``` ### Deploy Specific Directory ```bash node scripts/deploy.js ./my-site ``` ### Update Existing Site ```bash node scripts/deploy.js . --pid olhdscieyr ``` ### List All Sites ```bash node scripts/list.js ``` ### List Site Files ```bash node scripts/files.js YOUR_PID ``` Options: - `--raw` — Output raw JSON - `-k <key>` — Specify API key ### Delete Site ```bash node scripts/delete.js YOUR_PID ``` Options: - `-f, --force` — Skip confirmation prompt - `-k <key>` — Specify API key ### Download Site Download an existing site to your workspace for editing: ```bash node scripts/download.js YOUR_PID ``` This will: 1. Fetch the download URL from Static.app API 2. Download the site archive 3. Extract it to `staticapp/{pid}/` Options: - `-p, --pid` — Site PID to download - `-o, --output` — Custom output directory (default: `./staticapp/{pid}`) - `-k <key>` — Specify API key - `--raw` — Output raw JSON response Example: ```bash # Download site to default location node scripts/download.js abc123 # Download to custom folder node scripts/download.js abc123 -o ./my-site ``` ## Script Options ``` node scripts/deploy.js [SOURCE_DIR] [OPTIONS] Arguments: SOURCE_DIR Directory to deploy (default: current directory) Options: -k, --api-key API key (or set STATIC_APP_API_KEY env var) -p, --pid Project PID to update existing site -e, --exclude Comma-separated exclude patterns --keep-zip Keep zip archive after deployment ``` ## Default Exclusions The following are automatically excluded from deployment: - `node_modules` - `.git`, `.github` - `*.md` - `package*.json` - `.env` - `.openclaw` ## Important Notes ### ✅ What Works - **Static HTML sites** — Any number of `.html` pages - **CSS & JavaScript** — Frontend frameworks, vanilla JS - **Images & Assets** — Place in `images/` folder or root - **JavaScript files** — Place in `js/` folder or root - **Built JS Apps** — Deploy `dist/` or `build/` folder after `npm run build` ### ❌ What Doesn't Work - **Node.js Server Apps** — No server-side rendering, no Express.js, no API routes - **PHP, Python, Ruby** — Static.app only serves static files - **Databases** — Use client-side storage or external APIs ### JavaScript Apps Workflow ```bash # 1. Build your React/Vue/Angular app npm run build # 2. Deploy the build output node scripts/deploy.js ./dist --pid YOUR_PID ``` ## API Reference ### Deploy Site - **Endpoint**: `POST https://api.static.app/v1/sites/zip` - **Auth**: Bearer token (API key) - **Body**: Multipart form with `archive` (zip file) and optional `pid` ### List Sites - **Endpoint**: `GET https://api.static.app/v1/sites` - **Auth**: Bearer token (API key) - **Headers**: `Accept: application/json` ### List Site Files - **Endpoint**: `GET https://api.static.app/v1/sites/files/{pid}` - **Auth**: Bearer token (API key) - **Headers**: `Accept: application/json` ### Delete Site - **Endpoint**: `DELETE https://api.static.app/v1/sites/{pid}` - **Auth**: Bearer token (API key) - **Headers**: `Accept: application/json` ### Download Site - **Endpoint**: `GET https://api.static.app/v1/sites/download/{pid}` - **Auth**: Bearer token (API key) - **Headers**: `Accept: application/json` - **Response**: Returns download URL for the site archive ## Dependencies - `archiver` — Zip archive creation - `form-data` — Multipart form encoding - `node-fetch` — HTTP requests - `adm-zip` — Zip extraction Install with: `cd scripts && npm install` ## Response On success, the script outputs: ``` ✅ Deployment successful! 🌐 Site URL: https://xyz.static.app 📋 PID: abc123 STATIC_APP_URL=https://xyz.static.app STATIC_APP_PID=abc123 ``` ## Workflow 1. Check for `STATIC_APP_API_KEY` env var or `--api-key` 2. Create zip archive from source directory (with exclusions) 3. Upload to Static.app API 4. Parse response and output URLs 5. Clean up temporary zip file ## Error Handling - Missing API key → Clear error with instructions - Network issues → HTTP error details - Invalid PID → API error message

path: scripts/delete.js
content: #!/usr/bin/env node
/**
 * Delete site from Static.app
 */

const fetch = require('node-fetch');

const API_BASE = 'https://api.static.app/v1/sites';

function parseArgs() {
  const args = process.argv.slice(2);
  const options = {
    [REDACTED_SECRET],
    pid: null,
    force: false
  };

  for (let i = 0; i < args.length; i++) {
    const arg = args[i];
    if (arg === '-k' || arg === '--api-key') {
      options.[REDACTED_SECRET];
    } else if (arg === '-p' || arg === '--pid') {
      options.pid = args[++i];
    } else if (arg === '-f' || arg === '--force') {
      options.force = true;
    } else if (arg === '-h' || arg === '--help') {
      showHelp();
      process.exit(0);
    } else if (!arg.startsWith('-') && !options.pid) {
      options.pid = arg;
    }
  }

  return options;
}

function showHelp() {
  console.log(`
Usage: node delete.js [PID] [OPTIONS]

Delete a site from Static.app

Arguments:
  PID                Site PID to delete

Options:
  -p, --pid          Site PID to delete
  -k, --api-key      API key (or set STATIC_APP_API_KEY env var)
  -f, --force        Skip confirmation prompt
  -h, --help         Show this help

Examples:
  node delete.js abc123
  node delete.js -p abc123
  node delete.js abc123 --force
`);
}

async function deleteSite(apiKey, pid) {
  const url = `${API_BASE}/${pid}`;
  
  console.log(`🗑️  Deleting site: ${pid}...\n`);
  
  const response = await fetch(url, {
    method: 'DELETE',
    headers: {
      'Authorization': `Bearer ${apiKey}`,
      'Accept': 'application/json'
    }
  });

  if (!response.ok) {
    const errorText = await response.text();
    throw new Error(`HTTP ${response.status}: ${errorText}`);
  }

  // DELETE may return 204 No Content or empty body
  if (response.status === 204 || response.status === 200) {
    return { success: true, message: 'Site deleted successfully' };
  }

  // Try to parse JSON, but handle empty responses
  const text = await response.text();
  if (!text || text.trim() === '') {
    return { success: true, message: 'Site deleted successfully' };
  }
  
  try {
    return JSON.parse(text);
  } catch (e) {
    return { success: true, message: 'Site deleted successfully' };
  }
}

async function confirmDeletion(pid) {
  const readline = require('readline');
  const rl = readline.createInterface({
    input: process.stdin,
    output: process.stdout
  });

  return new Promise((resolve) => {
    rl.question(`⚠️  Are you sure you want to delete site "${pid}"? (yes/no): `, (answer) => {
      rl.close();
      resolve(answer.toLowerCase() === 'yes');
    });
  });
}

async function main() {
  const options = parseArgs();
  
  if (!options.apiKey) {
    console.error('❌ Error: API key required. Provide --api-key or set STATIC_APP_API_KEY env var.');
    console.error('   Get your API key at: https://static.app/account/api');
    process.exit(1);
  }
  
  if (!options.pid) {
    console.error('❌ Error: PID required. Provide PID as argument or use --pid.');
    console.error('   Example: node delete.js abc123');
    process.exit(1);
  }
  
  // Confirm deletion unless --force is used
  if (!options.force) {
    const confirmed = await confirmDeletion(options.pid);
    if (!confirmed) {
      console.log('\n❌ Deletion cancelled.');
      process.exit(0);
    }
  }
  
  try {
    const result = await deleteSite(options.apiKey, options.pid);
    
    console.log(`✅ Site "${options.pid}" deleted successfully!`);
    
  } catch (error) {
    console.error(`❌ Failed to delete site: ${error.message}`);
    process.exit(1);
  }
}

main();


path: scripts/deploy.js
content: #!/usr/bin/env node
/**
 * Static.app deployment script for OpenClaw.
 * Uploads a directory as a zip archive to Static.app hosting.
 */

const fs = require('fs');
const path = require('path');
const archiver = require('archiver');
const FormData = require('form-data');
const fetch = require('node-fetch');

const API_URL = 'https://api.static.app/v1/sites/zip/';
const DEFAULT_EXCLUDE = ['node_modules', '.git', '.github', '*.md', 'package*.json', '.env', '.openclaw'];

function parseArgs() {
  const args = process.argv.slice(2);
  const options = {
    sourceDir: '.',
    [REDACTED_SECRET],
    pid: null,
    exclude: null,
    keepZip: false
  };

  for (let i = 0; i < args.length; i++) {
    const arg = args[i];
    if (arg === '-k' || arg === '--api-key') {
      options.[REDACTED_SECRET];
    } else if (arg === '-p' || arg === '--pid') {
      options.pid = args[++i];
    } else if (arg === '-e' || arg === '--exclude') {
      options.exclude = args[++i];
    } else if (arg === '--keep-zip') {
      options.keepZip = true;
    } else if (!arg.startsWith('-')) {
      options.sourceDir = arg;
    }
  }

  return options;
}

function shouldExclude(filePath, excludePatterns) {
  const parts = filePath.split(path.sep);
  const fileName = parts[parts.length - 1];
  
  for (const pattern of excludePatterns) {
    // Handle directory patterns
    if (parts.includes(pattern)) return true;
    
    // Handle glob patterns like *.md
    if (pattern.startsWith('*')) {
      const ext = pattern.slice(1);
      if (fileName.endsWith(ext)) return true;
    }
    
    // Handle wildcard in middle like package*.json
    if (pattern.includes('*')) {
      const regex = new RegExp('^' + pattern.replace(/\*/g, '.*') + '$');
      if (regex.test(fileName)) return true;
    }
    
    // Exact match
    if (fileName === pattern) return true;
  }
  
  return false;
}

async function createZipArchive(sourceDir, excludePatterns) {
  const sourcePath = path.resolve(sourceDir);
  const zipPath = path.join(process.cwd(), 'static-app-deploy.zip');
  
  console.log(`📦 Creating zip archive from ${sourcePath}...`);
  
  return new Promise((resolve, reject) => {
    const output = fs.createWriteStream(zipPath);
    const archive = archiver('zip', { zlib: { level: 9 } });

    output.on('close', () => {
      const size = archive.pointer();
      console.log(`📦 Archive created: ${size.toLocaleString()} bytes`);
      resolve(zipPath);
    });

    archive.on('error', (err) => {
      reject(err);
    });

    archive.on('warning', (err) => {
      if (err.code === 'ENOENT') {
        console.warn(`⚠️  Warning: ${err.message}`);
      } else {
        reject(err);
      }
    });

    archive.pipe(output);

    // Read directory and add files
    function addDirectory(dir, baseDir) {
      const items = fs.readdirSync(dir);
      
      for (const item of items) {
        const fullPath = path.join(dir, item);
        const relPath = path.relative(baseDir, fullPath);
        
        if (shouldExclude(relPath, excludePatterns)) {
          continue;
        }
        
        const stat = fs.statSync(fullPath);
        
        if (stat.isDirectory()) {
          addDirectory(fullPath, baseDir);
        } else {
          archive.file(fullPath, { name: relPath });
        }
      }
    }

    addDirectory(sourcePath, sourcePath);
    archive.finalize();
  });
}

async function deploy(apiKey, zipPath, pid) {
  console.log(`🚀 Uploading to Static.app...`);
  
  const form = new FormData();
  // Read file into buffer to handle redirects properly
  const fileBuffer = fs.readFileSync(zipPath);
  form.append('archive', fileBuffer, { filename: 'deploy.zip' });
  
  if (pid) {
    form.append('pid', pid);
    console.log(`   Updating existing site: ${pid}`);
  }

  const response = await fetch(API_URL, {
    method: 'POST',
    body: form,
    headers: {
      'Authorization': `Bearer ${apiKey}`,
      ...form.getHeaders()
    },
    redirect: 'follow'
  });

  if (!response.ok) {
    const errorText = await response.text();
    throw new Error(`HTTP ${response.status}: ${errorText}`);
  }

  const result = await response.json();
  
  // Extract URLs from various possible response formats
  let siteUrl = result.url || result.site_url;
  if (!siteUrl && result.subdomain) {
    siteUrl = `https://${result.subdomain}.static.app`;
  }
  
  // Use pid as primary identifier (not id)
  const sitePid = result.pid || result.site_pid || pid;
  
  return {
    sitePid,
    siteUrl,
    deployUrl: result.deploy_url || siteUrl
  };
}

async function main() {
  const options = parseArgs();
  
  if (!options.apiKey) {
    console.error('❌ Error: API key required. Provide --api-key or set STATIC_APP_API_KEY env var.');
    console.error('   Get your API key at: https://static.app/account/api');
    process.exit(1);
  }
  
  // Parse exclude patterns
  const exclude = [...DEFAULT_EXCLUDE];
  if (options.exclude) {
    exclude.push(...options.exclude.split(',').map(p => p.trim()));
  }
  
  try {
    // Create zip
    const zipPath = await createZipArchive(options.sourceDir, exclude);
    
    // Deploy
    const result = await deploy(options.apiKey, zipPath, options.pid);
    
    // Clean up
    if (!options.keepZip) {
      fs.unlinkSync(zipPath);
      console.log(`🧹 Cleaned up temporary archive`);
    } else {
      console.log(`💾 Kept archive: ${zipPath}`);
    }
    
    // Output results
    console.log(`\n✅ Deployment successful!`);
    console.log(`🌐 Site URL: ${result.siteUrl}`);
    if (result.sitePid) {
      console.log(`📋 PID: ${result.sitePid}`);
    }
    
    // Output for OpenClaw to capture
    console.log(`\nSTATIC_APP_URL=${result.siteUrl}`);
    console.log(`STATIC_APP_PID=${result.sitePid}`);
    
  } catch (error) {
    console.error(`❌ Deployment failed: ${error.message}`);
    process.exit(1);
  }
}

main();


path: scripts/download.js
content: #!/usr/bin/env node
/**
 * Download and extract a site from Static.app
 * Fetches download URL, downloads the zip, and extracts to workspace.
 */

const fs = require('fs');
const path = require('path');
const fetch = require('node-fetch');
const AdmZip = require('adm-zip');

const API_BASE = 'https://api.static.app/v1/sites/download';
// Resolve workspace root: script is in skills/static-app/scripts/, go up 3 levels
const WORKSPACE_DIR = path.join(__dirname, '..', '..', '..', 'staticapp');

function parseArgs() {
  const args = process.argv.slice(2);
  const options = {
    [REDACTED_SECRET],
    pid: null,
    outputDir: null,
    raw: false
  };

  for (let i = 0; i < args.length; i++) {
    const arg = args[i];
    if (arg === '-k' || arg === '--api-key') {
      options.[REDACTED_SECRET];
    } else if (arg === '-p' || arg === '--pid') {
      options.pid = args[++i];
    } else if (arg === '-o' || arg === '--output') {
      options.outputDir = args[++i];
    } else if (arg === '--raw') {
      options.raw = true;
    } else if (arg === '-h' || arg === '--help') {
      showHelp();
      process.exit(0);
    } else if (!arg.startsWith('-') && !options.pid) {
      options.pid = arg;
    }
  }

  return options;
}

function showHelp() {
  console.log(`
Usage: node download.js [PID] [OPTIONS]

Download and extract a site from Static.app to your workspace.

Arguments:
  PID                Site PID to download

Options:
  -p, --pid          Site PID
  -k, --api-key      API key (or set STATIC_APP_API_KEY env var)
  -o, --output       Output directory (default: ./staticapp/{pid})
  --raw              Output raw JSON response for download URL
  -h, --help         Show this help

Examples:
  node download.js abc123
  node download.js -p abc123
  node download.js abc123 -o ./my-site
`);
}

async function getDownloadUrl(apiKey, pid) {
  const url = `${API_BASE}/${pid}`;
  
  console.log(`📥 Fetching download URL for site: ${pid}\n`);
  
  const response = await fetch(url, {
    method: 'GET',
    headers: {
      'Authorization': `Bearer ${apiKey}`,
      'Accept': 'application/json'
    }
  });

  if (!response.ok) {
    const errorText = await response.text();
    throw new Error(`HTTP ${response.status}: ${errorText}`);
  }

  return await response.json();
}

async function downloadFile(url, destPath) {
  console.log(`⬇️  Downloading from ${url}...`);
  
  const response = await fetch(url, {
    redirect: 'follow'
  });
  
  if (!response.ok) {
    throw new Error(`HTTP ${response.status}: Failed to download file`);
  }
  
  const fileStream = fs.createWriteStream(destPath);
  
  return new Promise((resolve, reject) => {
    response.body.pipe(fileStream);
    response.body.on('error', reject);
    fileStream.on('finish', () => {
      const stats = fs.statSync(destPath);
      console.log(`💾 Downloaded: ${formatBytes(stats.size)}`);
      resolve();
    });
  });
}

function extractZip(zipPath, extractPath) {
  console.log(`📦 Extracting to ${extractPath}...`);
  
  // Ensure extract directory exists
  if (!fs.existsSync(extractPath)) {
    fs.mkdirSync(extractPath, { recursive: true });
  }
  
  // Extract using adm-zip (pure Node.js, no shell commands)
  try {
    const zip = new AdmZip(zipPath);
    zip.extractAllTo(extractPath, true);
    console.log(`✅ Extracted successfully`);
  } catch (err) {
    console.error(`❌ Extraction failed: ${err.message}`);
    throw new Error('Extraction failed');
  }
}

function formatBytes(bytes) {
  if (bytes === 0) return '0 B';
  const k = 1024;
  const sizes = ['B', 'KB', 'MB', 'GB'];
  const i = Math.floor(Math.log(bytes) / Math.log(k));
  return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
}

function listExtractedFiles(dir, prefix = '') {
  let output = '';
  const items = fs.readdirSync(dir);
  
  items.forEach(item => {
    const fullPath = path.join(dir, item);
    const isDir = fs.statSync(fullPath).isDirectory();
    const icon = isDir ? '📁' : '📄';
    output += `${prefix}${icon} ${item}${isDir ? '/' : ''}\n`;
    
    if (isDir) {
      output += listExtractedFiles(fullPath, prefix + '  ');
    }
  });
  
  return output;
}

async function main() {
  const options = parseArgs();
  
  if (!options.apiKey) {
    console.error('❌ Error: API key required. Provide --api-key or set STATIC_APP_API_KEY env var.');
    console.error('   Get your API key at: https://static.app/account/api');
    process.exit(1);
  }
  
  if (!options.pid) {
    console.error('❌ Error: PID required. Provide PID as argument or use --pid.');
    console.error('   Example: node download.js abc123');
    process.exit(1);
  }
  
  try {
    // Step 1: Get download URL
    const result = await getDownloadUrl(options.apiKey, options.pid);
    
    if (options.raw) {
      console.log(JSON.stringify(result, null, 2));
      return;
    }
    
    // Extract download URL from response
    const downloadUrl = result.url || result.download_url || result.link;
    
    if (!downloadUrl) {
      console.error('❌ Error: No download URL in response');
      console.log('Response:', JSON.stringify(result, null, 2));
      process.exit(1);
    }
    
    console.log(`🔗 Download URL obtained`);
    
    // Step 2: Determine output directory
    const outputDir = options.outputDir || path.join(WORKSPACE_DIR, options.pid);
    const zipPath = path.join(process.cwd(), `${options.pid}-download.zip`);
    
    // Step 3: Download the file
    await downloadFile(downloadUrl, zipPath);
    
    // Step 4: Extract
    extractZip(zipPath, outputDir);
    
    // Step 5: Clean up zip file
    fs.unlinkSync(zipPath);
    console.log(`🧹 Cleaned up temporary archive`);
    
    // Step 6: Show results
    console.log(`\n✅ Site downloaded and extracted!`);
    console.log(`📁 Location: ${outputDir}`);
    console.log(`\n📂 Files:`);
    console.log(listExtractedFiles(outputDir));
    
    // Output for OpenClaw to capture
    console.log(`\nSTATIC_APP_DOWNLOAD_PATH=${outputDir}`);
    console.log(`STATIC_APP_PID=${options.pid}`);
    
  } catch (error) {
    console.error(`❌ Download failed: ${error.message}`);
    process.exit(1);
  }
}

main();

path: scripts/files.js
content: #!/usr/bin/env node
/**
 * List files of a site from Static.app
 */

const fetch = require('node-fetch');

const API_BASE = 'https://api.static.app/v1/sites/files';

function parseArgs() {
  const args = process.argv.slice(2);
  const options = {
    [REDACTED_SECRET],
    pid: null,
    raw: false
  };

  for (let i = 0; i < args.length; i++) {
    const arg = args[i];
    if (arg === '-k' || arg === '--api-key') {
      options.[REDACTED_SECRET];
    } else if (arg === '-p' || arg === '--pid') {
      options.pid = args[++i];
    } else if (arg === '--raw') {
      options.raw = true;
    } else if (arg === '-h' || arg === '--help') {
      showHelp();
      process.exit(0);
    } else if (!arg.startsWith('-') && !options.pid) {
      options.pid = arg;
    }
  }

  return options;
}

function showHelp() {
  console.log(`
Usage: node files.js [PID] [OPTIONS]

List all files of a site from Static.app

Arguments:
  PID                Site PID to list files

Options:
  -p, --pid          Site PID
  -k, --api-key      API key (or set STATIC_APP_API_KEY env var)
  --raw              Output raw JSON response
  -h, --help         Show this help

Examples:
  node files.js abc123
  node files.js -p abc123
  node files.js abc123 --raw
`);
}

async function listFiles(apiKey, pid) {
  const url = `${API_BASE}/${pid}`;
  
  console.log(`📁 Fetching files for site: ${pid}\n`);
  
  const response = await fetch(url, {
    method: 'GET',
    headers: {
      'Authorization': `Bearer ${apiKey}`,
      'Accept': 'application/json'
    }
  });

  if (!response.ok) {
    const errorText = await response.text();
    throw new Error(`HTTP ${response.status}: ${errorText}`);
  }

  return await response.json();
}

function formatFiles(files, basePath = '') {
  if (!Array.isArray(files) || files.length === 0) {
    return 'No files found.';
  }

  let output = '';
  let totalSize = 0;
  
  // Sort files: directories first, then by name
  const sorted = [...files].sort((a, b) => {
    const aIsDir = a.type === 'directory' || a.isDirectory;
    const bIsDir = b.type === 'directory' || b.isDirectory;
    if (aIsDir && !bIsDir) return -1;
    if (!aIsDir && bIsDir) return 1;
    return (a.name || a.path || '').localeCompare(b.name || b.path || '');
  });
  
  sorted.forEach(item => {
    const name = item.name || item.path || 'unnamed';
    const isDir = item.type === 'directory' || item.isDirectory;
    const size = item.size || 0;
    const sizeFormatted = formatBytes(size);
    
    if (isDir) {
      output += `📁 ${name}/\n`;
    } else {
      output += `📄 ${name} (${sizeFormatted})\n`;
      totalSize += size;
    }
  });
  
  output += `\n📊 Total: ${files.length} items, ${formatBytes(totalSize)}\n`;
  
  return output;
}

function formatBytes(bytes) {
  if (bytes === 0) return '0 B';
  const k = 1024;
  const sizes = ['B', 'KB', 'MB', 'GB'];
  const i = Math.floor(Math.log(bytes) / Math.log(k));
  return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
}

async function main() {
  const options = parseArgs();
  
  if (!options.apiKey) {
    console.error('❌ Error: API key required. Provide --api-key or set STATIC_APP_API_KEY env var.');
    console.error('   Get your API key at: https://static.app/account/api');
    process.exit(1);
  }
  
  if (!options.pid) {
    console.error('❌ Error: PID required. Provide PID as argument or use --pid.');
    console.error('   Example: node files.js abc123');
    process.exit(1);
  }
  
  try {
    const result = await listFiles(options.apiKey, options.pid);
    
    if (options.raw) {
      console.log(JSON.stringify(result, null, 2));
    } else {
      // Handle different response formats
      const files = Array.isArray(result) ? result : (result.files || result.data || []);
      console.log(formatFiles(files));
    }
    
  } catch (error) {
    console.error(`❌ Failed to list files: ${error.message}`);
    process.exit(1);
  }
}

main();


path: scripts/list.js
content: #!/usr/bin/env node
/**
 * List sites from Static.app
 */

const fetch = require('node-fetch');

const API_URL = 'https://api.static.app/v1/sites';

function parseArgs() {
  const args = process.argv.slice(2);
  const options = {
    [REDACTED_SECRET],
    raw: false
  };

  for (let i = 0; i < args.length; i++) {
    const arg = args[i];
    if (arg === '-k' || arg === '--api-key') {
      options.[REDACTED_SECRET];
    } else if (arg === '--raw') {
      options.raw = true;
    } else if (arg === '-h' || arg === '--help') {
      showHelp();
      process.exit(0);
    }
  }

  return options;
}

function showHelp() {
  console.log(`
Usage: node list.js [OPTIONS]

List all sites from Static.app account

Options:
  -k, --api-key    API key (or set STATIC_APP_API_KEY env var)
  --raw            Output raw JSON response
  -h, --help       Show this help

Examples:
  node list.js
  node list.js --raw
  node list.js -k sk_xxxxxxxx
`);
}

async function listSites(apiKey) {
  console.log('📋 Fetching sites from Static.app...\n');
  
  const response = await fetch(API_URL, {
    method: 'GET',
    headers: {
      'Authorization': `Bearer ${apiKey}`,
      'Accept': 'application/json'
    }
  });

  if (!response.ok) {
    const errorText = await response.text();
    throw new Error(`HTTP ${response.status}: ${errorText}`);
  }

  return await response.json();
}

function formatSites(sites) {
  if (!Array.isArray(sites) || sites.length === 0) {
    return 'No sites found.';
  }

  let output = `Found ${sites.length} site(s):\n\n`;
  
  sites.forEach((site, index) => {
    const name = site.name || site.title || 'Unnamed';
    const url = site.url || site.site_url || (site.subdomain ? `https://${site.subdomain}.static.app` : 'N/A');
    // Use pid as primary identifier (not id)
    const pid = site.pid || site.site_pid || 'N/A';
    const status = site.status || site.state || 'unknown';
    
    output += `${index + 1}. ${name}\n`;
    output += `   URL: ${url}\n`;
    output += `   PID: ${pid}\n`;
    output += `   Status: ${status}\n`;
    if (site.updated_at || site.last_deploy) {
      output += `   Updated: ${site.updated_at || site.last_deploy}\n`;
    }
    output += '\n';
  });
  
  return output;
}

async function main() {
  const options = parseArgs();
  
  if (!options.apiKey) {
    console.error('❌ Error: API key required. Provide --api-key or set STATIC_APP_API_KEY env var.');
    console.error('   Get your API key at: https://static.app/account/api');
    process.exit(1);
  }
  
  try {
    const sites = await listSites(options.apiKey);
    
    if (options.raw) {
      console.log(JSON.stringify(sites, null, 2));
    } else {
      console.log(formatSites(sites));
    }
    
  } catch (error) {
    console.error(`❌ Failed to list sites: ${error.message}`);
    process.exit(1);
  }
}

main();


path: scripts/package.json
content: {
  "name": "static-app-deploy",
  "version": "1.0.0",
  "description": "Deploy static sites to Static.app",
  "main": "deploy.js",
  "scripts": {
    "deploy": "node deploy.js"
  },
  "dependencies": {
    "adm-zip": "^0.5.16",
    "archiver": "^7.0.1",
    "form-data": "^4.0.0",
    "node-fetch": "^2.7.0"
  }
}


Security scan verdicts — ClawScan: clean | static analysis: suspicious | VirusTotal: clean | SkillSpector: suspicious (CRITICAL)