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', () => {