diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a7fa465..21caca4 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -11,6 +11,25 @@ concurrency: cancel-in-progress: true jobs: + lint: + name: Lint + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup Node + uses: actions/setup-node@v4 + with: + node-version: 22.x + cache: npm + + - name: Install dependencies + run: npm ci + + - name: Lint + run: npm run lint + test: name: Test (Node ${{ matrix.node-version }}) runs-on: ubuntu-latest diff --git a/biome.json b/biome.json new file mode 100644 index 0000000..d375876 --- /dev/null +++ b/biome.json @@ -0,0 +1,54 @@ +{ + "$schema": "https://biomejs.dev/schemas/2.4.16/schema.json", + "vcs": { + "enabled": true, + "clientKind": "git", + "useIgnoreFile": true + }, + "files": { + "ignoreUnknown": true, + "includes": ["**", "!dist", "!node_modules"] + }, + "formatter": { + "enabled": true, + "indentStyle": "space", + "indentWidth": 2, + "lineWidth": 90, + "bracketSpacing": false + }, + "assist": { + "enabled": true, + "actions": { + "source": { + "organizeImports": "on" + } + } + }, + "linter": { + "enabled": true, + "rules": { + "recommended": true, + "style": { + "noNonNullAssertion": "off" + }, + "complexity": { + "useLiteralKeys": "off" + }, + "correctness": { + "useExhaustiveDependencies": "off" + }, + "suspicious": { + "noArrayIndexKey": "off" + } + } + }, + "javascript": { + "formatter": { + "quoteStyle": "single", + "jsxQuoteStyle": "double", + "trailingCommas": "all", + "semicolons": "always", + "arrowParentheses": "always" + } + } +} diff --git a/package-lock.json b/package-lock.json index 7238c29..1b44a60 100644 --- a/package-lock.json +++ b/package-lock.json @@ -21,6 +21,7 @@ "kiqr": "dist/cli.js" }, "devDependencies": { + "@biomejs/biome": "^2.4.16", "@sindresorhus/tsconfig": "^8.1.0", "@types/node": "^25.9.1", "@types/react": "^19.2.15", @@ -68,6 +69,181 @@ "node": ">=6.9.0" } }, + "node_modules/@biomejs/biome": { + "version": "2.4.16", + "resolved": "https://registry.npmjs.org/@biomejs/biome/-/biome-2.4.16.tgz", + "integrity": "sha512-x9ajFh1zChVybCiM3TN6OD4phAqLgtPZjFrZF+aTMYCPjwBO+k529TX7PPsAqtGNLeV4UgzwQnowEgS7bGmzcA==", + "dev": true, + "license": "MIT OR Apache-2.0", + "bin": { + "biome": "bin/biome" + }, + "engines": { + "node": ">=14.21.3" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/biome" + }, + "optionalDependencies": { + "@biomejs/cli-darwin-arm64": "2.4.16", + "@biomejs/cli-darwin-x64": "2.4.16", + "@biomejs/cli-linux-arm64": "2.4.16", + "@biomejs/cli-linux-arm64-musl": "2.4.16", + "@biomejs/cli-linux-x64": "2.4.16", + "@biomejs/cli-linux-x64-musl": "2.4.16", + "@biomejs/cli-win32-arm64": "2.4.16", + "@biomejs/cli-win32-x64": "2.4.16" + } + }, + "node_modules/@biomejs/cli-darwin-arm64": { + "version": "2.4.16", + "resolved": "https://registry.npmjs.org/@biomejs/cli-darwin-arm64/-/cli-darwin-arm64-2.4.16.tgz", + "integrity": "sha512-wxPvu4XOA85YJk9ixSWUmq/QBHbid85BISbOAqqBM/5xQpPk9ayjk5375tOlSC0BeCwNSbPFafQBm+vBumXq0A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT OR Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=14.21.3" + } + }, + "node_modules/@biomejs/cli-darwin-x64": { + "version": "2.4.16", + "resolved": "https://registry.npmjs.org/@biomejs/cli-darwin-x64/-/cli-darwin-x64-2.4.16.tgz", + "integrity": "sha512-xFCqGPwYusQJp4N4NJLi1XJiZqjwFdjhT+KqtNy+Ug3qgfczqnTa6MSDvxJF6TkuDLoYJItMapz6tAf7kCekFw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT OR Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=14.21.3" + } + }, + "node_modules/@biomejs/cli-linux-arm64": { + "version": "2.4.16", + "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-arm64/-/cli-linux-arm64-2.4.16.tgz", + "integrity": "sha512-2kFb4//jxfZaP6D+Rj5VkHkxgyD9EoRAVBEQb8PKRv+s4NO2zYNJKXFaJmK1CmhufJOWEfpHKaRbOja7qjmdhQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT OR Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=14.21.3" + } + }, + "node_modules/@biomejs/cli-linux-arm64-musl": { + "version": "2.4.16", + "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-arm64-musl/-/cli-linux-arm64-musl-2.4.16.tgz", + "integrity": "sha512-oYxnW0ARfJkr72ezzF2OR8N/rtkgLUQeYtF8cFhVswbknHxtTcmzSsanVJP8yQKnGpGpc2ck6c5zLvHahL6Cbg==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT OR Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=14.21.3" + } + }, + "node_modules/@biomejs/cli-linux-x64": { + "version": "2.4.16", + "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-x64/-/cli-linux-x64-2.4.16.tgz", + "integrity": "sha512-NbcBbi/nJqn5baae6wqRXdS7Gadf2uRpehSh6vMSYpG8OhkXl/Xg8aorWrJ+9VWqAT5ml90alLvorkpMW0nBwQ==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT OR Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=14.21.3" + } + }, + "node_modules/@biomejs/cli-linux-x64-musl": { + "version": "2.4.16", + "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-x64-musl/-/cli-linux-x64-musl-2.4.16.tgz", + "integrity": "sha512-iHDS+MCM65DPqWGu+ECC3uoALyj2H7F4nVUPxIPjz/PIl94EUu+EDfGZDzFP+NY1EOPVt9NQvwFqq7HdMmowdg==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT OR Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=14.21.3" + } + }, + "node_modules/@biomejs/cli-win32-arm64": { + "version": "2.4.16", + "resolved": "https://registry.npmjs.org/@biomejs/cli-win32-arm64/-/cli-win32-arm64-2.4.16.tgz", + "integrity": "sha512-0rgImMsNb5v/chhkIFe3wu7PEFClS6RBAYUijGL9UsYN3PanSaoK24HSSuSJb1pYbYYVjzAyZTl3gtjJ84BM8A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT OR Apache-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=14.21.3" + } + }, + "node_modules/@biomejs/cli-win32-x64": { + "version": "2.4.16", + "resolved": "https://registry.npmjs.org/@biomejs/cli-win32-x64/-/cli-win32-x64-2.4.16.tgz", + "integrity": "sha512-Kp85jgoBHa05gix6UIRjfCDiUV3w/8VIdZ247VyyO2gEjaw12WEVhdIjlxp/AMzXxqxQwbxNTDVZ3Mwd2RG5rw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT OR Apache-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=14.21.3" + } + }, "node_modules/@emnapi/core": { "version": "1.10.0", "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.10.0.tgz", diff --git a/package.json b/package.json index 31186f2..7bde0ee 100644 --- a/package.json +++ b/package.json @@ -14,7 +14,9 @@ "dev": "tsup --watch", "test": "vitest run", "test:watch": "vitest", - "typecheck": "tsc --noEmit" + "typecheck": "tsc --noEmit", + "lint": "biome check .", + "format": "biome check --write ." }, "keywords": [ "wordpress", @@ -37,6 +39,7 @@ "zod": "^4.4.3" }, "devDependencies": { + "@biomejs/biome": "^2.4.16", "@sindresorhus/tsconfig": "^8.1.0", "@types/node": "^25.9.1", "@types/react": "^19.2.15", diff --git a/src/commands/db/dump.tsx b/src/commands/db/dump.tsx index efd4306..61f5f41 100644 --- a/src/commands/db/dump.tsx +++ b/src/commands/db/dump.tsx @@ -1,16 +1,16 @@ -import {useState, useRef} from 'react'; -import {Box, Text, useApp} from 'ink'; +import path from 'node:path'; import {createGzip} from 'node:zlib'; -import StepRunner from '../../components/StepRunner.js'; +import {Box, Text, useApp} from 'ink'; +import {useRef, useState} from 'react'; import type {Step} from '../../components/StepRunner.js'; -import {readProjectConfig} from '../../lib/config.js'; -import {getProjectRuntimeDir, getProjectBackupsDir} from '../../lib/paths.js'; -import {getMachineHostname} from '../../lib/hostname.js'; +import StepRunner from '../../components/StepRunner.js'; +import type {BackupRecord} from '../../lib/backup-storage.js'; import {createBackupStorage} from '../../lib/backup-storage.js'; import {encodeBackupId} from '../../lib/backups.js'; +import {readProjectConfig} from '../../lib/config.js'; +import {getMachineHostname} from '../../lib/hostname.js'; +import {getProjectBackupsDir, getProjectRuntimeDir} from '../../lib/paths.js'; import {spawnWpCli} from '../../lib/wpcli.js'; -import type {BackupRecord} from '../../lib/backup-storage.js'; -import path from 'node:path'; export const description = 'Create a compressed SQL backup of the database'; @@ -27,7 +27,10 @@ export default function DbDump() { if (!pc) { throw new Error('This project is not initialized. Run "kiqr init" first.'); } - const composePath = path.join(getProjectRuntimeDir(pc.project_id), 'compose.yaml'); + const composePath = path.join( + getProjectRuntimeDir(pc.project_id), + 'compose.yaml', + ); const storage = createBackupStorage(getProjectBackupsDir(pc.project_id)); const createdAt = new Date(); const id = encodeBackupId(createdAt); @@ -51,7 +54,9 @@ export default function DbDump() { if (code === 0) resolve(); else { const msg = Buffer.concat(stderrChunks).toString().trim(); - reject(new Error(`wp db export failed (exit ${code}): ${msg || 'no output'}`)); + reject( + new Error(`wp db export failed (exit ${code}): ${msg || 'no output'}`), + ); } }); }), @@ -74,10 +79,19 @@ export default function DbDump() { /> {complete && ref.current.record && ( - Backup created. + + Backup created. + - ID: {ref.current.record.id} - File: {ref.current.record.filename} + + ID:{' '} + + {ref.current.record.id} + + + + File: {ref.current.record.filename} + Restore with: kiqr db restore {ref.current.record.id} )} diff --git a/src/commands/db/index.tsx b/src/commands/db/index.tsx index 3c16d98..6ef8895 100644 --- a/src/commands/db/index.tsx +++ b/src/commands/db/index.tsx @@ -9,9 +9,18 @@ export default function DbIndex() { Backup and restore the project database Commands: - kiqr db dump Create a compressed SQL backup - kiqr db list List available backups - kiqr db restore <id> Restore from a backup + + {' '} + kiqr db dump Create a compressed SQL backup + + + {' '} + kiqr db list List available backups + + + {' '} + kiqr db restore <id> Restore from a backup + ); } diff --git a/src/commands/db/list.tsx b/src/commands/db/list.tsx index 36c6c99..1fe62cd 100644 --- a/src/commands/db/list.tsx +++ b/src/commands/db/list.tsx @@ -1,9 +1,9 @@ -import {useEffect, useState} from 'react'; import {Box, Text, useApp} from 'ink'; +import {useEffect, useState} from 'react'; +import type {BackupRecord} from '../../lib/backup-storage.js'; +import {createBackupStorage} from '../../lib/backup-storage.js'; import {readProjectConfig} from '../../lib/config.js'; import {getProjectBackupsDir} from '../../lib/paths.js'; -import {createBackupStorage} from '../../lib/backup-storage.js'; -import type {BackupRecord} from '../../lib/backup-storage.js'; export const description = 'List available database backups'; @@ -15,7 +15,7 @@ function formatBytes(n: number): string { } function formatDate(d: Date): string { - return d.toISOString().replace('T', ' ').slice(0, 19) + ' UTC'; + return `${d.toISOString().replace('T', ' ').slice(0, 19)} UTC`; } function pad(s: string, width: number): string { @@ -65,14 +65,15 @@ export default function DbList() { return ( - {pad('id', idW)} {pad('filename', fnW)} {pad('size', szW)} date + {pad('id', idW)} {pad('filename', fnW)} {pad('size', szW)} date - {'-'.repeat(idW)} {'-'.repeat(fnW)} {'-'.repeat(szW)} {'-'.repeat(23)} + {'-'.repeat(idW)} {'-'.repeat(fnW)} {'-'.repeat(szW)} {'-'.repeat(23)} {rows.map((r) => ( - {pad(r.id, idW)} {pad(r.filename, fnW)} {pad(formatBytes(r.sizeBytes), szW)} {formatDate(r.createdAt)} + {pad(r.id, idW)} {pad(r.filename, fnW)}{' '} + {pad(formatBytes(r.sizeBytes), szW)} {formatDate(r.createdAt)} ))} diff --git a/src/commands/db/restore.tsx b/src/commands/db/restore.tsx index 4fc6869..4ded4c5 100644 --- a/src/commands/db/restore.tsx +++ b/src/commands/db/restore.tsx @@ -1,26 +1,26 @@ -import {useState, useRef} from 'react'; -import {Box, Text, useApp} from 'ink'; -import {ConfirmInput, TextInput} from '@inkjs/ui'; +import path from 'node:path'; import {createGunzip, createGzip} from 'node:zlib'; -import zod from 'zod'; +import {ConfirmInput, TextInput} from '@inkjs/ui'; +import {Box, Text, useApp} from 'ink'; import {argument} from 'pastel'; -import {readProjectConfig} from '../../lib/config.js'; -import {getProjectRuntimeDir, getProjectBackupsDir} from '../../lib/paths.js'; -import {getMachineHostname} from '../../lib/hostname.js'; -import {createBackupStorage} from '../../lib/backup-storage.js'; -import {encodeBackupId} from '../../lib/backups.js'; -import {spawnWpCli, isWordPressInstalled} from '../../lib/wpcli.js'; -import StepRunner from '../../components/StepRunner.js'; +import {useRef, useState} from 'react'; +import zod from 'zod'; import type {Step} from '../../components/StepRunner.js'; +import StepRunner from '../../components/StepRunner.js'; import type {BackupRecord, BackupStorage} from '../../lib/backup-storage.js'; -import path from 'node:path'; +import {createBackupStorage} from '../../lib/backup-storage.js'; +import {encodeBackupId} from '../../lib/backups.js'; +import {readProjectConfig} from '../../lib/config.js'; +import {getMachineHostname} from '../../lib/hostname.js'; +import {getProjectBackupsDir, getProjectRuntimeDir} from '../../lib/paths.js'; +import {isWordPressInstalled, spawnWpCli} from '../../lib/wpcli.js'; export const description = 'Restore the database from a backup'; export const args = zod.tuple([ - zod.string().describe( - argument({name: 'id', description: 'Backup id (from `kiqr db list`)'}), - ), + zod + .string() + .describe(argument({name: 'id', description: 'Backup id (from `kiqr db list`)'})), ]); type Props = {args: zod.infer}; @@ -59,14 +59,23 @@ async function runDump( child.on('error', reject); child.on('close', (code) => { if (code === 0) resolve(); - else reject(new Error(`wp db export failed (exit ${code}): ${Buffer.concat(stderrChunks).toString().trim()}`)); + else + reject( + new Error( + `wp db export failed (exit ${code}): ${Buffer.concat(stderrChunks).toString().trim()}`, + ), + ); }); }), ]); return record; } -async function runRestore(composePath: string, record: BackupRecord, storage: BackupStorage): Promise { +async function runRestore( + composePath: string, + record: BackupRecord, + storage: BackupStorage, +): Promise { const child = spawnWpCli(composePath, ['wp', 'db', 'import', '-']); const stderrChunks: Buffer[] = []; child.stderr.on('data', (c: Buffer) => stderrChunks.push(c)); @@ -76,7 +85,12 @@ async function runRestore(composePath: string, record: BackupRecord, storage: Ba child.on('error', reject); child.on('close', (code) => { if (code === 0) resolve(); - else reject(new Error(`wp db import failed (exit ${code}): ${Buffer.concat(stderrChunks).toString().trim()}`)); + else + reject( + new Error( + `wp db import failed (exit ${code}): ${Buffer.concat(stderrChunks).toString().trim()}`, + ), + ); }); }); } @@ -123,7 +137,9 @@ export default function DbRestore({args}: Props) { const record = await storage.findById(args[0]); if (!record) { - setErrorMsg(`No backup with id "${args[0]}". Run "kiqr db list" to see available backups.`); + setErrorMsg( + `No backup with id "${args[0]}". Run "kiqr db list" to see available backups.`, + ); setPhase('error'); setTimeout(() => exit(new Error()), 100); return; @@ -131,7 +147,8 @@ export default function DbRestore({args}: Props) { ref.current.record = record; const recent = await storage.mostRecent(); - const hasRecent = recent && Date.now() - recent.createdAt.getTime() < FIVE_MINUTES_MS; + const hasRecent = + recent && Date.now() - recent.createdAt.getTime() < FIVE_MINUTES_MS; if (!hasRecent) { setPhase('ask-safety-backup'); } else { @@ -181,7 +198,12 @@ export default function DbRestore({args}: Props) { This will overwrite the current database with backup {ref.current.record!.id}. - To confirm, solve this: What is {challenge.a} + {challenge.b}? + + To confirm, solve this:{' '} + + What is {challenge.a} + {challenge.b}? + + {wrong && Wrong answer. } Answer: @@ -242,7 +264,11 @@ export default function DbRestore({args}: Props) { { label: `Restoring database from ${ref.current.record!.id}...`, run: async () => { - await runRestore(ref.current.composePath, ref.current.record!, ref.current.storage!); + await runRestore( + ref.current.composePath, + ref.current.record!, + ref.current.storage!, + ); }, }, ]; @@ -263,7 +289,9 @@ export default function DbRestore({args}: Props) { // phase === 'done' return ( - Database restored from {ref.current.record!.id}. + + Database restored from {ref.current.record!.id}. + {ref.current.safetyRecord && ( Safety backup saved as {ref.current.safetyRecord.id}. )} diff --git a/src/commands/destroy.tsx b/src/commands/destroy.tsx index ed13213..b85fdd3 100644 --- a/src/commands/destroy.tsx +++ b/src/commands/destroy.tsx @@ -1,14 +1,14 @@ -import {useState, useRef} from 'react'; -import {Box, Text, useApp} from 'ink'; +import fs from 'node:fs'; +import path from 'node:path'; import {TextInput} from '@inkjs/ui'; -import StepRunner from '../components/StepRunner.js'; +import {Box, Text, useApp} from 'ink'; +import {useRef, useState} from 'react'; import type {Step} from '../components/StepRunner.js'; -import {runDockerCompose} from '../lib/docker.js'; +import StepRunner from '../components/StepRunner.js'; import {readProjectConfig} from '../lib/config.js'; +import {runDockerCompose} from '../lib/docker.js'; import {getProjectRuntimeDir} from '../lib/paths.js'; import {stopTraefikIfIdle} from '../lib/traefik.js'; -import path from 'node:path'; -import fs from 'node:fs'; import type {ProjectConfig} from '../types/config.js'; export const description = 'Stop and remove all site data (database, uploads, etc.)'; @@ -35,10 +35,19 @@ export default function Destroy() { if (!confirmed) { return ( - This will permanently delete all site data. - Database, uploads, plugins, and configuration will be lost. + + This will permanently delete all site data. + + + Database, uploads, plugins, and configuration will be lost. + - To confirm, solve this: What is {challenge.a} + {challenge.b}? + + To confirm, solve this:{' '} + + What is {challenge.a} + {challenge.b}? + + {wrong && Wrong answer. } Answer: @@ -121,8 +130,12 @@ export default function Destroy() { /> {complete && ( - Project destroyed. - Run kiqr init to start fresh. + + Project destroyed. + + + Run kiqr init to start fresh. + )} diff --git a/src/commands/doctor.tsx b/src/commands/doctor.tsx index 84307a2..5c6e689 100644 --- a/src/commands/doctor.tsx +++ b/src/commands/doctor.tsx @@ -25,9 +25,7 @@ export default function Doctor() { {checks.map((check) => ( - - {check.ok ? '✓' : '✗'}{' '} - + {check.ok ? '✓' : '✗'} {check.name} — {check.detail} @@ -39,8 +37,7 @@ export default function Doctor() { ) : ( - Some checks failed. Resolve the issues above and run "kiqr doctor" - again. + Some checks failed. Resolve the issues above and run "kiqr doctor" again. )} diff --git a/src/commands/down.tsx b/src/commands/down.tsx index 5e4f09b..84a8a8b 100644 --- a/src/commands/down.tsx +++ b/src/commands/down.tsx @@ -1,12 +1,12 @@ -import {useState, useRef} from 'react'; +import path from 'node:path'; import {Box, Text, useApp} from 'ink'; -import StepRunner from '../components/StepRunner.js'; +import {useRef, useState} from 'react'; import type {Step} from '../components/StepRunner.js'; -import {runDockerCompose} from '../lib/docker.js'; +import StepRunner from '../components/StepRunner.js'; import {readProjectConfig} from '../lib/config.js'; +import {runDockerCompose} from '../lib/docker.js'; import {getProjectRuntimeDir} from '../lib/paths.js'; import {stopTraefikIfIdle} from '../lib/traefik.js'; -import path from 'node:path'; import type {ProjectConfig} from '../types/config.js'; export const description = 'Stop the WordPress development environment'; @@ -60,7 +60,9 @@ export default function Down() { /> {complete && ( - Your site has been stopped. + + Your site has been stopped. + )} diff --git a/src/commands/index.tsx b/src/commands/index.tsx index 3e7d1ed..7476da3 100644 --- a/src/commands/index.tsx +++ b/src/commands/index.tsx @@ -9,20 +9,58 @@ export default function Index() { Local WordPress theme development Commands: - kiqr doctor Check your environment for common problems - kiqr init Initialize a new project - kiqr up Start the development environment - kiqr down Stop the development environment - kiqr restart Restart the development environment - kiqr watch Watch files and auto-reload browser - kiqr info Show project info and credentials - kiqr open Open site, admin, or phpMyAdmin in browser - kiqr wp Run a WP-CLI command - kiqr db Backup and restore the database - kiqr logs Show WordPress logs - kiqr destroy Remove all site data and start fresh + + {' '} + kiqr doctor Check your environment for common problems + + + {' '} + kiqr init Initialize a new project + + + {' '} + kiqr up Start the development environment + + + {' '} + kiqr down Stop the development environment + + + {' '} + kiqr restart Restart the development environment + + + {' '} + kiqr watch Watch files and auto-reload browser + + + {' '} + kiqr info Show project info and credentials + + + {' '} + kiqr open Open site, admin, or phpMyAdmin in browser + + + {' '} + kiqr wp Run a WP-CLI command + + + {' '} + kiqr db Backup and restore the database + + + {' '} + kiqr logs Show WordPress logs + + + {' '} + kiqr destroy Remove all site data and start fresh + - Run kiqr --help for more information. + + Run kiqr --help for more information. + ); } diff --git a/src/commands/info.tsx b/src/commands/info.tsx index 1e6057d..006e40c 100644 --- a/src/commands/info.tsx +++ b/src/commands/info.tsx @@ -1,7 +1,7 @@ import {Box, Text, useApp} from 'ink'; -import {readProjectConfig, readLocalConfig} from '../lib/config.js'; -import {getProjectRuntimeDir} from '../lib/paths.js'; +import {readLocalConfig, readProjectConfig} from '../lib/config.js'; import {buildProjectHostname} from '../lib/hostname.js'; +import {getProjectRuntimeDir} from '../lib/paths.js'; export const description = 'Show project info and credentials'; @@ -28,27 +28,63 @@ export default function Info() { return ( Project - Name: {pc.name} - ID: {pc.project_id} + + {' '} + Name: {pc.name} + + + {' '} + ID: {pc.project_id} + URLs - Site: http://{hostname}:5477 - Admin: http://{hostname}:5477/wp-admin - phpMyAdmin: http://{phpMyAdminHostname}:5477 + + {' '} + Site: http://{hostname}:5477 + + + {' '} + Admin: http://{hostname}:5477/wp-admin + + + {' '} + phpMyAdmin: http://{phpMyAdminHostname}:5477 + WordPress - Username: admin - Password: admin + + {' '} + Username: admin + + + {' '} + Password: admin + Database - Host: mariadb - Name: wordpress - User: wordpress - Password: {lc?.db_password ?? 'unknown'} - Root pass: {lc?.db_password ?? 'unknown'} + + {' '} + Host: mariadb + + + {' '} + Name: wordpress + + + {' '} + User: wordpress + + + {' '} + Password: {lc?.db_password ?? 'unknown'} + + + {' '} + Root pass: {lc?.db_password ?? 'unknown'} + ); } diff --git a/src/commands/init.tsx b/src/commands/init.tsx index 9ad5d27..77c2468 100644 --- a/src/commands/init.tsx +++ b/src/commands/init.tsx @@ -1,13 +1,17 @@ -import {useState, useRef} from 'react'; -import {Box, Text, useApp} from 'ink'; import {randomUUID} from 'node:crypto'; -import StepRunner from '../components/StepRunner.js'; +import {Box, Text, useApp} from 'ink'; +import {useRef, useState} from 'react'; import type {Step} from '../components/StepRunner.js'; +import StepRunner from '../components/StepRunner.js'; +import { + projectConfigExists, + writeLocalConfig, + writeProjectConfig, +} from '../lib/config.js'; import {isDockerInstalled, isDockerRunning} from '../lib/docker.js'; -import {detectTheme} from '../lib/theme.js'; -import {projectConfigExists, writeProjectConfig, writeLocalConfig} from '../lib/config.js'; import {getProjectRuntimeDir} from '../lib/paths.js'; -import type {ProjectConfig, LocalConfig, ThemeInfo} from '../types/config.js'; +import {detectTheme} from '../lib/theme.js'; +import type {LocalConfig, ProjectConfig, ThemeInfo} from '../types/config.js'; export const description = 'Initialize a new Kiqr project in the current directory'; @@ -101,11 +105,17 @@ export default function Init() { /> {complete && ( - Your project is ready! + + Your project is ready! + - Theme: {themeName} + + Theme: {themeName} + - Run kiqr up to start your site. + + Run kiqr up to start your site. + )} diff --git a/src/commands/logs.tsx b/src/commands/logs.tsx index 3a4e412..e05294d 100644 --- a/src/commands/logs.tsx +++ b/src/commands/logs.tsx @@ -1,9 +1,9 @@ -import {useEffect} from 'react'; +import {execSync} from 'node:child_process'; +import path from 'node:path'; import {Box, Text, useApp} from 'ink'; +import {useEffect} from 'react'; import {readProjectConfig} from '../lib/config.js'; import {getProjectRuntimeDir} from '../lib/paths.js'; -import {execSync} from 'node:child_process'; -import path from 'node:path'; export const description = 'Show WordPress logs'; diff --git a/src/commands/open.tsx b/src/commands/open.tsx index 32818ff..6aa4686 100644 --- a/src/commands/open.tsx +++ b/src/commands/open.tsx @@ -1,28 +1,35 @@ -import {useEffect} from 'react'; -import {Box, Text, useApp} from 'ink'; import {execSync} from 'node:child_process'; -import zod from 'zod'; +import {Box, Text, useApp} from 'ink'; import {argument} from 'pastel'; -import {readProjectConfig, readLocalConfig} from '../lib/config.js'; -import {getProjectRuntimeDir, getProjectPluginsDir, getProjectUploadsDir} from '../lib/paths.js'; +import {useEffect} from 'react'; +import zod from 'zod'; +import {readLocalConfig, readProjectConfig} from '../lib/config.js'; import {buildProjectHostname} from '../lib/hostname.js'; +import { + getProjectPluginsDir, + getProjectRuntimeDir, + getProjectUploadsDir, +} from '../lib/paths.js'; export const description = 'Open a site URL or folder in your browser/explorer'; export const args = zod.tuple([ - zod.string().optional().describe( - argument({ - name: 'app', - description: - 'What to open (default: wp)\n\n' + - ' wp, wordpress, web Open the site\n' + - ' admin, wpadmin, dashboard Open the WordPress dashboard (auto-login)\n' + - ' phpmyadmin, pma Open phpMyAdmin (auto-login)\n' + - ' plugins Open the plugins folder\n' + - ' uploads, media Open the uploads folder\n' + - ' data Open the Kiqr project data folder', - }), - ), + zod + .string() + .optional() + .describe( + argument({ + name: 'app', + description: + 'What to open (default: wp)\n\n' + + ' wp, wordpress, web Open the site\n' + + ' admin, wpadmin, dashboard Open the WordPress dashboard (auto-login)\n' + + ' phpmyadmin, pma Open phpMyAdmin (auto-login)\n' + + ' plugins Open the plugins folder\n' + + ' uploads, media Open the uploads folder\n' + + ' data Open the Kiqr project data folder', + }), + ), ]); type Props = { diff --git a/src/commands/restart.tsx b/src/commands/restart.tsx index cf77331..6c085aa 100644 --- a/src/commands/restart.tsx +++ b/src/commands/restart.tsx @@ -1,18 +1,27 @@ -import {useState, useRef} from 'react'; +import fs from 'node:fs'; +import path from 'node:path'; import {Box, Text, useApp} from 'ink'; -import StepRunner from '../components/StepRunner.js'; +import {useRef, useState} from 'react'; import type {Step} from '../components/StepRunner.js'; -import {isDockerInstalled, isDockerRunning, runDockerCompose, removeDockerVolume} from '../lib/docker.js'; -import {readProjectConfig, readLocalConfig, writeLocalConfig} from '../lib/config.js'; -import {getProjectRuntimeDir, getProjectPluginsDir, getProjectUploadsDir} from '../lib/paths.js'; -import {ensureTraefikRunning} from '../lib/traefik.js'; -import {buildProjectHostname} from '../lib/hostname.js'; -import {detectTheme} from '../lib/theme.js'; +import StepRunner from '../components/StepRunner.js'; import {writeProjectCompose} from '../lib/compose.js'; +import {readLocalConfig, readProjectConfig, writeLocalConfig} from '../lib/config.js'; +import { + isDockerInstalled, + isDockerRunning, + removeDockerVolume, + runDockerCompose, +} from '../lib/docker.js'; +import {buildProjectHostname} from '../lib/hostname.js'; import {writeMuPlugin} from '../lib/mu-plugin.js'; -import fs from 'node:fs'; -import path from 'node:path'; -import type {ProjectConfig, LocalConfig} from '../types/config.js'; +import { + getProjectPluginsDir, + getProjectRuntimeDir, + getProjectUploadsDir, +} from '../lib/paths.js'; +import {detectTheme} from '../lib/theme.js'; +import {ensureTraefikRunning} from '../lib/traefik.js'; +import type {LocalConfig, ProjectConfig} from '../types/config.js'; export const description = 'Restart the WordPress development environment'; @@ -165,13 +174,19 @@ export default function Restart() { /> {complete && ( - Your site is ready! + + Your site is ready! + Site: - {siteUrl} + + {siteUrl} + phpMyAdmin: - {pmaUrl} + + {pmaUrl} + )} diff --git a/src/commands/up.tsx b/src/commands/up.tsx index 93d4847..72cdcf8 100644 --- a/src/commands/up.tsx +++ b/src/commands/up.tsx @@ -1,26 +1,35 @@ -import {useState, useRef} from 'react'; -import {Box, Text, useApp} from 'ink'; -import {ConfirmInput} from '@inkjs/ui'; import {randomUUID} from 'node:crypto'; -import StepRunner from '../components/StepRunner.js'; +import fs from 'node:fs'; +import path from 'node:path'; +import {ConfirmInput} from '@inkjs/ui'; +import {Box, Text, useApp} from 'ink'; +import {useRef, useState} from 'react'; import type {Step} from '../components/StepRunner.js'; -import {isDockerInstalled, isDockerRunning, runDockerCompose, removeDockerVolume} from '../lib/docker.js'; +import StepRunner from '../components/StepRunner.js'; +import {writeProjectCompose} from '../lib/compose.js'; import { - readProjectConfig, + projectConfigExists, readLocalConfig, - writeProjectConfig, + readProjectConfig, writeLocalConfig, - projectConfigExists, + writeProjectConfig, } from '../lib/config.js'; -import {getProjectRuntimeDir, getProjectPluginsDir, getProjectUploadsDir} from '../lib/paths.js'; -import {ensureTraefikRunning} from '../lib/traefik.js'; +import { + isDockerInstalled, + isDockerRunning, + removeDockerVolume, + runDockerCompose, +} from '../lib/docker.js'; import {buildProjectHostname} from '../lib/hostname.js'; -import {detectTheme} from '../lib/theme.js'; -import {writeProjectCompose} from '../lib/compose.js'; import {writeMuPlugin} from '../lib/mu-plugin.js'; -import fs from 'node:fs'; -import path from 'node:path'; -import type {ProjectConfig, LocalConfig} from '../types/config.js'; +import { + getProjectPluginsDir, + getProjectRuntimeDir, + getProjectUploadsDir, +} from '../lib/paths.js'; +import {detectTheme} from '../lib/theme.js'; +import {ensureTraefikRunning} from '../lib/traefik.js'; +import type {LocalConfig, ProjectConfig} from '../types/config.js'; export const description = 'Start the WordPress development environment'; @@ -39,7 +48,13 @@ export default function Up() { runtimeDir: string; composePath: string; checked: boolean; - }>({projectConfig: null, localConfig: null, runtimeDir: '', composePath: '', checked: false}); + }>({ + projectConfig: null, + localConfig: null, + runtimeDir: '', + composePath: '', + checked: false, + }); if (!ref.current.checked) { ref.current.checked = true; @@ -57,14 +72,12 @@ export default function Up() { return ( - No project configuration found, but {themeName} looks like a WordPress theme. + No project configuration found, but {themeName} looks like a + WordPress theme. Initialize this project? - setConfirmed(true)} - onCancel={() => exit()} - /> + setConfirmed(true)} onCancel={() => exit()} /> ); @@ -229,13 +242,19 @@ export default function Up() { /> {complete && ( - Your site is ready! + + Your site is ready! + Site: - {siteUrl} + + {siteUrl} + phpMyAdmin: - {pmaUrl} + + {pmaUrl} + WordPress may take a minute to fully start on first run. diff --git a/src/commands/watch.tsx b/src/commands/watch.tsx index 43b1d06..a4d7b0a 100644 --- a/src/commands/watch.tsx +++ b/src/commands/watch.tsx @@ -1,9 +1,9 @@ -import {useState, useEffect, useRef} from 'react'; import {Box, Text} from 'ink'; +import {useEffect, useRef, useState} from 'react'; import {readProjectConfig} from '../lib/config.js'; +import {LiveReloadServer} from '../lib/livereload.js'; import {detectTheme} from '../lib/theme.js'; import {createFileWatcher, getRelativePath} from '../lib/watch.js'; -import {LiveReloadServer} from '../lib/livereload.js'; export const description = 'Watch theme files and auto-reload the browser'; @@ -55,7 +55,16 @@ export default function Watch() { const watcher = createFileWatcher( theme.path, { - extensions: ['.php', '.css', '.js', '.scss', '.sass', '.less', '.html', '.txt'], + extensions: [ + '.php', + '.css', + '.js', + '.scss', + '.sass', + '.less', + '.html', + '.txt', + ], }, (eventType, filePath) => { const relativePath = getRelativePath(theme.path, filePath); @@ -106,7 +115,9 @@ export default function Watch() { if (error) { return ( - Error + + Error + {error} Press any key to exit... @@ -126,7 +137,9 @@ export default function Watch() { return ( - Watching for file changes + + Watching for file changes + Browser will auto-reload on changes @@ -146,17 +159,18 @@ export default function Watch() { {changes.length === 0 ? ( Waiting for changes... ) : ( - changes.slice(-10).reverse().map((change, i) => ( - - - [{change.timestamp.toLocaleTimeString()}] - {' '} - - [{change.type}] - {' '} - {change.path} - - )) + changes + .slice(-10) + .reverse() + .map((change, i) => ( + + [{change.timestamp.toLocaleTimeString()}]{' '} + + [{change.type}] + {' '} + {change.path} + + )) )} @@ -165,4 +179,4 @@ export default function Watch() { ); -} \ No newline at end of file +} diff --git a/src/commands/wp.tsx b/src/commands/wp.tsx index 88130bf..5167df5 100644 --- a/src/commands/wp.tsx +++ b/src/commands/wp.tsx @@ -1,11 +1,11 @@ -import {useEffect} from 'react'; -import {Box, Text, useApp} from 'ink'; import {execSync} from 'node:child_process'; -import zod from 'zod'; +import path from 'node:path'; +import {Box, Text, useApp} from 'ink'; import {argument} from 'pastel'; +import {useEffect} from 'react'; +import zod from 'zod'; import {readProjectConfig} from '../lib/config.js'; import {getProjectRuntimeDir} from '../lib/paths.js'; -import path from 'node:path'; export const description = 'Run a WP-CLI command'; @@ -22,7 +22,7 @@ type Props = { args: zod.infer; }; -export default function Wp({}: Props) { +export default function Wp(_props: Props) { const {exit} = useApp(); useEffect(() => { @@ -41,19 +41,17 @@ export default function Wp({}: Props) { const rawArgs = wpIndex >= 0 ? process.argv.slice(wpIndex + 1) : []; if (rawArgs.length === 0) { - execSync( - `docker compose -f "${composePath}" run --rm wpcli --help`, - {stdio: 'inherit'}, - ); + execSync(`docker compose -f "${composePath}" run --rm wpcli --help`, { + stdio: 'inherit', + }); exit(); return; } try { - execSync( - `docker compose -f "${composePath}" run --rm wpcli ${rawArgs.join(' ')}`, - {stdio: 'inherit'}, - ); + execSync(`docker compose -f "${composePath}" run --rm wpcli ${rawArgs.join(' ')}`, { + stdio: 'inherit', + }); } catch { // WP-CLI exits with non-zero for errors — already printed to stderr } diff --git a/src/components/StepRunner.tsx b/src/components/StepRunner.tsx index 87d6782..9685de6 100644 --- a/src/components/StepRunner.tsx +++ b/src/components/StepRunner.tsx @@ -1,6 +1,6 @@ -import {useState, useEffect} from 'react'; -import {Box, Text} from 'ink'; import {Spinner, StatusMessage} from '@inkjs/ui'; +import {Box, Text} from 'ink'; +import {useEffect, useState} from 'react'; export interface Step { label: string; diff --git a/src/lib/backup-storage.ts b/src/lib/backup-storage.ts index a594703..adf1c5c 100644 --- a/src/lib/backup-storage.ts +++ b/src/lib/backup-storage.ts @@ -1,13 +1,13 @@ +import crypto from 'node:crypto'; import fs from 'node:fs'; import path from 'node:path'; -import crypto from 'node:crypto'; -import {pipeline} from 'node:stream/promises'; import type {Readable} from 'node:stream'; +import {pipeline} from 'node:stream/promises'; import { BACKUP_EXTENSION, - SIDECAR_EXTENSION, buildBackupFilename, parseBackupFilename, + SIDECAR_EXTENSION, } from './backups.js'; export interface BackupRecord { @@ -70,7 +70,9 @@ export class LocalBackupStorage implements BackupStorage { const backupPath = path.join(this.dir, name); const sidecarPath = this.sidecarPathFor(backupPath); if (!fs.existsSync(sidecarPath)) continue; - const sidecar = JSON.parse(await fs.promises.readFile(sidecarPath, 'utf-8')) as SidecarJson; + const sidecar = JSON.parse( + await fs.promises.readFile(sidecarPath, 'utf-8'), + ) as SidecarJson; records.push({ id: sidecar.id, filename: name, @@ -99,7 +101,11 @@ export class LocalBackupStorage implements BackupStorage { async save(stream: Readable, opts: SaveOptions): Promise { fs.mkdirSync(this.dir, {recursive: true}); - const filename = buildBackupFilename({host: opts.host, date: opts.createdAt, id: opts.id}); + const filename = buildBackupFilename({ + host: opts.host, + date: opts.createdAt, + id: opts.id, + }); const backupPath = path.join(this.dir, filename); const sidecarPath = this.sidecarPathFor(backupPath); diff --git a/src/lib/compose.ts b/src/lib/compose.ts index 1658b70..09fc2ea 100644 --- a/src/lib/compose.ts +++ b/src/lib/compose.ts @@ -1,6 +1,6 @@ -import YAML from 'yaml'; import fs from 'node:fs'; import path from 'node:path'; +import YAML from 'yaml'; import type {RuntimeConfig} from '../providers/RuntimeProvider.js'; import {createRuntimeProvider} from './runtime.js'; import {KIQR_NETWORK} from './traefik.js'; diff --git a/src/lib/config.ts b/src/lib/config.ts index c6f9cd3..102a55f 100644 --- a/src/lib/config.ts +++ b/src/lib/config.ts @@ -42,11 +42,7 @@ function formatIssues(error: z.ZodError): string { .join(', '); } -function parseConfig( - schema: z.ZodType, - content: string, - fileName: string, -): T { +function parseConfig(schema: z.ZodType, content: string, fileName: string): T { let parsed: unknown; try { parsed = YAML.parse(content); @@ -66,9 +62,7 @@ export function projectConfigExists(dir: string = process.cwd()): boolean { return fs.existsSync(path.join(dir, PROJECT_CONFIG_FILE)); } -export function readProjectConfig( - dir: string = process.cwd(), -): ProjectConfig | null { +export function readProjectConfig(dir: string = process.cwd()): ProjectConfig | null { const filePath = path.join(dir, PROJECT_CONFIG_FILE); if (!fs.existsSync(filePath)) return null; const content = fs.readFileSync(filePath, 'utf-8'); diff --git a/src/lib/docker.ts b/src/lib/docker.ts index 495e953..d3ecdad 100644 --- a/src/lib/docker.ts +++ b/src/lib/docker.ts @@ -58,7 +58,9 @@ export function isContainerRunning(name: string): boolean { const output = execSync( `docker ps --filter "name=${name}" --filter "status=running" --format "{{.Names}}"`, {stdio: 'pipe'}, - ).toString().trim(); + ) + .toString() + .trim(); return output.includes(name); } catch { return false; diff --git a/src/lib/hostname.ts b/src/lib/hostname.ts index 5e49e21..40f1330 100644 --- a/src/lib/hostname.ts +++ b/src/lib/hostname.ts @@ -14,10 +14,7 @@ export function sanitizeHostname(raw: string): string { .replace(/^-|-$/g, ''); } -export function buildProjectHostname( - projectSlug: string, - subdomain?: string, -): string { +export function buildProjectHostname(projectSlug: string, subdomain?: string): string { const machine = getMachineHostname(); const base = `${projectSlug}.${machine}.lvh.me`; if (subdomain) { diff --git a/src/lib/livereload.ts b/src/lib/livereload.ts index c6b2b72..01aa77f 100644 --- a/src/lib/livereload.ts +++ b/src/lib/livereload.ts @@ -106,7 +106,7 @@ export class LiveReloadServer { res.writeHead(200, { 'Content-Type': 'text/event-stream', 'Cache-Control': 'no-cache', - 'Connection': 'keep-alive', + Connection: 'keep-alive', 'Access-Control-Allow-Origin': '*', }); @@ -171,4 +171,4 @@ export class LiveReloadServer { getClientsCount(): number { return this.clients.length; } -} \ No newline at end of file +} diff --git a/src/lib/mu-plugin.ts b/src/lib/mu-plugin.ts index ed8fc74..65b9595 100644 --- a/src/lib/mu-plugin.ts +++ b/src/lib/mu-plugin.ts @@ -13,54 +13,54 @@ if (!defined('ABSPATH') || !defined('KIQR_DEVELOPMENT') || !KIQR_DEVELOPMENT) { // Auto-activate the mounted theme on first boot add_action('init', function () { - \$slug = getenv('KIQR_THEME_SLUG'); - if (!\$slug) return; + $slug = getenv('KIQR_THEME_SLUG'); + if (!$slug) return; - \$active = get_option('stylesheet'); - if (\$active === \$slug) return; + $active = get_option('stylesheet'); + if ($active === $slug) return; // Only auto-activate if the theme hasn't been manually changed before if (get_option('kiqr_theme_activated')) return; - \$theme = wp_get_theme(\$slug); - if (!\$theme->exists()) return; + $theme = wp_get_theme($slug); + if (!$theme->exists()) return; - switch_theme(\$slug); + switch_theme($slug); update_option('kiqr_theme_activated', '1'); }, 1); // Auto login via signed URL add_action('init', function () { - if (empty(\$_GET['kiqr_login']) || is_user_logged_in()) { + if (empty($_GET['kiqr_login']) || is_user_logged_in()) { return; } - \$secret = getenv('KIQR_LOGIN_SECRET'); - if (!\$secret || !hash_equals(\$secret, \$_GET['kiqr_login'])) { + $secret = getenv('KIQR_LOGIN_SECRET'); + if (!$secret || !hash_equals($secret, $_GET['kiqr_login'])) { return; } - \$user = get_user_by('login', 'admin'); - if (!\$user) { - \$user = get_users(['role' => 'administrator', 'number' => 1])[0] ?? null; + $user = get_user_by('login', 'admin'); + if (!$user) { + $user = get_users(['role' => 'administrator', 'number' => 1])[0] ?? null; } - if (!\$user) { + if (!$user) { return; } - wp_set_current_user(\$user->ID); - wp_set_auth_cookie(\$user->ID, true); + wp_set_current_user($user->ID); + wp_set_auth_cookie($user->ID, true); - \$redirect = remove_query_arg('kiqr_login'); - wp_safe_redirect(\$redirect ?: admin_url()); + $redirect = remove_query_arg('kiqr_login'); + wp_safe_redirect($redirect ?: admin_url()); exit; }); // Inject LiveReload script in development add_action('wp_footer', function () { - \$lr_port = getenv('KIQR_LIVERELOAD_PORT'); - if (!\$lr_port) \$lr_port = '35729'; - echo ''; + $lr_port = getenv('KIQR_LIVERELOAD_PORT'); + if (!$lr_port) $lr_port = '35729'; + echo ''; }); `; diff --git a/src/lib/paths.ts b/src/lib/paths.ts index e274e97..435a7a6 100644 --- a/src/lib/paths.ts +++ b/src/lib/paths.ts @@ -33,14 +33,23 @@ export function getTraefikDir(platform: NodeJS.Platform = process.platform): str return path.join(getKiqrDataDir(platform), 'traefik'); } -export function getProjectPluginsDir(projectId: string, platform: NodeJS.Platform = process.platform): string { +export function getProjectPluginsDir( + projectId: string, + platform: NodeJS.Platform = process.platform, +): string { return path.join(getProjectRuntimeDir(projectId, platform), 'plugins'); } -export function getProjectUploadsDir(projectId: string, platform: NodeJS.Platform = process.platform): string { +export function getProjectUploadsDir( + projectId: string, + platform: NodeJS.Platform = process.platform, +): string { return path.join(getProjectRuntimeDir(projectId, platform), 'uploads'); } -export function getProjectBackupsDir(projectId: string, platform: NodeJS.Platform = process.platform): string { +export function getProjectBackupsDir( + projectId: string, + platform: NodeJS.Platform = process.platform, +): string { return path.join(getProjectRuntimeDir(projectId, platform), 'backups'); } diff --git a/src/lib/runtime.ts b/src/lib/runtime.ts index 28c6fb1..a1ac3fc 100644 --- a/src/lib/runtime.ts +++ b/src/lib/runtime.ts @@ -1,8 +1,11 @@ -import type {RuntimeProvider} from '../providers/RuntimeProvider.js'; import {BitnamiRuntimeProvider} from '../providers/BitnamiRuntimeProvider.js'; +import type {RuntimeProvider} from '../providers/RuntimeProvider.js'; -export type {RuntimeProvider} from '../providers/RuntimeProvider.js'; -export type {RuntimeConfig, ComposeService} from '../providers/RuntimeProvider.js'; +export type { + ComposeService, + RuntimeConfig, + RuntimeProvider, +} from '../providers/RuntimeProvider.js'; export function createRuntimeProvider(runtime: string = 'bitnami'): RuntimeProvider { switch (runtime) { diff --git a/src/lib/traefik.ts b/src/lib/traefik.ts index 4e056a2..e0f0b9a 100644 --- a/src/lib/traefik.ts +++ b/src/lib/traefik.ts @@ -1,11 +1,11 @@ -import YAML from 'yaml'; +import {execSync} from 'node:child_process'; import fs from 'node:fs'; import path from 'node:path'; -import {execSync} from 'node:child_process'; -import {getTraefikDir} from './paths.js'; +import YAML from 'yaml'; import {runDockerCompose} from './docker.js'; -import {writeSplashPage} from './splash.js'; import {writeNginxSplashConf} from './nginx-splash.js'; +import {getTraefikDir} from './paths.js'; +import {writeSplashPage} from './splash.js'; export const KIQR_NETWORK = 'kiqr'; const TRAEFIK_CONTAINER = 'kiqr-traefik'; @@ -96,7 +96,9 @@ function hasRunningProjects(): boolean { const output = execSync( `docker ps --filter "network=${KIQR_NETWORK}" --filter "status=running" --format "{{.Names}}"`, {stdio: 'pipe'}, - ).toString().trim(); + ) + .toString() + .trim(); if (!output) return false; diff --git a/src/lib/watch.ts b/src/lib/watch.ts index 19d0896..3424a8a 100644 --- a/src/lib/watch.ts +++ b/src/lib/watch.ts @@ -6,7 +6,10 @@ export interface WatchOptions { ignored?: RegExp | string[]; } -export type FileChangeCallback = (event: 'add' | 'change' | 'unlink', filePath: string) => void; +export type FileChangeCallback = ( + event: 'add' | 'change' | 'unlink', + filePath: string, +) => void; export function createFileWatcher( watchDir: string, @@ -29,13 +32,7 @@ export function createFileWatcher( const relativePath = path.relative(watchDir, filePath); // Ignore common non-theme directories - const ignorePatterns = [ - /node_modules/, - /\.git/, - /\.env/, - /vendor/, - /\.cache/, - ]; + const ignorePatterns = [/node_modules/, /\.git/, /\.env/, /vendor/, /\.cache/]; return ignorePatterns.some((pattern) => pattern.test(relativePath)); } @@ -46,10 +43,7 @@ export function createFileWatcher( return extensions.some((e) => e.toLowerCase() === ext); } - function handleFileChange( - event: 'add' | 'change' | 'unlink', - filePath: string, - ) { + function handleFileChange(event: 'add' | 'change' | 'unlink', filePath: string) { if (isIgnored(filePath)) return; if (!matchesExtensions(filePath)) return; diff --git a/src/lib/wpcli.ts b/src/lib/wpcli.ts index d6e9074..3a182a4 100644 --- a/src/lib/wpcli.ts +++ b/src/lib/wpcli.ts @@ -1,4 +1,4 @@ -import {spawn, spawnSync, type ChildProcessByStdio} from 'node:child_process'; +import {type ChildProcessByStdio, spawn, spawnSync} from 'node:child_process'; import type {Readable, Writable} from 'node:stream'; export function buildWpCliArgs(composePath: string, wpArgs: string[]): string[] { @@ -14,8 +14,12 @@ export function spawnWpCli(composePath: string, wpArgs: string[]): WpCliProcess } export function isWordPressInstalled(composePath: string): boolean { - const result = spawnSync('docker', buildWpCliArgs(composePath, ['wp', 'core', 'is-installed']), { - stdio: 'pipe', - }); + const result = spawnSync( + 'docker', + buildWpCliArgs(composePath, ['wp', 'core', 'is-installed']), + { + stdio: 'pipe', + }, + ); return result.status === 0; } diff --git a/src/providers/BitnamiRuntimeProvider.ts b/src/providers/BitnamiRuntimeProvider.ts index 1835677..8f2d79f 100644 --- a/src/providers/BitnamiRuntimeProvider.ts +++ b/src/providers/BitnamiRuntimeProvider.ts @@ -1,8 +1,8 @@ import type { - RuntimeProvider, - RuntimeConfig, ComposeService, DatabaseCredentials, + RuntimeConfig, + RuntimeProvider, } from './RuntimeProvider.js'; const KIQR_NETWORK = 'kiqr'; diff --git a/tests/lib/backup-storage.test.ts b/tests/lib/backup-storage.test.ts index efb82ac..cafaa9b 100644 --- a/tests/lib/backup-storage.test.ts +++ b/tests/lib/backup-storage.test.ts @@ -1,13 +1,13 @@ -import {describe, it, expect, beforeEach, afterEach} from 'vitest'; import fs from 'node:fs'; import os from 'node:os'; import path from 'node:path'; import {Readable} from 'node:stream'; import {createGzip} from 'node:zlib'; +import {afterEach, beforeEach, describe, expect, it} from 'vitest'; import { + createBackupStorage, LocalBackupStorage, RemoteBackupStorage, - createBackupStorage, } from '../../src/lib/backup-storage.js'; let tmp: string; @@ -33,7 +33,9 @@ describe('LocalBackupStorage.save', () => { expect(record.filename).toBe('my-mbp-20260605-143012-abc12xy.sql.gz'); expect(fs.existsSync(record.path)).toBe(true); expect(fs.existsSync(record.path.replace('.sql.gz', '.json'))).toBe(true); - const sidecar = JSON.parse(fs.readFileSync(record.path.replace('.sql.gz', '.json'), 'utf-8')); + const sidecar = JSON.parse( + fs.readFileSync(record.path.replace('.sql.gz', '.json'), 'utf-8'), + ); expect(sidecar.id).toBe('abc12xy'); expect(sidecar.projectId).toBe('proj-1'); expect(sidecar.sha256).toMatch(/^[0-9a-f]{64}$/); diff --git a/tests/lib/backups.test.ts b/tests/lib/backups.test.ts index 6288925..40531b6 100644 --- a/tests/lib/backups.test.ts +++ b/tests/lib/backups.test.ts @@ -1,10 +1,10 @@ -import {describe, it, expect} from 'vitest'; +import {describe, expect, it} from 'vitest'; import { - encodeBackupId, - decodeBackupTimestamp, + BACKUP_EXTENSION, buildBackupFilename, + decodeBackupTimestamp, + encodeBackupId, parseBackupFilename, - BACKUP_EXTENSION, SIDECAR_EXTENSION, } from '../../src/lib/backups.js'; diff --git a/tests/lib/compose.test.ts b/tests/lib/compose.test.ts index f9165b8..181c8f1 100644 --- a/tests/lib/compose.test.ts +++ b/tests/lib/compose.test.ts @@ -1,7 +1,7 @@ -import {describe, it, expect} from 'vitest'; +import {describe, expect, it} from 'vitest'; +import YAML from 'yaml'; import {generateProjectCompose} from '../../src/lib/compose.js'; import type {RuntimeConfig} from '../../src/providers/RuntimeProvider.js'; -import YAML from 'yaml'; describe('generateProjectCompose', () => { const config: RuntimeConfig = { diff --git a/tests/lib/config.test.ts b/tests/lib/config.test.ts index 7840d68..ec6d209 100644 --- a/tests/lib/config.test.ts +++ b/tests/lib/config.test.ts @@ -1,15 +1,15 @@ -import {describe, it, expect, beforeEach, afterEach} from 'vitest'; import fs from 'node:fs'; -import path from 'node:path'; import os from 'node:os'; +import path from 'node:path'; +import {afterEach, beforeEach, describe, expect, it} from 'vitest'; import { - readProjectConfig, - writeProjectConfig, + projectConfigExists, readLocalConfig, + readProjectConfig, writeLocalConfig, - projectConfigExists, + writeProjectConfig, } from '../../src/lib/config.js'; -import type {ProjectConfig, LocalConfig} from '../../src/types/config.js'; +import type {LocalConfig, ProjectConfig} from '../../src/types/config.js'; describe('project config', () => { let tmpDir: string; diff --git a/tests/lib/docker.test.ts b/tests/lib/docker.test.ts index 5dee282..bb26746 100644 --- a/tests/lib/docker.test.ts +++ b/tests/lib/docker.test.ts @@ -1,10 +1,10 @@ -import {describe, it, expect, vi, beforeEach} from 'vitest'; +import {execSync} from 'node:child_process'; +import {beforeEach, describe, expect, it, vi} from 'vitest'; import { isDockerInstalled, isDockerRunning, runDockerCompose, } from '../../src/lib/docker.js'; -import {execSync} from 'node:child_process'; vi.mock('node:child_process', () => ({ execSync: vi.fn(), diff --git a/tests/lib/doctor.test.ts b/tests/lib/doctor.test.ts index 772405f..cf99ac2 100644 --- a/tests/lib/doctor.test.ts +++ b/tests/lib/doctor.test.ts @@ -42,9 +42,7 @@ describe('runDoctorChecks', () => { expect(find(checks, 'Docker installed').ok).toBe(true); expect(find(checks, 'Docker running').ok).toBe(true); expect(find(checks, `Traefik port ${TRAEFIK_PORT} available`).ok).toBe(true); - expect(find(checks, `LiveReload port ${LIVERELOAD_PORT} available`).ok).toBe( - true, - ); + expect(find(checks, `LiveReload port ${LIVERELOAD_PORT} available`).ok).toBe(true); }); it('fails the Docker installed check when the CLI is missing', async () => { @@ -79,9 +77,7 @@ describe('runDoctorChecks', () => { mockPort.mockImplementation(async (port: number) => port !== TRAEFIK_PORT); const checks = await runDoctorChecks(); expect(find(checks, `Traefik port ${TRAEFIK_PORT} available`).ok).toBe(false); - expect(find(checks, `LiveReload port ${LIVERELOAD_PORT} available`).ok).toBe( - true, - ); + expect(find(checks, `LiveReload port ${LIVERELOAD_PORT} available`).ok).toBe(true); }); it('always reports the platform check as informational (ok)', async () => { @@ -103,9 +99,7 @@ describe('isWSL', () => { }); it('returns false for a native kernel', () => { - vi.spyOn(fs, 'readFileSync').mockReturnValue( - 'Linux version 6.0.0-generic', - ); + vi.spyOn(fs, 'readFileSync').mockReturnValue('Linux version 6.0.0-generic'); expect(isWSL()).toBe(false); }); diff --git a/tests/lib/hostname.test.ts b/tests/lib/hostname.test.ts index 02ebe36..bdd5380 100644 --- a/tests/lib/hostname.test.ts +++ b/tests/lib/hostname.test.ts @@ -1,6 +1,6 @@ -import {describe, it, expect, vi} from 'vitest'; -import {sanitizeHostname, buildProjectHostname} from '../../src/lib/hostname.js'; import os from 'node:os'; +import {describe, expect, it, vi} from 'vitest'; +import {buildProjectHostname, sanitizeHostname} from '../../src/lib/hostname.js'; vi.mock('node:os', () => ({ default: {hostname: vi.fn()}, diff --git a/tests/lib/livereload.test.ts b/tests/lib/livereload.test.ts index 7486600..7a5d0e7 100644 --- a/tests/lib/livereload.test.ts +++ b/tests/lib/livereload.test.ts @@ -1,4 +1,4 @@ -import {describe, it, expect, beforeEach, afterEach} from 'vitest'; +import {afterEach, beforeEach, describe, expect, it} from 'vitest'; import {LiveReloadServer} from '../../src/lib/livereload.js'; describe('LiveReloadServer', () => { @@ -43,4 +43,4 @@ describe('LiveReloadServer', () => { server1.stop(); }); -}); \ No newline at end of file +}); diff --git a/tests/lib/mu-plugin.test.ts b/tests/lib/mu-plugin.test.ts index 4c5de8f..37c29a5 100644 --- a/tests/lib/mu-plugin.test.ts +++ b/tests/lib/mu-plugin.test.ts @@ -1,7 +1,7 @@ -import {describe, it, expect, beforeEach, afterEach} from 'vitest'; import fs from 'node:fs'; import os from 'node:os'; import path from 'node:path'; +import {afterEach, beforeEach, describe, expect, it} from 'vitest'; import {writeMuPlugin} from '../../src/lib/mu-plugin.js'; describe('writeMuPlugin', () => { diff --git a/tests/lib/nginx-splash.test.ts b/tests/lib/nginx-splash.test.ts index 555a2e7..8f2042f 100644 --- a/tests/lib/nginx-splash.test.ts +++ b/tests/lib/nginx-splash.test.ts @@ -1,7 +1,7 @@ -import {describe, it, expect, beforeEach, afterEach} from 'vitest'; import fs from 'node:fs'; import os from 'node:os'; import path from 'node:path'; +import {afterEach, beforeEach, describe, expect, it} from 'vitest'; import {writeNginxSplashConf} from '../../src/lib/nginx-splash.js'; describe('writeNginxSplashConf', () => { diff --git a/tests/lib/paths.test.ts b/tests/lib/paths.test.ts index 8ae653b..3ea978b 100644 --- a/tests/lib/paths.test.ts +++ b/tests/lib/paths.test.ts @@ -1,5 +1,9 @@ -import {describe, it, expect, vi, beforeEach} from 'vitest'; -import {getKiqrDataDir, getProjectRuntimeDir, getProjectBackupsDir} from '../../src/lib/paths.js'; +import {beforeEach, describe, expect, it, vi} from 'vitest'; +import { + getKiqrDataDir, + getProjectBackupsDir, + getProjectRuntimeDir, +} from '../../src/lib/paths.js'; describe('getKiqrDataDir', () => { beforeEach(() => { @@ -33,7 +37,9 @@ describe('getProjectRuntimeDir', () => { it('returns path under data dir with project id', () => { vi.stubEnv('HOME', '/Users/testuser'); const result = getProjectRuntimeDir('abc-123', 'darwin'); - expect(result).toBe('/Users/testuser/Library/Application Support/Kiqr/projects/abc-123'); + expect(result).toBe( + '/Users/testuser/Library/Application Support/Kiqr/projects/abc-123', + ); }); }); diff --git a/tests/lib/ports.test.ts b/tests/lib/ports.test.ts index 4cf1e83..dcf0c5b 100644 --- a/tests/lib/ports.test.ts +++ b/tests/lib/ports.test.ts @@ -1,4 +1,4 @@ -import {describe, it, expect} from 'vitest'; +import {describe, expect, it} from 'vitest'; import {findAvailablePort, isPortAvailable} from '../../src/lib/ports.js'; describe('isPortAvailable', () => { diff --git a/tests/lib/splash.test.ts b/tests/lib/splash.test.ts index ec5f829..93930b5 100644 --- a/tests/lib/splash.test.ts +++ b/tests/lib/splash.test.ts @@ -1,7 +1,7 @@ -import {describe, it, expect, beforeEach, afterEach} from 'vitest'; import fs from 'node:fs'; import os from 'node:os'; import path from 'node:path'; +import {afterEach, beforeEach, describe, expect, it} from 'vitest'; import {writeSplashPage} from '../../src/lib/splash.js'; describe('writeSplashPage', () => { @@ -50,7 +50,9 @@ describe('writeSplashPage', () => { it('renders the requested hostname client-side', () => { const content = fs.readFileSync(writeSplashPage(tmp), 'utf-8'); expect(content).toContain('id="hostname"'); - expect(content).toContain("document.getElementById('hostname').textContent = location.host"); + expect(content).toContain( + "document.getElementById('hostname').textContent = location.host", + ); }); it('links back to the Kiqr project', () => { diff --git a/tests/lib/theme.test.ts b/tests/lib/theme.test.ts index d533e96..c07f3b5 100644 --- a/tests/lib/theme.test.ts +++ b/tests/lib/theme.test.ts @@ -1,4 +1,4 @@ -import {describe, it, expect} from 'vitest'; +import {describe, expect, it} from 'vitest'; import {parseThemeName, slugify} from '../../src/lib/theme.js'; describe('parseThemeName', () => { diff --git a/tests/lib/traefik.test.ts b/tests/lib/traefik.test.ts index fd96d3b..cfffbc4 100644 --- a/tests/lib/traefik.test.ts +++ b/tests/lib/traefik.test.ts @@ -1,6 +1,6 @@ -import {describe, it, expect} from 'vitest'; -import {generateTraefikCompose} from '../../src/lib/traefik.js'; +import {describe, expect, it} from 'vitest'; import YAML from 'yaml'; +import {generateTraefikCompose} from '../../src/lib/traefik.js'; describe('generateTraefikCompose', () => { it('generates valid compose YAML for Traefik', () => { diff --git a/tests/lib/watch.test.ts b/tests/lib/watch.test.ts index 203ca42..0e075c5 100644 --- a/tests/lib/watch.test.ts +++ b/tests/lib/watch.test.ts @@ -1,8 +1,8 @@ -import {describe, it, expect, vi, beforeEach, afterEach} from 'vitest'; -import {createFileWatcher, getRelativePath} from '../../src/lib/watch.js'; import fs from 'node:fs'; -import path from 'node:path'; import os from 'node:os'; +import path from 'node:path'; +import {afterEach, beforeEach, describe, expect, it, vi} from 'vitest'; +import {createFileWatcher, getRelativePath} from '../../src/lib/watch.js'; describe('watch', () => { let tempDir: string; @@ -27,86 +27,110 @@ describe('watch', () => { }); it('handles nested directories', () => { - const result = getRelativePath('/project/theme', '/project/theme/src/components/Header.php'); + const result = getRelativePath( + '/project/theme', + '/project/theme/src/components/Header.php', + ); expect(result).toBe('src/components/Header.php'); }); }); describe('createFileWatcher', () => { - it('reports file changes', () => new Promise((resolve) => { - const callback = vi.fn(); - const watcher = createFileWatcher(tempDir, {extensions: ['.php']}, callback); - - // Give chokidar time to begin watching before writing. - setTimeout(() => { - const testFile = path.join(tempDir, 'test.php'); - fs.writeFileSync(testFile, ' { - watcher.stop(); - // At least one callback should have been called (add or change) - expect(callback.mock.calls.length).toBeGreaterThan(0); - resolve(); - }, 800); - }), 5000); - - it('detects changes in nested subdirectories', () => new Promise((resolve) => { - const callback = vi.fn(); - const watcher = createFileWatcher(tempDir, {extensions: ['.php']}, callback); - - setTimeout(() => { - const nestedDir = path.join(tempDir, 'src', 'components', 'deep'); - fs.mkdirSync(nestedDir, {recursive: true}); - fs.writeFileSync(path.join(nestedDir, 'Header.php'), ' { - watcher.stop(); - const matched = callback.mock.calls.some( - ([, filePath]) => typeof filePath === 'string' && filePath.endsWith('Header.php'), - ); - expect(matched).toBe(true); - resolve(); - }, 800); - }), 5000); - - it('ignores files without matching extensions', () => new Promise((resolve) => { - const callback = vi.fn(); - const watcher = createFileWatcher(tempDir, {extensions: ['.php']}, callback); - - setTimeout(() => { - const testFile = path.join(tempDir, 'test.txt'); - fs.writeFileSync(testFile, 'test content'); - }, 200); - - setTimeout(() => { - watcher.stop(); - // No callbacks should have been called for .txt files - expect(callback).not.toHaveBeenCalled(); - resolve(); - }, 800); - }), 5000); - - it('ignores node_modules directories', () => new Promise((resolve) => { - const callback = vi.fn(); - const watcher = createFileWatcher(tempDir, {extensions: ['.php']}, callback); - - setTimeout(() => { - // Create a file in node_modules - const nodeModulesDir = path.join(tempDir, 'node_modules'); - fs.mkdirSync(nodeModulesDir, {recursive: true}); - fs.writeFileSync(path.join(nodeModulesDir, 'test.php'), ' { - watcher.stop(); - // No callbacks for node_modules - expect(callback).not.toHaveBeenCalled(); - resolve(); - }, 800); - }), 5000); + it( + 'reports file changes', + () => + new Promise((resolve) => { + const callback = vi.fn(); + const watcher = createFileWatcher(tempDir, {extensions: ['.php']}, callback); + + // Give chokidar time to begin watching before writing. + setTimeout(() => { + const testFile = path.join(tempDir, 'test.php'); + fs.writeFileSync(testFile, ' { + watcher.stop(); + // At least one callback should have been called (add or change) + expect(callback.mock.calls.length).toBeGreaterThan(0); + resolve(); + }, 800); + }), + 5000, + ); + + it( + 'detects changes in nested subdirectories', + () => + new Promise((resolve) => { + const callback = vi.fn(); + const watcher = createFileWatcher(tempDir, {extensions: ['.php']}, callback); + + setTimeout(() => { + const nestedDir = path.join(tempDir, 'src', 'components', 'deep'); + fs.mkdirSync(nestedDir, {recursive: true}); + fs.writeFileSync(path.join(nestedDir, 'Header.php'), ' { + watcher.stop(); + const matched = callback.mock.calls.some( + ([, filePath]) => + typeof filePath === 'string' && filePath.endsWith('Header.php'), + ); + expect(matched).toBe(true); + resolve(); + }, 800); + }), + 5000, + ); + + it( + 'ignores files without matching extensions', + () => + new Promise((resolve) => { + const callback = vi.fn(); + const watcher = createFileWatcher(tempDir, {extensions: ['.php']}, callback); + + setTimeout(() => { + const testFile = path.join(tempDir, 'test.txt'); + fs.writeFileSync(testFile, 'test content'); + }, 200); + + setTimeout(() => { + watcher.stop(); + // No callbacks should have been called for .txt files + expect(callback).not.toHaveBeenCalled(); + resolve(); + }, 800); + }), + 5000, + ); + + it( + 'ignores node_modules directories', + () => + new Promise((resolve) => { + const callback = vi.fn(); + const watcher = createFileWatcher(tempDir, {extensions: ['.php']}, callback); + + setTimeout(() => { + // Create a file in node_modules + const nodeModulesDir = path.join(tempDir, 'node_modules'); + fs.mkdirSync(nodeModulesDir, {recursive: true}); + fs.writeFileSync(path.join(nodeModulesDir, 'test.php'), ' { + watcher.stop(); + // No callbacks for node_modules + expect(callback).not.toHaveBeenCalled(); + resolve(); + }, 800); + }), + 5000, + ); it('returns a stop function', () => { const callback = vi.fn(); diff --git a/tests/lib/wpcli.test.ts b/tests/lib/wpcli.test.ts index ec81daa..2816bf7 100644 --- a/tests/lib/wpcli.test.ts +++ b/tests/lib/wpcli.test.ts @@ -1,4 +1,4 @@ -import {describe, it, expect} from 'vitest'; +import {describe, expect, it} from 'vitest'; import {buildWpCliArgs} from '../../src/lib/wpcli.js'; describe('buildWpCliArgs', () => { diff --git a/tests/providers/BitnamiRuntimeProvider.test.ts b/tests/providers/BitnamiRuntimeProvider.test.ts index 0c7910c..8a4be64 100644 --- a/tests/providers/BitnamiRuntimeProvider.test.ts +++ b/tests/providers/BitnamiRuntimeProvider.test.ts @@ -1,4 +1,4 @@ -import {describe, it, expect} from 'vitest'; +import {describe, expect, it} from 'vitest'; import {BitnamiRuntimeProvider} from '../../src/providers/BitnamiRuntimeProvider.js'; describe('BitnamiRuntimeProvider', () => {