Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,11 @@ Theme Name: My Theme
| `kiqr open uploads` | Open the uploads folder |
| `kiqr logs` | Show WordPress logs |
| `kiqr destroy` | Remove all site data and start fresh |
| `kiqr agent start` | Start the kiqr agent (shared background service) |
| `kiqr agent stop` | Stop the kiqr agent |
| `kiqr agent restart` | Restart the kiqr agent |
| `kiqr agent status` | Show whether the agent is running |
| `kiqr agent logs` | Stream the agent container logs |

## Configuration

Expand Down Expand Up @@ -108,6 +113,22 @@ Kiqr runs WordPress, MariaDB, and phpMyAdmin in Docker containers, with [Traefik

Each developer on the team gets their own local hostname based on their computer name, so there are no port conflicts when working on the same network.

### The kiqr agent

The reverse proxy and the splash fallback page are managed together as the **kiqr agent** — a single, persistent background service shared by every project on your machine. The agent starts automatically on your first `kiqr up` and keeps running across projects (it uses Docker's `restart: unless-stopped` policy), so switching between themes no longer churns the proxy up and down.

`kiqr down` and `kiqr destroy` only affect the current project; the agent keeps running. Manage it explicitly with:

```bash
kiqr agent status # is it running?
kiqr agent start # start it
kiqr agent stop # stop it (e.g. to free the port)
kiqr agent restart # restart it
kiqr agent logs # stream its logs
```

The agent runs as the `kiqr-agent` Docker Compose project and is where future shared infrastructure (a dashboard, a mail catcher, the collaboration tunnel client) will live.

## License

MIT
34 changes: 34 additions & 0 deletions src/commands/agent/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import {Box, Text} from 'ink';

export const description = 'Manage the kiqr agent (shared background service)';

export default function AgentIndex() {
return (
<Box flexDirection="column" paddingTop={1}>
<Text bold>Kiqr Agent</Text>
<Text dimColor>The shared background service powering every kiqr project</Text>
<Text> </Text>
<Text>Commands:</Text>
<Text>
{' '}
<Text bold>kiqr agent start</Text> Start the agent
</Text>
<Text>
{' '}
<Text bold>kiqr agent stop</Text> Stop the agent
</Text>
<Text>
{' '}
<Text bold>kiqr agent restart</Text> Restart the agent
</Text>
<Text>
{' '}
<Text bold>kiqr agent status</Text> Show whether the agent is running
</Text>
<Text>
{' '}
<Text bold>kiqr agent logs</Text> Stream the agent container logs
</Text>
</Box>
);
}
30 changes: 30 additions & 0 deletions src/commands/agent/logs.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import {execSync} from 'node:child_process';
import {Box, Text, useApp} from 'ink';
import {useEffect} from 'react';
import {writeAgentCompose} from '../../lib/agent.js';

export const description = 'Stream the kiqr agent logs';

export default function AgentLogs() {
const {exit} = useApp();

useEffect(() => {
const composePath = writeAgentCompose();

try {
execSync(`docker compose -f "${composePath}" logs -f`, {
stdio: 'inherit',
});
} catch {
// User pressed Ctrl+C or the agent is not running
}

exit();
}, []);

return (
<Box>
<Text dimColor>Loading agent logs...</Text>
</Box>
);
}
62 changes: 62 additions & 0 deletions src/commands/agent/restart.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import {Box, Text, useApp} from 'ink';
import {useState} from 'react';
import type {Step} from '../../components/StepRunner.js';
import StepRunner from '../../components/StepRunner.js';
import {AGENT_PORT, restartAgent} from '../../lib/agent.js';
import {isDockerInstalled, isDockerRunning} from '../../lib/docker.js';

export const description = 'Restart the kiqr agent';

export default function AgentRestart() {
const {exit} = useApp();
const [complete, setComplete] = useState(false);

const steps: Step[] = [
{
label: 'Checking Docker...',
run: async () => {
if (!isDockerInstalled()) {
throw new Error(
'Docker is not installed. Please install Docker Desktop from https://docker.com',
);
}
if (!isDockerRunning()) {
throw new Error(
'Docker is not running. Please start Docker Desktop and try again.',
);
}
},
},
{
label: 'Restarting kiqr agent...',
run: async () => {
restartAgent();
},
},
];

return (
<Box flexDirection="column">
<StepRunner
steps={steps}
onComplete={() => {
setComplete(true);
setTimeout(() => exit(), 100);
}}
onError={() => setTimeout(() => exit(new Error()), 100)}
/>
{complete && (
<Box flexDirection="column" marginTop={1}>
<Text bold color="green">
The kiqr agent has been restarted.
</Text>
<Text> </Text>
<Text>Proxy:</Text>
<Text bold color="cyan">
http://localhost:{AGENT_PORT}
</Text>
</Box>
)}
</Box>
);
}
66 changes: 66 additions & 0 deletions src/commands/agent/start.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import {Box, Text, useApp} from 'ink';
import {useState} from 'react';
import type {Step} from '../../components/StepRunner.js';
import StepRunner from '../../components/StepRunner.js';
import {AGENT_PORT, ensureAgentRunning} from '../../lib/agent.js';
import {isDockerInstalled, isDockerRunning} from '../../lib/docker.js';

export const description = 'Start the kiqr agent';

export default function AgentStart() {
const {exit} = useApp();
const [complete, setComplete] = useState(false);

const steps: Step[] = [
{
label: 'Checking Docker...',
run: async () => {
if (!isDockerInstalled()) {
throw new Error(
'Docker is not installed. Please install Docker Desktop from https://docker.com',
);
}
if (!isDockerRunning()) {
throw new Error(
'Docker is not running. Please start Docker Desktop and try again.',
);
}
},
},
{
label: 'Starting kiqr agent...',
run: async () => {
ensureAgentRunning();
},
},
];

return (
<Box flexDirection="column">
<StepRunner
steps={steps}
onComplete={() => {
setComplete(true);
setTimeout(() => exit(), 100);
}}
onError={() => setTimeout(() => exit(new Error()), 100)}
/>
{complete && (
<Box flexDirection="column" marginTop={1}>
<Text bold color="green">
The kiqr agent is running.
</Text>
<Text> </Text>
<Text>Proxy:</Text>
<Text bold color="cyan">
http://localhost:{AGENT_PORT}
</Text>
<Text> </Text>
<Text dimColor>
It stays up across projects until you run "kiqr agent stop".
</Text>
</Box>
)}
</Box>
);
}
43 changes: 43 additions & 0 deletions src/commands/agent/status.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import {Box, Text, useApp} from 'ink';
import {getAgentStatus} from '../../lib/agent.js';

export const description = 'Show the kiqr agent status';

export default function AgentStatus() {
const {exit} = useApp();
const status = getAgentStatus();

setTimeout(() => exit(), 50);

return (
<Box flexDirection="column" paddingTop={1}>
<Text bold>Kiqr Agent</Text>
<Text>
{' '}
Status:{' '}
{status.running ? (
<Text bold color="green">
running
</Text>
) : (
<Text bold color="red">
stopped
</Text>
)}
</Text>
<Text>
{' '}
Proxy: <Text color="cyan">http://localhost:{status.port}</Text>
</Text>
<Text> </Text>
<Text bold>Containers</Text>
{status.containers.map((c) => (
<Text key={c.name}>
{' '}
{c.running ? <Text color="green">●</Text> : <Text color="red">○</Text>} {c.name}{' '}
<Text dimColor>({c.running ? 'running' : 'stopped'})</Text>
</Text>
))}
</Box>
);
}
42 changes: 42 additions & 0 deletions src/commands/agent/stop.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import {Box, Text, useApp} from 'ink';
import {useState} from 'react';
import type {Step} from '../../components/StepRunner.js';
import StepRunner from '../../components/StepRunner.js';
import {stopAgent} from '../../lib/agent.js';

export const description = 'Stop the kiqr agent';

export default function AgentStop() {
const {exit} = useApp();
const [complete, setComplete] = useState(false);

const steps: Step[] = [
{
label: 'Stopping kiqr agent...',
run: async () => {
stopAgent();
},
},
];

return (
<Box flexDirection="column">
<StepRunner
steps={steps}
onComplete={() => {
setComplete(true);
setTimeout(() => exit(), 100);
}}
onError={() => setTimeout(() => exit(new Error()), 100)}
/>
{complete && (
<Box flexDirection="column" marginTop={1}>
<Text bold color="green">
The kiqr agent has been stopped.
</Text>
<Text dimColor>It will start again on your next "kiqr up".</Text>
</Box>
)}
</Box>
);
}
7 changes: 0 additions & 7 deletions src/commands/destroy.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ 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 type {ProjectConfig} from '../types/config.js';

export const description = 'Stop and remove all site data (database, uploads, etc.)';
Expand Down Expand Up @@ -108,12 +107,6 @@ export default function Destroy() {
}
},
},
{
label: 'Cleaning up...',
run: async () => {
stopTraefikIfIdle();
},
},
];

return (
Expand Down
7 changes: 0 additions & 7 deletions src/commands/down.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ 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 type {ProjectConfig} from '../types/config.js';

export const description = 'Stop the WordPress development environment';
Expand Down Expand Up @@ -38,12 +37,6 @@ export default function Down() {
runDockerCompose(composePath, 'down');
},
},
{
label: 'Cleaning up...',
run: async () => {
stopTraefikIfIdle();
},
},
];

return (
Expand Down
4 changes: 4 additions & 0 deletions src/commands/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,10 @@ export default function Index() {
{' '}
<Text bold>kiqr db</Text> Backup and restore the database
</Text>
<Text>
{' '}
<Text bold>kiqr agent</Text> Manage the shared background service
</Text>
<Text>
{' '}
<Text bold>kiqr logs</Text> Show WordPress logs
Expand Down
6 changes: 3 additions & 3 deletions src/commands/restart.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ 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 {ensureAgentRunning} from '../lib/agent.js';
import {writeProjectCompose} from '../lib/compose.js';
import {readLocalConfig, readProjectConfig, writeLocalConfig} from '../lib/config.js';
import {
Expand All @@ -20,7 +21,6 @@ import {
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';
Expand Down Expand Up @@ -130,9 +130,9 @@ export default function Restart() {
},
},
{
label: 'Starting reverse proxy...',
label: 'Starting kiqr agent...',
run: async () => {
ensureTraefikRunning();
ensureAgentRunning();
},
},
{
Expand Down
Loading
Loading