@gwatch/gwatch-cli 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +64 -0
- package/dist/commands/create.js +61 -0
- package/dist/commands/enter.js +35 -0
- package/dist/commands/ls.js +26 -0
- package/dist/commands/stop.js +44 -0
- package/dist/db.js +80 -0
- package/dist/docker.js +132 -0
- package/dist/index.js +53 -0
- package/dist/utils/ui.js +67 -0
- package/docs/logo.jpg +0 -0
- package/gwatch-watchtower-1.0.0.tgz +0 -0
- package/package.json +50 -0
package/README.md
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
# G-Watch CLI
|
|
2
|
+
|
|
3
|
+
G-Watch CLI is a tool that wraps Docker and Tmux to manage persistent development environments.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- **Workspace Management**: Clone remote repos or map local directories.
|
|
8
|
+
- **Environment Automation**: Automatically installs Node.js, Tmux, and `@google/gemini-cli` inside the container.
|
|
9
|
+
- **Session Management**: Wraps Tmux sessions for persistent development.
|
|
10
|
+
- **Easy Access**: Simple commands to create, enter, list, and stop environments.
|
|
11
|
+
|
|
12
|
+
## Installation
|
|
13
|
+
|
|
14
|
+
Install globally via npm:
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
npm install -g @gwatch/gwatch-cli
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
Alternatively, to install from source:
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
git clone https://github.com/G-Watch/Watchtower.git
|
|
24
|
+
cd Watchtower
|
|
25
|
+
npm install -g .
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
## Usage
|
|
29
|
+
|
|
30
|
+
### 1. Create a container from a remote git repo
|
|
31
|
+
```bash
|
|
32
|
+
gwatch-cli create --repo https://github.com/user/project.git --dockerfile ./Dockerfile
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
### 2. Create a container from current local directory
|
|
36
|
+
```bash
|
|
37
|
+
gwatch-cli create --dir . --dockerfile ./Dockerfile
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
### 3. Enter a container/session
|
|
41
|
+
```bash
|
|
42
|
+
# Enter container 0, default session
|
|
43
|
+
gwatch-cli enter 0
|
|
44
|
+
|
|
45
|
+
# Enter container 0, specific session (creates if not exists)
|
|
46
|
+
gwatch-cli enter 0/debug_sass
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
### 4. List status
|
|
50
|
+
```bash
|
|
51
|
+
gwatch-cli ls
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
### 5. Stop commands
|
|
55
|
+
```bash
|
|
56
|
+
# Stops the Docker Container
|
|
57
|
+
gwatch-cli stop 0
|
|
58
|
+
|
|
59
|
+
# Kills only the specific tmux session inside the container
|
|
60
|
+
gwatch-cli stop 0/debug_sass
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
## Internal Database
|
|
64
|
+
Metadata is stored in `~/.watchtower/db.json`.
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.createAction = createAction;
|
|
7
|
+
const path_1 = __importDefault(require("path"));
|
|
8
|
+
const os_1 = __importDefault(require("os"));
|
|
9
|
+
const fs_extra_1 = __importDefault(require("fs-extra"));
|
|
10
|
+
const docker_1 = require("../docker");
|
|
11
|
+
const db_1 = require("../db");
|
|
12
|
+
const ui_1 = require("../utils/ui");
|
|
13
|
+
async function createAction(options) {
|
|
14
|
+
const { repo, dir, dockerfile, name: suffix } = options;
|
|
15
|
+
if (!repo && !dir) {
|
|
16
|
+
console.error('Error: You must provide either --repo or --dir');
|
|
17
|
+
process.exit(1);
|
|
18
|
+
}
|
|
19
|
+
let workspacePath;
|
|
20
|
+
let baseName;
|
|
21
|
+
if (repo) {
|
|
22
|
+
baseName = repo.split('/').pop()?.replace('.git', '') || 'unknown';
|
|
23
|
+
workspacePath = path_1.default.join(os_1.default.tmpdir(), 'watchtower', baseName);
|
|
24
|
+
await fs_extra_1.default.ensureDir(workspacePath);
|
|
25
|
+
await (0, docker_1.cloneRepo)(repo, workspacePath);
|
|
26
|
+
}
|
|
27
|
+
else {
|
|
28
|
+
workspacePath = path_1.default.resolve(dir);
|
|
29
|
+
baseName = path_1.default.basename(workspacePath);
|
|
30
|
+
}
|
|
31
|
+
const userName = os_1.default.userInfo().username;
|
|
32
|
+
const db = await (0, db_1.readDb)();
|
|
33
|
+
let index = 1;
|
|
34
|
+
const generateContainerName = (idx) => {
|
|
35
|
+
const parts = [baseName, userName];
|
|
36
|
+
if (suffix)
|
|
37
|
+
parts.push(suffix);
|
|
38
|
+
parts.push(idx.toString());
|
|
39
|
+
return parts.join('-');
|
|
40
|
+
};
|
|
41
|
+
let containerName = generateContainerName(index);
|
|
42
|
+
while (db.containers.some(c => c.name === containerName)) {
|
|
43
|
+
index++;
|
|
44
|
+
containerName = generateContainerName(index);
|
|
45
|
+
}
|
|
46
|
+
const imageName = await (0, docker_1.buildImage)(dockerfile, baseName, workspacePath);
|
|
47
|
+
const container = await (0, docker_1.createContainer)(imageName, containerName, workspacePath);
|
|
48
|
+
console.log('Setting up environment (Node.js, Tmux, Gemini CLI)...');
|
|
49
|
+
await (0, docker_1.ensureEnvironment)(container.id);
|
|
50
|
+
console.log('Setting up default tmux session...');
|
|
51
|
+
await (0, docker_1.setupDefaultTmux)(container.id);
|
|
52
|
+
await (0, db_1.addContainer)({
|
|
53
|
+
id: container.id,
|
|
54
|
+
name: containerName,
|
|
55
|
+
creator: userName,
|
|
56
|
+
repoUrl: repo,
|
|
57
|
+
localDir: dir ? path_1.default.resolve(dir) : undefined,
|
|
58
|
+
sessions: [{ name: 'default', lastAccess: new Date().toISOString() }],
|
|
59
|
+
});
|
|
60
|
+
(0, ui_1.logSuccess)(`Container ${containerName} created successfully! (Index: ${db.containers.length})`);
|
|
61
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.enterAction = enterAction;
|
|
4
|
+
const db_1 = require("../db");
|
|
5
|
+
const execa_1 = require("execa");
|
|
6
|
+
const ui_1 = require("../utils/ui");
|
|
7
|
+
async function enterAction(target) {
|
|
8
|
+
const [indexStr, sessionName] = target.split('/');
|
|
9
|
+
const index = parseInt(indexStr, 10);
|
|
10
|
+
const db = await (0, db_1.readDb)();
|
|
11
|
+
const container = db.containers[index];
|
|
12
|
+
if (!container) {
|
|
13
|
+
(0, ui_1.logError)(`Container at index ${index} not found.`);
|
|
14
|
+
process.exit(1);
|
|
15
|
+
}
|
|
16
|
+
const finalSessionName = sessionName || 'default';
|
|
17
|
+
// Check if session exists, if not create it
|
|
18
|
+
try {
|
|
19
|
+
await (0, execa_1.execa)('docker', ['exec', container.id, 'tmux', 'has-session', '-t', finalSessionName]);
|
|
20
|
+
}
|
|
21
|
+
catch (e) {
|
|
22
|
+
(0, ui_1.logInfo)(`Session ${finalSessionName} not found. Creating it...`);
|
|
23
|
+
await (0, execa_1.execa)('docker', ['exec', container.id, 'tmux', 'new-session', '-d', '-s', finalSessionName]);
|
|
24
|
+
}
|
|
25
|
+
await (0, db_1.updateContainerSession)(container.name, finalSessionName);
|
|
26
|
+
(0, ui_1.logInfo)(`Entering session ${finalSessionName} in container ${container.name}...`);
|
|
27
|
+
try {
|
|
28
|
+
await (0, execa_1.execa)('docker', ['exec', '-it', container.id, 'tmux', 'attach', '-t', finalSessionName], {
|
|
29
|
+
stdio: 'inherit',
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
catch (e) {
|
|
33
|
+
(0, ui_1.logError)(`Error attaching to tmux session: ${e.message}`);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.lsAction = lsAction;
|
|
4
|
+
const db_1 = require("../db");
|
|
5
|
+
const docker_1 = require("../docker");
|
|
6
|
+
const ui_1 = require("../utils/ui");
|
|
7
|
+
async function lsAction() {
|
|
8
|
+
const db = await (0, db_1.readDb)();
|
|
9
|
+
const runningContainers = await (0, docker_1.getRunningContainers)();
|
|
10
|
+
let output = '| Index/Session | Container Name | Creator | Uptime | Last Access |\n';
|
|
11
|
+
output += '|---------------|----------------------|---------|----------------------|-------------|\n';
|
|
12
|
+
db.containers.forEach((container, idx) => {
|
|
13
|
+
const dockerInfo = runningContainers.find(c => c.Id === container.id || c.Names.some(n => n.includes(container.name)));
|
|
14
|
+
const uptime = dockerInfo ? dockerInfo.Status : 'Stopped';
|
|
15
|
+
container.sessions.forEach(session => {
|
|
16
|
+
const lastAccess = new Date(session.lastAccess);
|
|
17
|
+
const today = new Date();
|
|
18
|
+
let lastAccessStr = lastAccess.toLocaleString();
|
|
19
|
+
if (lastAccess.toDateString() === today.toDateString()) {
|
|
20
|
+
lastAccessStr = `${lastAccess.getHours().toString().padStart(2, '0')}:${lastAccess.getMinutes().toString().padStart(2, '0')} Today`;
|
|
21
|
+
}
|
|
22
|
+
output += `| ${(idx + '/' + session.name).padEnd(13)} | ${container.name.padEnd(20)} | ${container.creator.padEnd(7)} | ${uptime.padEnd(20)} | ${lastAccessStr} |\n`;
|
|
23
|
+
});
|
|
24
|
+
});
|
|
25
|
+
(0, ui_1.renderBox)(output, 'Watchtower Environments');
|
|
26
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.stopAction = stopAction;
|
|
7
|
+
const db_1 = require("../db");
|
|
8
|
+
const execa_1 = require("execa");
|
|
9
|
+
const dockerode_1 = __importDefault(require("dockerode"));
|
|
10
|
+
const ui_1 = require("../utils/ui");
|
|
11
|
+
const docker = new dockerode_1.default();
|
|
12
|
+
async function stopAction(target) {
|
|
13
|
+
const parts = target.split('/');
|
|
14
|
+
const index = parseInt(parts[0], 10);
|
|
15
|
+
const sessionName = parts[1];
|
|
16
|
+
const db = await (0, db_1.readDb)();
|
|
17
|
+
const containerInfo = db.containers[index];
|
|
18
|
+
if (!containerInfo) {
|
|
19
|
+
(0, ui_1.logError)(`Container at index ${index} not found.`);
|
|
20
|
+
process.exit(1);
|
|
21
|
+
}
|
|
22
|
+
if (sessionName) {
|
|
23
|
+
try {
|
|
24
|
+
await (0, execa_1.execa)('docker', ['exec', containerInfo.id, 'tmux', 'kill-session', '-t', sessionName]);
|
|
25
|
+
await (0, db_1.removeSession)(index, sessionName);
|
|
26
|
+
(0, ui_1.logSuccess)(`Tmux session ${sessionName} in container ${containerInfo.name} stopped.`);
|
|
27
|
+
}
|
|
28
|
+
catch (e) {
|
|
29
|
+
(0, ui_1.logError)(`Error killing session ${sessionName}: ${e.message}`);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
else {
|
|
33
|
+
try {
|
|
34
|
+
const container = docker.getContainer(containerInfo.id);
|
|
35
|
+
await container.stop();
|
|
36
|
+
await container.remove();
|
|
37
|
+
await (0, db_1.removeContainer)(containerInfo.name);
|
|
38
|
+
(0, ui_1.logSuccess)(`Container ${containerInfo.name} stopped and removed.`);
|
|
39
|
+
}
|
|
40
|
+
catch (e) {
|
|
41
|
+
(0, ui_1.logError)(`Error stopping container ${containerInfo.name}: ${e.message}`);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}
|
package/dist/db.js
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.initDb = initDb;
|
|
7
|
+
exports.readDb = readDb;
|
|
8
|
+
exports.writeDb = writeDb;
|
|
9
|
+
exports.updateContainerSession = updateContainerSession;
|
|
10
|
+
exports.addContainer = addContainer;
|
|
11
|
+
exports.removeContainer = removeContainer;
|
|
12
|
+
exports.removeSession = removeSession;
|
|
13
|
+
const fs_extra_1 = __importDefault(require("fs-extra"));
|
|
14
|
+
const path_1 = __importDefault(require("path"));
|
|
15
|
+
const os_1 = __importDefault(require("os"));
|
|
16
|
+
const zod_1 = require("zod");
|
|
17
|
+
const DB_DIR = path_1.default.join(os_1.default.homedir(), '.watchtower');
|
|
18
|
+
const DB_FILE = path_1.default.join(DB_DIR, 'db.json');
|
|
19
|
+
const SessionSchema = zod_1.z.object({
|
|
20
|
+
name: zod_1.z.string(),
|
|
21
|
+
lastAccess: zod_1.z.string(),
|
|
22
|
+
});
|
|
23
|
+
const ContainerSchema = zod_1.z.object({
|
|
24
|
+
id: zod_1.z.string(), // Docker container ID
|
|
25
|
+
name: zod_1.z.string(), // Watchtower container name
|
|
26
|
+
creator: zod_1.z.string(),
|
|
27
|
+
repoUrl: zod_1.z.string().optional(),
|
|
28
|
+
localDir: zod_1.z.string().optional(),
|
|
29
|
+
sessions: zod_1.z.array(SessionSchema),
|
|
30
|
+
});
|
|
31
|
+
const DatabaseSchema = zod_1.z.object({
|
|
32
|
+
containers: zod_1.z.array(ContainerSchema),
|
|
33
|
+
});
|
|
34
|
+
async function initDb() {
|
|
35
|
+
await fs_extra_1.default.ensureDir(DB_DIR);
|
|
36
|
+
if (!(await fs_extra_1.default.pathExists(DB_FILE))) {
|
|
37
|
+
await fs_extra_1.default.writeJson(DB_FILE, { containers: [] });
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
async function readDb() {
|
|
41
|
+
await initDb();
|
|
42
|
+
const data = await fs_extra_1.default.readJson(DB_FILE);
|
|
43
|
+
return DatabaseSchema.parse(data);
|
|
44
|
+
}
|
|
45
|
+
async function writeDb(db) {
|
|
46
|
+
await fs_extra_1.default.writeJson(DB_FILE, db, { spaces: 2 });
|
|
47
|
+
}
|
|
48
|
+
async function updateContainerSession(containerName, sessionName) {
|
|
49
|
+
const db = await readDb();
|
|
50
|
+
const container = db.containers.find(c => c.name === containerName);
|
|
51
|
+
if (container) {
|
|
52
|
+
const session = container.sessions.find(s => s.name === sessionName);
|
|
53
|
+
const now = new Date().toISOString();
|
|
54
|
+
if (session) {
|
|
55
|
+
session.lastAccess = now;
|
|
56
|
+
}
|
|
57
|
+
else {
|
|
58
|
+
container.sessions.push({ name: sessionName, lastAccess: now });
|
|
59
|
+
}
|
|
60
|
+
await writeDb(db);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
async function addContainer(container) {
|
|
64
|
+
const db = await readDb();
|
|
65
|
+
db.containers.push(container);
|
|
66
|
+
await writeDb(db);
|
|
67
|
+
}
|
|
68
|
+
async function removeContainer(containerName) {
|
|
69
|
+
const db = await readDb();
|
|
70
|
+
db.containers = db.containers.filter(c => c.name !== containerName);
|
|
71
|
+
await writeDb(db);
|
|
72
|
+
}
|
|
73
|
+
async function removeSession(containerIndex, sessionName) {
|
|
74
|
+
const db = await readDb();
|
|
75
|
+
const container = db.containers[containerIndex];
|
|
76
|
+
if (container) {
|
|
77
|
+
container.sessions = container.sessions.filter(s => s.name !== sessionName);
|
|
78
|
+
await writeDb(db);
|
|
79
|
+
}
|
|
80
|
+
}
|
package/dist/docker.js
ADDED
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.getRunningContainers = getRunningContainers;
|
|
7
|
+
exports.buildImage = buildImage;
|
|
8
|
+
exports.createContainer = createContainer;
|
|
9
|
+
exports.ensureEnvironment = ensureEnvironment;
|
|
10
|
+
exports.setupDefaultTmux = setupDefaultTmux;
|
|
11
|
+
exports.cloneRepo = cloneRepo;
|
|
12
|
+
const dockerode_1 = __importDefault(require("dockerode"));
|
|
13
|
+
const execa_1 = require("execa");
|
|
14
|
+
const path_1 = __importDefault(require("path"));
|
|
15
|
+
const ora_1 = __importDefault(require("ora"));
|
|
16
|
+
const ui_1 = require("./utils/ui");
|
|
17
|
+
const docker = new dockerode_1.default();
|
|
18
|
+
async function getRunningContainers() {
|
|
19
|
+
return await docker.listContainers();
|
|
20
|
+
}
|
|
21
|
+
async function runInBox(title, cmd, args, options = {}) {
|
|
22
|
+
const dynamicBox = new ui_1.DynamicBox(title);
|
|
23
|
+
const subprocess = (0, execa_1.execa)(cmd, args, {
|
|
24
|
+
...options,
|
|
25
|
+
all: true,
|
|
26
|
+
});
|
|
27
|
+
if (subprocess.all) {
|
|
28
|
+
subprocess.all.on('data', (data) => {
|
|
29
|
+
const lines = data.toString().split('\n');
|
|
30
|
+
lines.forEach((line) => dynamicBox.update(line));
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
try {
|
|
34
|
+
const result = await subprocess;
|
|
35
|
+
dynamicBox.stop();
|
|
36
|
+
return result;
|
|
37
|
+
}
|
|
38
|
+
catch (error) {
|
|
39
|
+
dynamicBox.stop();
|
|
40
|
+
throw error;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
async function buildImage(dockerfilePath, repoName, contextDir) {
|
|
44
|
+
const imageName = `watchtower-${repoName.toLowerCase().replace(/[^a-z0-9]/g, '-')}`;
|
|
45
|
+
console.log(`Building image ${imageName}...`);
|
|
46
|
+
try {
|
|
47
|
+
await runInBox(`Building Image: ${imageName}`, 'docker', [
|
|
48
|
+
'build', '-t', imageName, '-f', path_1.default.resolve(dockerfilePath), '.'
|
|
49
|
+
], {
|
|
50
|
+
cwd: contextDir,
|
|
51
|
+
env: { ...process.env, DOCKER_BUILDKIT: '0' }
|
|
52
|
+
});
|
|
53
|
+
(0, ui_1.logSuccess)(`Image ${imageName} built successfully.`);
|
|
54
|
+
return imageName;
|
|
55
|
+
}
|
|
56
|
+
catch (error) {
|
|
57
|
+
(0, ui_1.logError)(`Failed to build image ${imageName}.`);
|
|
58
|
+
(0, ui_1.renderBox)(error.stderr || error.message, 'Docker Build Error', 'red');
|
|
59
|
+
process.exit(1);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
async function createContainer(imageName, containerName, workspacePath) {
|
|
63
|
+
const spinner = (0, ora_1.default)(`Creating container ${containerName}...`).start();
|
|
64
|
+
try {
|
|
65
|
+
const container = await docker.createContainer({
|
|
66
|
+
Image: imageName,
|
|
67
|
+
name: containerName,
|
|
68
|
+
Tty: true,
|
|
69
|
+
OpenStdin: true,
|
|
70
|
+
HostConfig: {
|
|
71
|
+
Binds: [`${path_1.default.resolve(workspacePath)}:/workspace`],
|
|
72
|
+
},
|
|
73
|
+
WorkingDir: '/workspace',
|
|
74
|
+
Cmd: ['/bin/bash'],
|
|
75
|
+
});
|
|
76
|
+
await container.start();
|
|
77
|
+
spinner.succeed(`Container ${containerName} started.`);
|
|
78
|
+
return container;
|
|
79
|
+
}
|
|
80
|
+
catch (error) {
|
|
81
|
+
spinner.fail(`Failed to create or start container ${containerName}.`);
|
|
82
|
+
(0, ui_1.renderBox)(error.json?.message || error.message, 'Docker Container Error', 'red');
|
|
83
|
+
process.exit(1);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
async function ensureEnvironment(containerId) {
|
|
87
|
+
const container = docker.getContainer(containerId);
|
|
88
|
+
console.log('Configuring environment inside container...');
|
|
89
|
+
const setupCommands = [
|
|
90
|
+
{ name: 'Node.js', cmd: 'which node || (apt-get update && apt-get install -y curl && curl -fsSL https://deb.nodesource.com/setup_current.x | bash - && apt-get install -y nodejs)' },
|
|
91
|
+
{ name: 'Tmux', cmd: 'which tmux || (apt-get update && apt-get install -y tmux)' },
|
|
92
|
+
{ name: 'Gemini CLI', cmd: 'which gemini || npm install -g @google/gemini-cli' },
|
|
93
|
+
];
|
|
94
|
+
for (const item of setupCommands) {
|
|
95
|
+
try {
|
|
96
|
+
await runInBox(`Installing ${item.name}`, 'docker', ['exec', containerId, 'bash', '-c', item.cmd]);
|
|
97
|
+
}
|
|
98
|
+
catch (error) {
|
|
99
|
+
(0, ui_1.logError)(`Failed to install ${item.name}.`);
|
|
100
|
+
(0, ui_1.renderBox)(error.message, 'Environment Setup Error', 'red');
|
|
101
|
+
process.exit(1);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
(0, ui_1.logSuccess)('Environment configured successfully.');
|
|
105
|
+
}
|
|
106
|
+
async function setupDefaultTmux(containerId) {
|
|
107
|
+
const spinner = (0, ora_1.default)('Starting default tmux session...').start();
|
|
108
|
+
try {
|
|
109
|
+
const container = docker.getContainer(containerId);
|
|
110
|
+
const exec = await container.exec({
|
|
111
|
+
Cmd: ['tmux', 'new-session', '-d', '-s', 'default'],
|
|
112
|
+
});
|
|
113
|
+
await exec.start({});
|
|
114
|
+
spinner.succeed('Default tmux session started.');
|
|
115
|
+
}
|
|
116
|
+
catch (error) {
|
|
117
|
+
spinner.fail('Failed to start default tmux session.');
|
|
118
|
+
process.exit(1);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
async function cloneRepo(repoUrl, targetDir) {
|
|
122
|
+
console.log(`Cloning repository ${repoUrl}...`);
|
|
123
|
+
try {
|
|
124
|
+
await runInBox(`Cloning Repository`, 'git', ['clone', '--recursive', repoUrl, targetDir]);
|
|
125
|
+
(0, ui_1.logSuccess)('Repository cloned successfully.');
|
|
126
|
+
}
|
|
127
|
+
catch (error) {
|
|
128
|
+
(0, ui_1.logError)(`Failed to clone repository ${repoUrl}.`);
|
|
129
|
+
(0, ui_1.renderBox)(error.stderr || error.message, 'Git Clone Error', 'red');
|
|
130
|
+
process.exit(1);
|
|
131
|
+
}
|
|
132
|
+
}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
4
|
+
const commander_1 = require("commander");
|
|
5
|
+
const create_1 = require("./commands/create");
|
|
6
|
+
const enter_1 = require("./commands/enter");
|
|
7
|
+
const ls_1 = require("./commands/ls");
|
|
8
|
+
const stop_1 = require("./commands/stop");
|
|
9
|
+
const { version } = require('../package.json');
|
|
10
|
+
const program = new commander_1.Command();
|
|
11
|
+
const LOGO = `
|
|
12
|
+
|
|
13
|
+
▄▄▄▄ ▄▄▄ ▄▄▄▄ ▄▄ ▄▄▄▄▄▄▄▄▄
|
|
14
|
+
▀███ ███ ███▀ ██ ██ ▀▀▀███▀▀▀
|
|
15
|
+
███ ███ ███ ▀▀█▄ ▀██▀▀ ▄████ ████▄ ███ ▄███▄ ██ ██ ▄█▀█▄ ████▄
|
|
16
|
+
███▄▄███▄▄███ ▄█▀██ ██ ██ ██ ██ ███ ██ ██ ██ █ ██ ██▄█▀ ██ ▀▀
|
|
17
|
+
▀████▀████▀ ▀█▄██ ██ ▀████ ██ ██ ███ ▀███▀ ██▀██ ▀█▄▄▄ ██
|
|
18
|
+
|
|
19
|
+
`;
|
|
20
|
+
program
|
|
21
|
+
.name('gwatch-cli')
|
|
22
|
+
.description('Persistent development environments wrapper for Docker and Tmux')
|
|
23
|
+
.version(version)
|
|
24
|
+
.addHelpText('before', LOGO);
|
|
25
|
+
program
|
|
26
|
+
.command('create')
|
|
27
|
+
.description('Create a container from a remote git repo or local directory')
|
|
28
|
+
.option('--repo <url>', 'Git repository URL')
|
|
29
|
+
.option('--dir <path>', 'Local directory path')
|
|
30
|
+
.option('--dockerfile <path>', 'Path to Dockerfile', './Dockerfile')
|
|
31
|
+
.option('--name <name>', 'Optional name override')
|
|
32
|
+
.action(create_1.createAction);
|
|
33
|
+
program
|
|
34
|
+
.command('enter <target>')
|
|
35
|
+
.description('Enter a container/session (e.g., 0/default)')
|
|
36
|
+
.action(enter_1.enterAction);
|
|
37
|
+
program
|
|
38
|
+
.command('ls')
|
|
39
|
+
.description('List containers and sessions')
|
|
40
|
+
.action(ls_1.lsAction);
|
|
41
|
+
program
|
|
42
|
+
.command('stop <target>')
|
|
43
|
+
.description('Stop a container or a specific tmux session')
|
|
44
|
+
.action(stop_1.stopAction);
|
|
45
|
+
// Logic to show logo and help if no command is provided
|
|
46
|
+
if (process.argv.length === 2) {
|
|
47
|
+
program.help();
|
|
48
|
+
}
|
|
49
|
+
// Logic to show logo on version
|
|
50
|
+
if (process.argv.includes('-V') || process.argv.includes('--version')) {
|
|
51
|
+
console.log(LOGO);
|
|
52
|
+
}
|
|
53
|
+
program.parse(process.argv);
|
package/dist/utils/ui.js
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.DynamicBox = void 0;
|
|
7
|
+
exports.renderBox = renderBox;
|
|
8
|
+
exports.logInfo = logInfo;
|
|
9
|
+
exports.logSuccess = logSuccess;
|
|
10
|
+
exports.logError = logError;
|
|
11
|
+
const boxen_1 = __importDefault(require("boxen"));
|
|
12
|
+
const chalk_1 = __importDefault(require("chalk"));
|
|
13
|
+
const log_update_1 = __importDefault(require("log-update"));
|
|
14
|
+
function renderBox(content, title, color = 'cyan') {
|
|
15
|
+
console.log((0, boxen_1.default)(content, {
|
|
16
|
+
padding: 1,
|
|
17
|
+
margin: 1,
|
|
18
|
+
borderStyle: 'round',
|
|
19
|
+
borderColor: color,
|
|
20
|
+
title: title ? chalk_1.default.bold(title) : undefined,
|
|
21
|
+
titleAlignment: 'left',
|
|
22
|
+
}));
|
|
23
|
+
}
|
|
24
|
+
function logInfo(message) {
|
|
25
|
+
console.log(chalk_1.default.blue('ℹ ') + message);
|
|
26
|
+
}
|
|
27
|
+
function logSuccess(message) {
|
|
28
|
+
console.log(chalk_1.default.green('✔ ') + message);
|
|
29
|
+
}
|
|
30
|
+
function logError(message) {
|
|
31
|
+
console.log(chalk_1.default.red('▀ ') + message);
|
|
32
|
+
}
|
|
33
|
+
class DynamicBox {
|
|
34
|
+
lines = [];
|
|
35
|
+
maxLines;
|
|
36
|
+
title;
|
|
37
|
+
color;
|
|
38
|
+
constructor(title, maxLines = 10, color = 'cyan') {
|
|
39
|
+
this.title = title;
|
|
40
|
+
this.maxLines = maxLines;
|
|
41
|
+
this.color = color;
|
|
42
|
+
}
|
|
43
|
+
update(line) {
|
|
44
|
+
if (!line.trim())
|
|
45
|
+
return;
|
|
46
|
+
this.lines.push(line.trim());
|
|
47
|
+
if (this.lines.length > this.maxLines) {
|
|
48
|
+
this.lines.shift();
|
|
49
|
+
}
|
|
50
|
+
this.render();
|
|
51
|
+
}
|
|
52
|
+
render() {
|
|
53
|
+
const content = this.lines.join('\n');
|
|
54
|
+
(0, log_update_1.default)((0, boxen_1.default)(content, {
|
|
55
|
+
padding: { left: 1, right: 1, top: 0, bottom: 0 },
|
|
56
|
+
margin: 1,
|
|
57
|
+
borderStyle: 'round',
|
|
58
|
+
borderColor: this.color,
|
|
59
|
+
title: chalk_1.default.bold(this.title),
|
|
60
|
+
titleAlignment: 'left',
|
|
61
|
+
}));
|
|
62
|
+
}
|
|
63
|
+
stop() {
|
|
64
|
+
log_update_1.default.done();
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
exports.DynamicBox = DynamicBox;
|
package/docs/logo.jpg
ADDED
|
Binary file
|
|
Binary file
|
package/package.json
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@gwatch/gwatch-cli",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Watchtower: Persistent development environments wrapper for Docker and Tmux",
|
|
5
|
+
"main": "dist/index.js",
|
|
6
|
+
"bin": {
|
|
7
|
+
"gwatch-cli": "dist/index.js"
|
|
8
|
+
},
|
|
9
|
+
"scripts": {
|
|
10
|
+
"build": "tsc",
|
|
11
|
+
"start": "ts-node src/index.ts",
|
|
12
|
+
"watch": "tsc -w",
|
|
13
|
+
"prepublishOnly": "npm run build"
|
|
14
|
+
},
|
|
15
|
+
"publishConfig": {
|
|
16
|
+
"access": "public"
|
|
17
|
+
},
|
|
18
|
+
"repository": {
|
|
19
|
+
"type": "git",
|
|
20
|
+
"url": "git+https://github.com/G-Watch/Watchtower.git"
|
|
21
|
+
},
|
|
22
|
+
"keywords": [],
|
|
23
|
+
"author": "",
|
|
24
|
+
"license": "ISC",
|
|
25
|
+
"bugs": {
|
|
26
|
+
"url": "https://github.com/G-Watch/Watchtower/issues"
|
|
27
|
+
},
|
|
28
|
+
"homepage": "https://github.com/G-Watch/Watchtower#readme",
|
|
29
|
+
"dependencies": {
|
|
30
|
+
"boxen": "5.1.2",
|
|
31
|
+
"chalk": "4.1.2",
|
|
32
|
+
"commander": "^14.0.3",
|
|
33
|
+
"dockerode": "^4.0.9",
|
|
34
|
+
"execa": "^9.6.1",
|
|
35
|
+
"fs-extra": "^11.3.3",
|
|
36
|
+
"log-update": "4.0.0",
|
|
37
|
+
"ora": "5.4.1",
|
|
38
|
+
"zod": "^4.3.6"
|
|
39
|
+
},
|
|
40
|
+
"devDependencies": {
|
|
41
|
+
"@types/chalk": "^2.2.0",
|
|
42
|
+
"@types/dockerode": "^4.0.1",
|
|
43
|
+
"@types/fs-extra": "^11.0.4",
|
|
44
|
+
"@types/log-update": "^3.0.0",
|
|
45
|
+
"@types/node": "^25.2.2",
|
|
46
|
+
"@types/ora": "^5.2.0",
|
|
47
|
+
"ts-node": "^10.9.2",
|
|
48
|
+
"typescript": "^5.9.3"
|
|
49
|
+
}
|
|
50
|
+
}
|