myrlin-workbook 0.9.24 → 0.9.26
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/logs/server.pid +1 -1
- package/package.json +67 -66
- package/src/web/file-manager.js +149 -0
- package/src/web/git-manager.js +115 -0
- package/src/web/public/app.js +918 -1
- package/src/web/public/index.html +66 -38
- package/src/web/public/styles.css +381 -5
- package/src/web/public/vendor/codemirror.bundle.js +12 -0
- package/src/web/server.js +187 -3
package/logs/server.pid
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
|
|
1
|
+
44496
|
package/package.json
CHANGED
|
@@ -1,66 +1,67 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "myrlin-workbook",
|
|
3
|
-
"version": "0.9.
|
|
4
|
-
"description": "Browser-based project manager for Claude Code sessions - session discovery, multi-terminal, cost tracking, docs, and kanban board",
|
|
5
|
-
"main": "src/index.js",
|
|
6
|
-
"bin": {
|
|
7
|
-
"myrlin-workbook": "./src/gui.js",
|
|
8
|
-
"myrlin": "./src/gui.js",
|
|
9
|
-
"myrlin-tui": "./src/index.js",
|
|
10
|
-
"cwm": "./src/index.js"
|
|
11
|
-
},
|
|
12
|
-
"scripts": {
|
|
13
|
-
"start": "node src/index.js",
|
|
14
|
-
"demo": "node src/demo.js",
|
|
15
|
-
"gui": "node src/supervisor.js",
|
|
16
|
-
"gui:daemon": "node src/supervisor.js --daemon",
|
|
17
|
-
"gui:bare": "node src/gui.js",
|
|
18
|
-
"gui:demo": "node src/supervisor.js --demo",
|
|
19
|
-
"test": "node test/run.js",
|
|
20
|
-
"mcp:visual-qa": "node src/mcp/visual-qa.js",
|
|
21
|
-
"gui:cdp": "node src/supervisor.js --cdp",
|
|
22
|
-
"postinstall": "node scripts/postinstall.js",
|
|
23
|
-
"restart": "bash scripts/restart-gui.sh"
|
|
24
|
-
},
|
|
25
|
-
"repository": {
|
|
26
|
-
"type": "git",
|
|
27
|
-
"url": "https://github.com/therealarthur/myrlin-workbook.git"
|
|
28
|
-
},
|
|
29
|
-
"homepage": "https://github.com/therealarthur/myrlin-workbook",
|
|
30
|
-
"engines": {
|
|
31
|
-
"node": ">=18.0.0"
|
|
32
|
-
},
|
|
33
|
-
"keywords": [
|
|
34
|
-
"claude",
|
|
35
|
-
"workspace",
|
|
36
|
-
"manager",
|
|
37
|
-
"terminal",
|
|
38
|
-
"tui",
|
|
39
|
-
"ai",
|
|
40
|
-
"coding-assistant",
|
|
41
|
-
"session-manager",
|
|
42
|
-
"developer-tools",
|
|
43
|
-
"xterm",
|
|
44
|
-
"myrlin"
|
|
45
|
-
],
|
|
46
|
-
"author": "Arthur",
|
|
47
|
-
"license": "AGPL-3.0-only",
|
|
48
|
-
"dependencies": {
|
|
49
|
-
"blessed": "^0.1.81",
|
|
50
|
-
"blessed-contrib": "^4.11.0",
|
|
51
|
-
"chalk": "^5.6.2",
|
|
52
|
-
"chrome-remote-interface": "^0.34.0",
|
|
53
|
-
"express": "^5.2.1",
|
|
54
|
-
"node-pty": "^1.1.0",
|
|
55
|
-
"qrcode": "^1.5.4",
|
|
56
|
-
"
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
"@
|
|
61
|
-
"@xterm/addon-
|
|
62
|
-
"@xterm/
|
|
63
|
-
"
|
|
64
|
-
"
|
|
65
|
-
|
|
66
|
-
}
|
|
1
|
+
{
|
|
2
|
+
"name": "myrlin-workbook",
|
|
3
|
+
"version": "0.9.26",
|
|
4
|
+
"description": "Browser-based project manager for Claude Code sessions - session discovery, multi-terminal, cost tracking, docs, and kanban board",
|
|
5
|
+
"main": "src/index.js",
|
|
6
|
+
"bin": {
|
|
7
|
+
"myrlin-workbook": "./src/gui.js",
|
|
8
|
+
"myrlin": "./src/gui.js",
|
|
9
|
+
"myrlin-tui": "./src/index.js",
|
|
10
|
+
"cwm": "./src/index.js"
|
|
11
|
+
},
|
|
12
|
+
"scripts": {
|
|
13
|
+
"start": "node src/index.js",
|
|
14
|
+
"demo": "node src/demo.js",
|
|
15
|
+
"gui": "node src/supervisor.js",
|
|
16
|
+
"gui:daemon": "node src/supervisor.js --daemon",
|
|
17
|
+
"gui:bare": "node src/gui.js",
|
|
18
|
+
"gui:demo": "node src/supervisor.js --demo",
|
|
19
|
+
"test": "node test/run.js",
|
|
20
|
+
"mcp:visual-qa": "node src/mcp/visual-qa.js",
|
|
21
|
+
"gui:cdp": "node src/supervisor.js --cdp",
|
|
22
|
+
"postinstall": "node scripts/postinstall.js",
|
|
23
|
+
"restart": "bash scripts/restart-gui.sh"
|
|
24
|
+
},
|
|
25
|
+
"repository": {
|
|
26
|
+
"type": "git",
|
|
27
|
+
"url": "https://github.com/therealarthur/myrlin-workbook.git"
|
|
28
|
+
},
|
|
29
|
+
"homepage": "https://github.com/therealarthur/myrlin-workbook",
|
|
30
|
+
"engines": {
|
|
31
|
+
"node": ">=18.0.0"
|
|
32
|
+
},
|
|
33
|
+
"keywords": [
|
|
34
|
+
"claude",
|
|
35
|
+
"workspace",
|
|
36
|
+
"manager",
|
|
37
|
+
"terminal",
|
|
38
|
+
"tui",
|
|
39
|
+
"ai",
|
|
40
|
+
"coding-assistant",
|
|
41
|
+
"session-manager",
|
|
42
|
+
"developer-tools",
|
|
43
|
+
"xterm",
|
|
44
|
+
"myrlin"
|
|
45
|
+
],
|
|
46
|
+
"author": "Arthur",
|
|
47
|
+
"license": "AGPL-3.0-only",
|
|
48
|
+
"dependencies": {
|
|
49
|
+
"blessed": "^0.1.81",
|
|
50
|
+
"blessed-contrib": "^4.11.0",
|
|
51
|
+
"chalk": "^5.6.2",
|
|
52
|
+
"chrome-remote-interface": "^0.34.0",
|
|
53
|
+
"express": "^5.2.1",
|
|
54
|
+
"node-pty": "^1.1.0",
|
|
55
|
+
"qrcode": "^1.5.4",
|
|
56
|
+
"simple-git": "^3.33.0",
|
|
57
|
+
"ws": "^8.19.0"
|
|
58
|
+
},
|
|
59
|
+
"devDependencies": {
|
|
60
|
+
"@playwright/test": "^1.58.2",
|
|
61
|
+
"@xterm/addon-fit": "^0.11.0",
|
|
62
|
+
"@xterm/addon-web-links": "^0.12.0",
|
|
63
|
+
"@xterm/xterm": "^6.0.0",
|
|
64
|
+
"ffmpeg-static": "^5.3.0",
|
|
65
|
+
"sharp": "^0.34.5"
|
|
66
|
+
}
|
|
67
|
+
}
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Map of file extensions to CodeMirror language names.
|
|
8
|
+
*/
|
|
9
|
+
const EXT_TO_LANG = {
|
|
10
|
+
js: 'javascript', mjs: 'javascript', cjs: 'javascript',
|
|
11
|
+
ts: 'typescript', tsx: 'typescript',
|
|
12
|
+
jsx: 'javascript',
|
|
13
|
+
json: 'json',
|
|
14
|
+
html: 'html', htm: 'html',
|
|
15
|
+
css: 'css', scss: 'css', less: 'css',
|
|
16
|
+
md: 'markdown', markdown: 'markdown',
|
|
17
|
+
py: 'python',
|
|
18
|
+
sh: 'shell', bash: 'shell',
|
|
19
|
+
yaml: 'yaml', yml: 'yaml',
|
|
20
|
+
toml: 'toml',
|
|
21
|
+
rs: 'rust',
|
|
22
|
+
go: 'go',
|
|
23
|
+
java: 'java',
|
|
24
|
+
rb: 'ruby',
|
|
25
|
+
php: 'php',
|
|
26
|
+
sql: 'sql',
|
|
27
|
+
xml: 'xml',
|
|
28
|
+
txt: 'text',
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Directory names to skip when building the file tree.
|
|
33
|
+
* Prevents traversal into version control internals and build artifacts.
|
|
34
|
+
*/
|
|
35
|
+
const SKIP_DIRS = new Set([
|
|
36
|
+
'.git', 'node_modules', '.DS_Store', '__pycache__',
|
|
37
|
+
'.next', 'dist', 'build', '.cache',
|
|
38
|
+
]);
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Validate that the resolved target path stays within root.
|
|
42
|
+
* Throws if path traversal is detected.
|
|
43
|
+
*
|
|
44
|
+
* @param {string} root - Absolute workspace root path
|
|
45
|
+
* @param {string} relPath - Relative path from client
|
|
46
|
+
* @returns {string} Resolved absolute path
|
|
47
|
+
*/
|
|
48
|
+
function validatePath(root, relPath) {
|
|
49
|
+
if (!root || typeof root !== 'string') throw new Error('root is required');
|
|
50
|
+
const resolved = path.resolve(root, relPath || '');
|
|
51
|
+
if (!resolved.startsWith(path.resolve(root) + path.sep) && resolved !== path.resolve(root)) {
|
|
52
|
+
throw new Error('Path traversal detected');
|
|
53
|
+
}
|
|
54
|
+
return resolved;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* GET /api/files/tree
|
|
59
|
+
* Returns directory entries (dirs and files) for a given subpath within the
|
|
60
|
+
* workspace root. Skips .git, node_modules, and other build artifacts.
|
|
61
|
+
*
|
|
62
|
+
* @param {string} workingDir - Workspace root directory
|
|
63
|
+
* @param {string} [subpath=''] - Relative subpath to list
|
|
64
|
+
* @returns {Promise<{path: string, entries: Array}>}
|
|
65
|
+
*/
|
|
66
|
+
async function getTree(workingDir, subpath = '') {
|
|
67
|
+
const root = path.resolve(workingDir);
|
|
68
|
+
const target = validatePath(root, subpath);
|
|
69
|
+
|
|
70
|
+
const entries = await fs.promises.readdir(target, { withFileTypes: true });
|
|
71
|
+
const result = [];
|
|
72
|
+
|
|
73
|
+
for (const entry of entries) {
|
|
74
|
+
if (SKIP_DIRS.has(entry.name)) continue;
|
|
75
|
+
// Skip hidden files/dirs (dot-files) at root level
|
|
76
|
+
if (subpath === '' && entry.name.startsWith('.')) continue;
|
|
77
|
+
const relPath = subpath ? path.join(subpath, entry.name) : entry.name;
|
|
78
|
+
if (entry.isDirectory()) {
|
|
79
|
+
result.push({ name: entry.name, path: relPath, type: 'dir' });
|
|
80
|
+
} else if (entry.isFile()) {
|
|
81
|
+
const ext = path.extname(entry.name).slice(1).toLowerCase();
|
|
82
|
+
result.push({ name: entry.name, path: relPath, type: 'file', ext });
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Sort: directories first, then files, each group sorted alphabetically
|
|
87
|
+
result.sort((a, b) => {
|
|
88
|
+
if (a.type !== b.type) return a.type === 'dir' ? -1 : 1;
|
|
89
|
+
return a.name.localeCompare(b.name);
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
return { path: subpath || '/', entries: result };
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* GET /api/files/content
|
|
97
|
+
* Returns file content as UTF-8 text with a language hint for CodeMirror.
|
|
98
|
+
* Rejects files larger than 5MB.
|
|
99
|
+
*
|
|
100
|
+
* @param {string} workingDir - Workspace root directory
|
|
101
|
+
* @param {string} file - Relative path to the file
|
|
102
|
+
* @returns {Promise<{content: string, language: string, size: number, path: string}>}
|
|
103
|
+
*/
|
|
104
|
+
async function getContent(workingDir, file) {
|
|
105
|
+
if (!file) throw new Error('file is required');
|
|
106
|
+
const root = path.resolve(workingDir);
|
|
107
|
+
const target = validatePath(root, file);
|
|
108
|
+
|
|
109
|
+
const stat = await fs.promises.stat(target);
|
|
110
|
+
if (stat.size > 5 * 1024 * 1024) throw new Error('File too large (> 5MB)');
|
|
111
|
+
|
|
112
|
+
const content = await fs.promises.readFile(target, 'utf8');
|
|
113
|
+
const ext = path.extname(file).slice(1).toLowerCase();
|
|
114
|
+
return {
|
|
115
|
+
content,
|
|
116
|
+
language: EXT_TO_LANG[ext] || 'text',
|
|
117
|
+
size: stat.size,
|
|
118
|
+
path: file,
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* POST /api/files/save
|
|
124
|
+
* Atomically writes file content by writing to a temp file then renaming.
|
|
125
|
+
* Creates parent directories if they don't exist.
|
|
126
|
+
*
|
|
127
|
+
* @param {string} workingDir - Workspace root directory
|
|
128
|
+
* @param {string} file - Relative path to the file
|
|
129
|
+
* @param {string} content - UTF-8 text content to write
|
|
130
|
+
* @returns {Promise<{ok: boolean}>}
|
|
131
|
+
*/
|
|
132
|
+
async function saveContent(workingDir, file, content) {
|
|
133
|
+
if (!file) throw new Error('file is required');
|
|
134
|
+
if (typeof content !== 'string') throw new Error('content must be a string');
|
|
135
|
+
const root = path.resolve(workingDir);
|
|
136
|
+
const target = validatePath(root, file);
|
|
137
|
+
|
|
138
|
+
// Ensure parent directory exists
|
|
139
|
+
await fs.promises.mkdir(path.dirname(target), { recursive: true });
|
|
140
|
+
|
|
141
|
+
// Atomic write: write to temp file then rename into place
|
|
142
|
+
const tmp = target + '.tmp.' + Date.now();
|
|
143
|
+
await fs.promises.writeFile(tmp, content, 'utf8');
|
|
144
|
+
await fs.promises.rename(tmp, target);
|
|
145
|
+
|
|
146
|
+
return { ok: true };
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
module.exports = { getTree, getContent, saveContent };
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Git Manager - wraps simple-git for use in API endpoints.
|
|
5
|
+
*
|
|
6
|
+
* All functions accept a workingDir string and perform git operations
|
|
7
|
+
* scoped to that directory. path.resolve() is used to prevent traversal.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
const { simpleGit } = require('simple-git');
|
|
11
|
+
const path = require('path');
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Resolve and validate the working directory for git operations.
|
|
15
|
+
* @param {string} workingDir - The workspace working directory
|
|
16
|
+
* @returns {string} The resolved absolute path
|
|
17
|
+
*/
|
|
18
|
+
function resolveGitDir(workingDir) {
|
|
19
|
+
if (!workingDir || typeof workingDir !== 'string') {
|
|
20
|
+
throw new Error('workingDir is required');
|
|
21
|
+
}
|
|
22
|
+
return path.resolve(workingDir);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Get the current git status for a working directory.
|
|
27
|
+
* Returns branch name, staged/modified/untracked/deleted files.
|
|
28
|
+
* @param {string} workingDir - Workspace root directory
|
|
29
|
+
* @returns {Promise<object>} Status object with branch and file lists
|
|
30
|
+
*/
|
|
31
|
+
async function getStatus(workingDir) {
|
|
32
|
+
const git = simpleGit(resolveGitDir(workingDir));
|
|
33
|
+
const status = await git.status();
|
|
34
|
+
return {
|
|
35
|
+
branch: status.current,
|
|
36
|
+
staged: status.staged.map(f => ({ file: f, state: 'staged' })),
|
|
37
|
+
modified: status.modified.map(f => ({ file: f, state: 'modified' })),
|
|
38
|
+
notAdded: status.not_added.map(f => ({ file: f, state: 'untracked' })),
|
|
39
|
+
deleted: status.deleted.map(f => ({ file: f, state: 'deleted' })),
|
|
40
|
+
isClean: status.isClean(),
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Get the commit log for a working directory.
|
|
46
|
+
* @param {string} workingDir - Workspace root directory
|
|
47
|
+
* @param {number} [limit=20] - Maximum number of commits to return (capped at 100)
|
|
48
|
+
* @returns {Promise<Array>} Array of commit objects with hash, author, date, message
|
|
49
|
+
*/
|
|
50
|
+
async function getLog(workingDir, limit = 20) {
|
|
51
|
+
const git = simpleGit(resolveGitDir(workingDir));
|
|
52
|
+
try {
|
|
53
|
+
const log = await git.log({ maxCount: Math.min(parseInt(limit, 10) || 20, 100) });
|
|
54
|
+
return (log.all || []).map(c => ({
|
|
55
|
+
hash: c.hash,
|
|
56
|
+
shortHash: c.hash.slice(0, 7),
|
|
57
|
+
author: c.author_name,
|
|
58
|
+
date: c.date,
|
|
59
|
+
message: c.message,
|
|
60
|
+
}));
|
|
61
|
+
} catch (e) {
|
|
62
|
+
// Fresh repo with no commits yet
|
|
63
|
+
if (e.message && e.message.includes('does not have any commits')) return [];
|
|
64
|
+
throw e;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Get the diff for a specific file in the working directory.
|
|
70
|
+
* @param {string} workingDir - Workspace root directory
|
|
71
|
+
* @param {string} file - File path relative to workingDir
|
|
72
|
+
* @param {boolean} [staged=false] - Whether to show staged diff (vs unstaged)
|
|
73
|
+
* @returns {Promise<string>} The unified diff output
|
|
74
|
+
*/
|
|
75
|
+
async function getDiff(workingDir, file, staged = false) {
|
|
76
|
+
const git = simpleGit(resolveGitDir(workingDir));
|
|
77
|
+
let diff;
|
|
78
|
+
if (staged) {
|
|
79
|
+
diff = await git.diff(['--cached', '--', file]);
|
|
80
|
+
} else {
|
|
81
|
+
diff = await git.diff(['--', file]);
|
|
82
|
+
}
|
|
83
|
+
return diff || '';
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Get branch information for a working directory.
|
|
88
|
+
* @param {string} workingDir - Workspace root directory
|
|
89
|
+
* @returns {Promise<{current: string, all: string[]}>} Current branch and all branch names
|
|
90
|
+
*/
|
|
91
|
+
async function getBranches(workingDir) {
|
|
92
|
+
const git = simpleGit(resolveGitDir(workingDir));
|
|
93
|
+
const result = await git.branch(['-a']);
|
|
94
|
+
return {
|
|
95
|
+
current: result.current,
|
|
96
|
+
all: result.all,
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Get the full diff output for a specific commit (git show).
|
|
102
|
+
* @param {string} workingDir - Workspace root directory
|
|
103
|
+
* @param {string} hash - Full or short commit hash
|
|
104
|
+
* @returns {Promise<string>} The unified diff/show output
|
|
105
|
+
*/
|
|
106
|
+
async function getCommitDiff(workingDir, hash) {
|
|
107
|
+
if (!hash || !/^[0-9a-f]{4,40}$/i.test(hash)) {
|
|
108
|
+
throw new Error('Invalid commit hash');
|
|
109
|
+
}
|
|
110
|
+
const git = simpleGit(resolveGitDir(workingDir));
|
|
111
|
+
const output = await git.show([hash, '--stat', '--patch']);
|
|
112
|
+
return output || '';
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
module.exports = { getStatus, getLog, getDiff, getBranches, getCommitDiff };
|