Skip to content
Open
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
10 changes: 10 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
# Craft-style database variables
DB_DRIVER="mysql"
DB_SERVER="127.0.0.1"
DB_PORT="3306"
DB_DATABASE="craft"
DB_USER="root"
DB_PASSWORD=""

# Or use a URL instead
# DATABASE_URL="mysql://root:@127.0.0.1:3306/craft"
35 changes: 35 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
# dependencies (bun install)
node_modules

# output
out
dist
*.tgz

# code coverage
coverage
*.lcov

# logs
logs
*.log
report.[0-9]*_[0-9]*_[0-9]*_[0-9]*.json

# dotenv environment variable files
.env
.env.development.local
.env.test.local
.env.production.local
.env.local

# caches
.eslintcache
.cache
*.tsbuildinfo

# IntelliJ based IDEs
.idea

# Finder (MacOS) folder config
.DS_Store
.tug
51 changes: 51 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
# tug

`tug` is a Bun + TypeScript CLI/TUI for common Craft CMS deployment workflows: file sync, database sync, composer file sync, backups, diagnostics, and remote shell access.

## Requirements

- Bun 1.3+
- Zig for OpenTUI development and builds
- macOS-first support in v1
- Runtime tools:
- `ssh`
- `rsync`
- `gzip`
- `mysql` and `mysqldump` for MySQL/MariaDB projects
- `psql` and `pg_dump` for Postgres projects

## Install

```bash
bun install
```

## Run

```bash
bun run src/cli/main.ts
```

## Commands

- `tug`
- `tug init`
- `tug doctor`
- `tug db doctor`
- `tug folder pull`
- `tug folder push`
- `tug db pull`
- `tug db push`
- `tug composer pull`
- `tug composer push`
- `tug shell`
- `tug backups list`
- `tug backups open`

## Development

```bash
bun run lint
bun run typecheck
bun test
```
297 changes: 297 additions & 0 deletions bun.lock

Large diffs are not rendered by default.

8 changes: 8 additions & 0 deletions oxlint.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"ignorePatterns": ["node_modules", ".tug"],
"categories": {
"correctness": "error",
"suspicious": "error",
"style": "warn"
}
}
30 changes: 30 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
{
"name": "tug",
"version": "0.1.0",
"type": "module",
"private": true,
"description": "Craft CMS deployment and sync workflows in a Bun/OpenTUI CLI.",
"bin": {
"tug": "./src/cli/main.ts"
},
"scripts": {
"dev": "bun run src/cli/main.ts",
"typecheck": "tsc --noEmit",
"lint": "oxlint --deny-warnings .",
"test": "bun test",
"doctor": "bun run src/cli/main.ts doctor"
},
"dependencies": {
"@opentui/core": "^0.1.87",
"citty": "^0.2.1",
"dotenv": "^17.3.1",
"es-toolkit": "^1.45.1",
"smol-toml": "^1.6.0",
"zod": "^4.3.6"
},
"devDependencies": {
"@types/bun": "^1.3.10",
"oxlint": "^1.55.0",
"typescript": "^5.9.3"
}
}
30 changes: 30 additions & 0 deletions src/cli/commands/backups.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { defineCommand } from "citty";
import * as backupsTask from "../../tasks/backups";
import { printTaskResult, sharedArgs, withRuntime } from "../common";

function createActionCommand(action: "list" | "open") {
return defineCommand({
meta: {
name: action,
description: action === "list" ? "List backups." : "Open backups in Finder.",
},
args: sharedArgs,
async run(context) {
await withRuntime(context.args, async (runtime) => {
const result = await backupsTask.run(runtime, action);
printTaskResult(result);
});
},
});
}

export const backupsCommand = defineCommand({
meta: {
name: "backups",
description: "Inspect tug backup snapshots.",
},
subCommands: {
list: createActionCommand("list"),
open: createActionCommand("open"),
},
});
30 changes: 30 additions & 0 deletions src/cli/commands/composer.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { defineCommand } from "citty";
import * as composerTask from "../../tasks/composer";
import { printTaskResult, sharedArgs, withRuntime } from "../common";

function createActionCommand(action: "pull" | "push") {
return defineCommand({
meta: {
name: action,
description: `${action === "pull" ? "Pull" : "Push"} composer files.`,
},
args: sharedArgs,
async run(context) {
await withRuntime(context.args, async (runtime) => {
const result = await composerTask.run(runtime, action);
printTaskResult(result);
});
},
});
}

export const composerCommand = defineCommand({
meta: {
name: "composer",
description: "Run composer sync tasks.",
},
subCommands: {
pull: createActionCommand("pull"),
push: createActionCommand("push"),
},
});
50 changes: 50 additions & 0 deletions src/cli/commands/db.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import { defineCommand } from "citty";
import * as databaseTask from "../../tasks/database";
import { printTaskResult, sharedArgs, withRuntime } from "../common";

function createActionCommand(action: "pull" | "push") {
return defineCommand({
meta: {
name: action,
description: `${action === "pull" ? "Pull" : "Push"} the configured database.`,
},
args: sharedArgs,
async run(context) {
await withRuntime(context.args, async (runtime) => {
const result = await databaseTask.run(runtime, action);
printTaskResult(result);
});
},
});
}

export const dbCommand = defineCommand({
meta: {
name: "db",
description: "Run database sync tasks.",
},
subCommands: {
doctor: defineCommand({
meta: {
name: "doctor",
description: "Check local and remote database prerequisites.",
},
args: sharedArgs,
async run(context) {
await withRuntime(context.args, async (runtime) => {
const report = await databaseTask.doctor(runtime);
if (runtime.options.json) {
console.log(JSON.stringify(report, null, 2));
} else {
console.log(databaseTask.formatDatabaseDoctorReport(report));
}
if (!report.ok) {
process.exitCode = 1;
}
});
},
}),
pull: createActionCommand("pull"),
push: createActionCommand("push"),
},
});
37 changes: 37 additions & 0 deletions src/cli/commands/doctor.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { defineCommand } from "citty";
import { inspectProject } from "../../core/doctor";
import { BunProcessRunner } from "../../core/process";
import { optionsFromArgs, sharedArgs } from "../common";

export const doctorCommand = defineCommand({
meta: {
name: "doctor",
description: "Validate tug config, tools, and SSH reachability.",
},
args: sharedArgs,
async run(context) {
const options = optionsFromArgs(context.args);
const runner = new BunProcessRunner();
const report = await inspectProject(
options,
runner,
options.json
? undefined
: (check) => {
const badge =
check.status === "pass" ? "PASS" : check.status === "warn" ? "WARN" : "FAIL";
console.log(`[${badge}] ${check.label}: ${check.message}`);
},
);
if (options.json) {
console.log(JSON.stringify(report, null, 2));
if (!report.ok) {
process.exitCode = 1;
}
return;
}
if (!report.ok) {
process.exitCode = 1;
}
},
});
30 changes: 30 additions & 0 deletions src/cli/commands/folder.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { defineCommand } from "citty";
import * as folderTask from "../../tasks/folder";
import { printTaskResult, sharedArgs, withRuntime } from "../common";

function createActionCommand(action: "pull" | "push") {
return defineCommand({
meta: {
name: action,
description: `${action === "pull" ? "Pull" : "Push"} configured folders.`,
},
args: sharedArgs,
async run(context) {
await withRuntime(context.args, async (runtime) => {
const result = await folderTask.run(runtime, action);
printTaskResult(result);
});
},
});
}

export const folderCommand = defineCommand({
meta: {
name: "folder",
description: "Run folder sync tasks.",
},
subCommands: {
pull: createActionCommand("pull"),
push: createActionCommand("push"),
},
});
20 changes: 20 additions & 0 deletions src/cli/commands/init.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { defineCommand } from "citty";
import { writeInitialConfig } from "../../core/config";
import { optionsFromArgs, sharedArgs } from "../common";

export const initCommand = defineCommand({
meta: {
name: "init",
description: "Create a starter tug.toml file.",
},
args: sharedArgs,
async run(context) {
const options = optionsFromArgs(context.args);
const filePath = await writeInitialConfig(
options.cwd,
options.configPath,
options.force,
);
console.log(`Created ${filePath}`);
Comment thread
coderabbitai[bot] marked this conversation as resolved.
},
});
17 changes: 17 additions & 0 deletions src/cli/commands/shell.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { defineCommand } from "citty";
import * as shellTask from "../../tasks/shell";
import { printTaskResult, sharedArgs, withRuntime } from "../common";

export const shellCommand = defineCommand({
meta: {
name: "shell",
description: "Open a remote shell session.",
},
args: sharedArgs,
async run(context) {
await withRuntime(context.args, async (runtime) => {
const result = await shellTask.run(runtime);
printTaskResult(result);
});
},
});
Loading