@j-ho/agents-office 0.1.2
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 +115 -0
- package/cli/agents-office.mjs +370 -0
- package/package.json +62 -0
package/README.md
ADDED
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
# Agents Office
|
|
2
|
+
|
|
3
|
+
Claude Code가 작업하는 과정을 **사무실 속 에이전트(Researcher/Coder/Reviewer/Manager)**로 시각화하는 Tauri 데스크톱 앱입니다.
|
|
4
|
+
로컬의 Claude 로그(`$HOME/.claude/**`)를 감시(watch)하고, 이벤트를 프론트엔드(PixiJS 캔버스 + Inbox 로그 패널)로 스트리밍합니다.
|
|
5
|
+
|
|
6
|
+

|
|
7
|
+
|
|
8
|
+
## 주요 기능
|
|
9
|
+
- **에이전트 시각화**: 상태(Idle/Working/Thinking/Passing/Error)를 픽셀 아트 스타일로 표시
|
|
10
|
+
- **Inbox 로그**: Claude 로그 라인을 `LogEntry`로 파싱해 최근 항목을 표시(최대 100개)
|
|
11
|
+
- **Watcher 상태 표시**: `Watching/Idle`, 세션 ID 표시(이벤트 기반)
|
|
12
|
+
|
|
13
|
+
## 요구사항
|
|
14
|
+
- **Node.js**: 18 이상 권장
|
|
15
|
+
- **Rust**: stable toolchain
|
|
16
|
+
- **Tauri prerequisites**: OS별 빌드 의존성 설치가 필요합니다. 자세한 내용은 [Tauri prerequisites](https://tauri.app/start/prerequisites/)를 참고하세요.
|
|
17
|
+
|
|
18
|
+
## 실행 방법
|
|
19
|
+
|
|
20
|
+
### 0) npx로 바로 실행 (macOS, 권장)
|
|
21
|
+
|
|
22
|
+
로컬에 Rust/Tauri 툴체인 없이도 아래 명령으로 앱을 실행할 수 있습니다.
|
|
23
|
+
실제 앱 바이너리는 GitHub Releases에서 다운로드되며, 한 번 다운로드되면 캐시에 저장되어 다음 실행은 빨라집니다.
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
npx @j-ho/agents-office
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
- **버전 고정 실행**:
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
npx @j-ho/agents-office --version 0.1.2
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
- **캐시 강제 갱신**:
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
npx @j-ho/agents-office --force
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
#### Gatekeeper 주의사항 (macOS)
|
|
42
|
+
다운로드된 앱이 차단되면 **System Settings → Privacy & Security**에서 “Open Anyway(또는 허용)”를 선택해야 할 수 있습니다.
|
|
43
|
+
|
|
44
|
+
### 1) 의존성 설치
|
|
45
|
+
|
|
46
|
+
```bash
|
|
47
|
+
npm install
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
### 2) 웹(브라우저)로 개발 실행
|
|
51
|
+
|
|
52
|
+
```bash
|
|
53
|
+
npm run dev
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
### 3) 데스크톱(Tauri)로 개발 실행
|
|
57
|
+
|
|
58
|
+
```bash
|
|
59
|
+
npm run tauri:dev
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
## 빌드
|
|
63
|
+
|
|
64
|
+
### 웹 빌드
|
|
65
|
+
|
|
66
|
+
```bash
|
|
67
|
+
npm run build
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
### 데스크톱(Tauri) 빌드
|
|
71
|
+
|
|
72
|
+
```bash
|
|
73
|
+
npm run tauri:build
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
## 권한/보안 (중요)
|
|
77
|
+
이 앱은 Claude 로그를 읽기 위해 Tauri capability로 **로컬 파일 읽기 권한**을 사용합니다.
|
|
78
|
+
|
|
79
|
+
- **읽는 경로**: `$HOME/.claude/**`
|
|
80
|
+
- 주로 `$HOME/.claude/debug`, `$HOME/.claude/projects` 하위를 감시합니다.
|
|
81
|
+
- **읽는 파일 유형**: `.txt`, `.jsonl`, `.json`
|
|
82
|
+
- **동작 방식**: 파일의 “새로 추가된 줄”만 읽어 프론트로 이벤트를 emit 합니다.
|
|
83
|
+
- **주의**: 로그에 민감 정보가 포함될 수 있습니다. 앱은 로컬에서만 처리하지만, 화면 공유/스크린샷에 포함되지 않도록 주의하세요.
|
|
84
|
+
|
|
85
|
+
관련 설정은 [`src-tauri/capabilities/default.json`](./src-tauri/capabilities/default.json)에서 확인할 수 있습니다.
|
|
86
|
+
|
|
87
|
+
## 아키텍처 개요
|
|
88
|
+
|
|
89
|
+
```mermaid
|
|
90
|
+
flowchart LR
|
|
91
|
+
claudeHome[claudeHomeDir] --> debugDir[debugDir]
|
|
92
|
+
claudeHome --> projectsDir[projectsDir]
|
|
93
|
+
watcher[logWatcherRust] -->|"emit(app-event)"| frontend[reactPxiUi]
|
|
94
|
+
frontend --> stores[zustandStores]
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
### 이벤트 흐름(요약)
|
|
98
|
+
- Rust 워처가 파일 변경을 감지하고 로그 라인을 파싱
|
|
99
|
+
- `app-event`로 프론트에 이벤트 전송
|
|
100
|
+
- `LogEntry`: Inbox 로그 추가
|
|
101
|
+
- `AgentUpdate`: 에이전트 상태/업무 표시 갱신
|
|
102
|
+
- `WatcherStatus`: 상단 상태(Watching/Idle) 갱신
|
|
103
|
+
|
|
104
|
+
## 릴리스 자산 규격 (npx 실행용)
|
|
105
|
+
`npx @j-ho/agents-office`는 GitHub Releases(`awesomelon/agents-office`)에서 macOS 빌드 산출물을 다운로드합니다.
|
|
106
|
+
|
|
107
|
+
- **태그 규칙**: `vX.Y.Z` (예: `v0.1.2`)
|
|
108
|
+
- **권장 자산 이름**: `Agents-Office-macos.zip`
|
|
109
|
+
- zip 내부에 `Agents Office.app/` 번들이 포함되어 있어야 합니다.
|
|
110
|
+
- (선택) 무결성 검증:
|
|
111
|
+
- `Agents-Office-macos.zip.sha256` 또는 `checksums.txt`를 함께 업로드하면 CLI가 sha256 검증을 수행합니다.
|
|
112
|
+
|
|
113
|
+
## 라이선스
|
|
114
|
+
MIT
|
|
115
|
+
|
|
@@ -0,0 +1,370 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import fs from "node:fs";
|
|
3
|
+
import os from "node:os";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import crypto from "node:crypto";
|
|
6
|
+
import https from "node:https";
|
|
7
|
+
import { spawnSync } from "node:child_process";
|
|
8
|
+
|
|
9
|
+
const GITHUB_OWNER = "awesomelon";
|
|
10
|
+
const GITHUB_REPO = "agents-office";
|
|
11
|
+
const USER_AGENT = "agents-office-cli";
|
|
12
|
+
|
|
13
|
+
const CACHE_DIR = process.env.AGENTS_OFFICE_CACHE_DIR
|
|
14
|
+
? path.resolve(process.env.AGENTS_OFFICE_CACHE_DIR)
|
|
15
|
+
: path.join(os.homedir(), "Library", "Caches", "agents-office");
|
|
16
|
+
|
|
17
|
+
const ASSET_NAME_CANDIDATES = [
|
|
18
|
+
"Agents-Office-macos.zip",
|
|
19
|
+
"Agents Office-macos.zip",
|
|
20
|
+
"AgentsOffice-macos.zip",
|
|
21
|
+
"Agents-Office-macOS.zip",
|
|
22
|
+
"Agents Office.app.zip",
|
|
23
|
+
];
|
|
24
|
+
|
|
25
|
+
function parseArgs(argv) {
|
|
26
|
+
const args = { version: null, help: false, force: false, quiet: false };
|
|
27
|
+
for (let i = 0; i < argv.length; i++) {
|
|
28
|
+
const a = argv[i];
|
|
29
|
+
if (a === "--help" || a === "-h") args.help = true;
|
|
30
|
+
else if (a === "--quiet" || a === "-q") args.quiet = true;
|
|
31
|
+
else if (a === "--force") args.force = true;
|
|
32
|
+
else if (a === "--version" || a === "-v") args.version = argv[++i] ?? null;
|
|
33
|
+
}
|
|
34
|
+
return args;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function log(msg, { quiet }) {
|
|
38
|
+
if (!quiet) process.stdout.write(`${msg}\n`);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function fail(msg) {
|
|
42
|
+
process.stderr.write(`${msg}\n`);
|
|
43
|
+
process.exit(1);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function usage() {
|
|
47
|
+
return `
|
|
48
|
+
Usage:
|
|
49
|
+
npx @j-ho/agents-office [--version <x.y.z>] [--force] [--quiet]
|
|
50
|
+
|
|
51
|
+
Options:
|
|
52
|
+
--version, -v Use a specific version tag (e.g. 0.1.2 -> v0.1.2)
|
|
53
|
+
--force Re-download even if cached
|
|
54
|
+
--quiet, -q Reduce logs
|
|
55
|
+
--help, -h Show help
|
|
56
|
+
`;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function ensureDir(dirPath) {
|
|
60
|
+
fs.mkdirSync(dirPath, { recursive: true });
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function withLock(lockPath, fn) {
|
|
64
|
+
const started = Date.now();
|
|
65
|
+
const timeoutMs = 60_000;
|
|
66
|
+
while (true) {
|
|
67
|
+
try {
|
|
68
|
+
const fd = fs.openSync(lockPath, "wx");
|
|
69
|
+
try {
|
|
70
|
+
return fn();
|
|
71
|
+
} finally {
|
|
72
|
+
try {
|
|
73
|
+
fs.closeSync(fd);
|
|
74
|
+
} catch {}
|
|
75
|
+
try {
|
|
76
|
+
fs.unlinkSync(lockPath);
|
|
77
|
+
} catch {}
|
|
78
|
+
}
|
|
79
|
+
} catch (err) {
|
|
80
|
+
if (err && err.code !== "EEXIST") throw err;
|
|
81
|
+
if (Date.now() - started > timeoutMs) {
|
|
82
|
+
fail(`Another agents-office process is busy (lock timeout): ${lockPath}`);
|
|
83
|
+
}
|
|
84
|
+
// simple backoff
|
|
85
|
+
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 250);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function httpsGetJson(url) {
|
|
91
|
+
return new Promise((resolve, reject) => {
|
|
92
|
+
const req = https.request(
|
|
93
|
+
url,
|
|
94
|
+
{
|
|
95
|
+
method: "GET",
|
|
96
|
+
headers: {
|
|
97
|
+
"User-Agent": USER_AGENT,
|
|
98
|
+
Accept: "application/vnd.github+json",
|
|
99
|
+
},
|
|
100
|
+
},
|
|
101
|
+
(res) => {
|
|
102
|
+
let body = "";
|
|
103
|
+
res.setEncoding("utf8");
|
|
104
|
+
res.on("data", (chunk) => (body += chunk));
|
|
105
|
+
res.on("end", () => {
|
|
106
|
+
if (res.statusCode && res.statusCode >= 200 && res.statusCode < 300) {
|
|
107
|
+
try {
|
|
108
|
+
resolve(JSON.parse(body));
|
|
109
|
+
} catch (e) {
|
|
110
|
+
reject(new Error(`Failed to parse JSON from ${url}: ${e.message}`));
|
|
111
|
+
}
|
|
112
|
+
} else {
|
|
113
|
+
reject(new Error(`HTTP ${res.statusCode} from ${url}: ${body.slice(0, 200)}`));
|
|
114
|
+
}
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
);
|
|
118
|
+
req.on("error", reject);
|
|
119
|
+
req.end();
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function downloadToFile(url, destPath, { quiet }) {
|
|
124
|
+
return new Promise((resolve, reject) => {
|
|
125
|
+
ensureDir(path.dirname(destPath));
|
|
126
|
+
const tmpPath = `${destPath}.tmp-${process.pid}`;
|
|
127
|
+
const file = fs.createWriteStream(tmpPath);
|
|
128
|
+
|
|
129
|
+
const req = https.request(
|
|
130
|
+
url,
|
|
131
|
+
{
|
|
132
|
+
method: "GET",
|
|
133
|
+
headers: {
|
|
134
|
+
"User-Agent": USER_AGENT,
|
|
135
|
+
Accept: "application/octet-stream",
|
|
136
|
+
},
|
|
137
|
+
},
|
|
138
|
+
(res) => {
|
|
139
|
+
if (res.statusCode && res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) {
|
|
140
|
+
file.close(() => {
|
|
141
|
+
fs.rmSync(tmpPath, { force: true });
|
|
142
|
+
resolve(downloadToFile(res.headers.location, destPath, { quiet }));
|
|
143
|
+
});
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
if (!res.statusCode || res.statusCode < 200 || res.statusCode >= 300) {
|
|
148
|
+
file.close(() => {
|
|
149
|
+
fs.rmSync(tmpPath, { force: true });
|
|
150
|
+
reject(new Error(`Download failed: HTTP ${res.statusCode} ${url}`));
|
|
151
|
+
});
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
res.pipe(file);
|
|
156
|
+
file.on("finish", () => {
|
|
157
|
+
file.close(() => {
|
|
158
|
+
fs.renameSync(tmpPath, destPath);
|
|
159
|
+
log(`Downloaded: ${path.basename(destPath)}`, { quiet });
|
|
160
|
+
resolve();
|
|
161
|
+
});
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
);
|
|
165
|
+
|
|
166
|
+
req.on("error", (err) => {
|
|
167
|
+
try {
|
|
168
|
+
file.close(() => fs.rmSync(tmpPath, { force: true }));
|
|
169
|
+
} catch {}
|
|
170
|
+
reject(err);
|
|
171
|
+
});
|
|
172
|
+
file.on("error", (err) => {
|
|
173
|
+
try {
|
|
174
|
+
fs.rmSync(tmpPath, { force: true });
|
|
175
|
+
} catch {}
|
|
176
|
+
reject(err);
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
req.end();
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function sha256File(filePath) {
|
|
184
|
+
const hash = crypto.createHash("sha256");
|
|
185
|
+
const buf = fs.readFileSync(filePath);
|
|
186
|
+
hash.update(buf);
|
|
187
|
+
return hash.digest("hex");
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function extractZip(zipPath, outDir) {
|
|
191
|
+
ensureDir(outDir);
|
|
192
|
+
// Use macOS built-in ditto for zip extraction.
|
|
193
|
+
const r = spawnSync("ditto", ["-x", "-k", zipPath, outDir], { stdio: "inherit" });
|
|
194
|
+
if (r.status !== 0) {
|
|
195
|
+
throw new Error(`Failed to extract zip (ditto exit ${r.status})`);
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
function findAppBundle(dirPath, maxDepth = 5) {
|
|
200
|
+
if (maxDepth < 0) return null;
|
|
201
|
+
const entries = fs.readdirSync(dirPath, { withFileTypes: true });
|
|
202
|
+
for (const ent of entries) {
|
|
203
|
+
const full = path.join(dirPath, ent.name);
|
|
204
|
+
if (ent.isDirectory() && ent.name.endsWith(".app")) return full;
|
|
205
|
+
}
|
|
206
|
+
for (const ent of entries) {
|
|
207
|
+
const full = path.join(dirPath, ent.name);
|
|
208
|
+
if (ent.isDirectory() && !ent.name.endsWith(".app")) {
|
|
209
|
+
const found = findAppBundle(full, maxDepth - 1);
|
|
210
|
+
if (found) return found;
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
return null;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
function openApp(appPath) {
|
|
217
|
+
const r = spawnSync("open", [appPath], { stdio: "inherit" });
|
|
218
|
+
return r.status ?? 1;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
async function resolveReleaseTag(requestedVersion) {
|
|
222
|
+
if (requestedVersion) {
|
|
223
|
+
const v = requestedVersion.startsWith("v") ? requestedVersion.slice(1) : requestedVersion;
|
|
224
|
+
return `v${v}`;
|
|
225
|
+
}
|
|
226
|
+
const latest = await httpsGetJson(`https://api.github.com/repos/${GITHUB_OWNER}/${GITHUB_REPO}/releases/latest`);
|
|
227
|
+
if (!latest?.tag_name) throw new Error("GitHub latest release has no tag_name");
|
|
228
|
+
return latest.tag_name;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
async function fetchReleaseByTag(tag) {
|
|
232
|
+
return await httpsGetJson(`https://api.github.com/repos/${GITHUB_OWNER}/${GITHUB_REPO}/releases/tags/${encodeURIComponent(tag)}`);
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
function pickAsset(release) {
|
|
236
|
+
const assets = Array.isArray(release.assets) ? release.assets : [];
|
|
237
|
+
for (const name of ASSET_NAME_CANDIDATES) {
|
|
238
|
+
const a = assets.find((x) => x && x.name === name);
|
|
239
|
+
if (a) return a;
|
|
240
|
+
}
|
|
241
|
+
// Fallback: any zip that looks like macos
|
|
242
|
+
const fallback = assets.find((a) => typeof a?.name === "string" && a.name.endsWith(".zip") && /mac/i.test(a.name));
|
|
243
|
+
return fallback ?? null;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
function findChecksumAsset(release, zipAssetName) {
|
|
247
|
+
const assets = Array.isArray(release.assets) ? release.assets : [];
|
|
248
|
+
const direct = assets.find((a) => a?.name === `${zipAssetName}.sha256`);
|
|
249
|
+
if (direct) return { kind: "sha256", asset: direct };
|
|
250
|
+
const checksums = assets.find((a) => a?.name === "checksums.txt");
|
|
251
|
+
if (checksums) return { kind: "checksums.txt", asset: checksums };
|
|
252
|
+
return null;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
async function downloadOptionalChecksum(release, zipAsset, cacheDir, { quiet }) {
|
|
256
|
+
const checksumInfo = findChecksumAsset(release, zipAsset.name);
|
|
257
|
+
if (!checksumInfo) return null;
|
|
258
|
+
|
|
259
|
+
const checksumPath = path.join(cacheDir, checksumInfo.asset.name);
|
|
260
|
+
await downloadToFile(checksumInfo.asset.browser_download_url, checksumPath, { quiet });
|
|
261
|
+
|
|
262
|
+
if (checksumInfo.kind === "sha256") {
|
|
263
|
+
const expected = fs.readFileSync(checksumPath, "utf8").trim().split(/\s+/)[0];
|
|
264
|
+
return { expected, source: checksumInfo.asset.name };
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
// checksums.txt: try to find a line containing the asset name
|
|
268
|
+
const text = fs.readFileSync(checksumPath, "utf8");
|
|
269
|
+
const line = text
|
|
270
|
+
.split("\n")
|
|
271
|
+
.map((l) => l.trim())
|
|
272
|
+
.find((l) => l && l.includes(zipAsset.name));
|
|
273
|
+
if (!line) return null;
|
|
274
|
+
const expected = line.split(/\s+/)[0];
|
|
275
|
+
return { expected, source: checksumInfo.asset.name };
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
async function main() {
|
|
279
|
+
const args = parseArgs(process.argv.slice(2));
|
|
280
|
+
if (args.help) {
|
|
281
|
+
process.stdout.write(usage());
|
|
282
|
+
process.exit(0);
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
ensureDir(CACHE_DIR);
|
|
286
|
+
const lockPath = path.join(CACHE_DIR, "download.lock");
|
|
287
|
+
|
|
288
|
+
await withLock(lockPath, async () => {
|
|
289
|
+
const tag = await resolveReleaseTag(args.version);
|
|
290
|
+
const versionDir = path.join(CACHE_DIR, tag);
|
|
291
|
+
const extractDir = path.join(versionDir, "extract");
|
|
292
|
+
const marker = path.join(versionDir, ".ready");
|
|
293
|
+
|
|
294
|
+
log(`Agents Office CLI (tag: ${tag})`, args);
|
|
295
|
+
|
|
296
|
+
if (!args.force && fs.existsSync(marker)) {
|
|
297
|
+
const appPath = findAppBundle(extractDir);
|
|
298
|
+
if (!appPath) {
|
|
299
|
+
// cache seems broken; force re-download below
|
|
300
|
+
log("Cache marker exists but .app not found; re-downloading.", args);
|
|
301
|
+
} else {
|
|
302
|
+
log(`Using cache: ${appPath}`, args);
|
|
303
|
+
const code = openApp(appPath);
|
|
304
|
+
if (code !== 0) {
|
|
305
|
+
fail(
|
|
306
|
+
`Failed to open app (exit ${code}). If macOS Gatekeeper blocks it, allow it in System Settings -> Privacy & Security.`
|
|
307
|
+
);
|
|
308
|
+
}
|
|
309
|
+
return;
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
// Prepare fresh directory
|
|
314
|
+
fs.rmSync(versionDir, { recursive: true, force: true });
|
|
315
|
+
ensureDir(versionDir);
|
|
316
|
+
ensureDir(extractDir);
|
|
317
|
+
|
|
318
|
+
const release = await fetchReleaseByTag(tag);
|
|
319
|
+
const asset = pickAsset(release);
|
|
320
|
+
if (!asset?.browser_download_url || !asset?.name) {
|
|
321
|
+
fail(
|
|
322
|
+
`No suitable macOS zip asset found in release ${tag}.\nExpected one of: ${ASSET_NAME_CANDIDATES.join(
|
|
323
|
+
", "
|
|
324
|
+
)}\nPlease upload a zip containing 'Agents Office.app/'.`
|
|
325
|
+
);
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
const zipPath = path.join(versionDir, asset.name);
|
|
329
|
+
log(`Downloading release asset: ${asset.name}`, args);
|
|
330
|
+
await downloadToFile(asset.browser_download_url, zipPath, args);
|
|
331
|
+
|
|
332
|
+
const checksum = await downloadOptionalChecksum(release, asset, versionDir, args);
|
|
333
|
+
if (checksum?.expected) {
|
|
334
|
+
const actual = sha256File(zipPath);
|
|
335
|
+
if (actual.toLowerCase() !== checksum.expected.toLowerCase()) {
|
|
336
|
+
fail(
|
|
337
|
+
`Checksum mismatch for ${asset.name}\nExpected(${checksum.source}): ${checksum.expected}\nActual: ${actual}`
|
|
338
|
+
);
|
|
339
|
+
}
|
|
340
|
+
log(`Checksum OK (${checksum.source})`, args);
|
|
341
|
+
} else {
|
|
342
|
+
log("Checksum: skipped (no checksum asset found)", args);
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
log("Extracting...", args);
|
|
346
|
+
extractZip(zipPath, extractDir);
|
|
347
|
+
|
|
348
|
+
const appPath = findAppBundle(extractDir);
|
|
349
|
+
if (!appPath) {
|
|
350
|
+
fail(
|
|
351
|
+
`Extraction completed but .app bundle not found.\nPlease ensure the zip contains 'Agents Office.app/' at any depth.`
|
|
352
|
+
);
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
fs.writeFileSync(marker, new Date().toISOString(), "utf8");
|
|
356
|
+
log(`Launching: ${appPath}`, args);
|
|
357
|
+
const code = openApp(appPath);
|
|
358
|
+
if (code !== 0) {
|
|
359
|
+
fail(
|
|
360
|
+
`Failed to open app (exit ${code}). If macOS Gatekeeper blocks it, allow it in System Settings -> Privacy & Security.`
|
|
361
|
+
);
|
|
362
|
+
}
|
|
363
|
+
});
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
main().catch((err) => {
|
|
367
|
+
const msg = err?.stack || err?.message || String(err);
|
|
368
|
+
fail(`[agents-office] ${msg}`);
|
|
369
|
+
});
|
|
370
|
+
|
package/package.json
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@j-ho/agents-office",
|
|
3
|
+
"version": "0.1.2",
|
|
4
|
+
"description": "Claude Code visualization as agents working in an office",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"author": "j-ho",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "git+https://github.com/awesomelon/agents-office.git"
|
|
10
|
+
},
|
|
11
|
+
"bugs": {
|
|
12
|
+
"url": "https://github.com/awesomelon/agents-office/issues"
|
|
13
|
+
},
|
|
14
|
+
"homepage": "https://github.com/awesomelon/agents-office#readme",
|
|
15
|
+
"keywords": [
|
|
16
|
+
"tauri",
|
|
17
|
+
"vite",
|
|
18
|
+
"react",
|
|
19
|
+
"pixijs",
|
|
20
|
+
"claude",
|
|
21
|
+
"visualization"
|
|
22
|
+
],
|
|
23
|
+
"type": "module",
|
|
24
|
+
"bin": {
|
|
25
|
+
"agents-office": "./cli/agents-office.mjs"
|
|
26
|
+
},
|
|
27
|
+
"engines": {
|
|
28
|
+
"node": ">=18"
|
|
29
|
+
},
|
|
30
|
+
"files": [
|
|
31
|
+
"cli/"
|
|
32
|
+
],
|
|
33
|
+
"scripts": {
|
|
34
|
+
"dev": "vite",
|
|
35
|
+
"build": "tsc && vite build",
|
|
36
|
+
"preview": "vite preview",
|
|
37
|
+
"tauri": "tauri",
|
|
38
|
+
"tauri:dev": "tauri dev",
|
|
39
|
+
"tauri:build": "tauri build"
|
|
40
|
+
},
|
|
41
|
+
"dependencies": {
|
|
42
|
+
"@pixi/react": "^7.1.2",
|
|
43
|
+
"@tauri-apps/api": "^2.1.1",
|
|
44
|
+
"@tauri-apps/plugin-fs": "^2.2.0",
|
|
45
|
+
"@tauri-apps/plugin-shell": "^2.2.0",
|
|
46
|
+
"pixi.js": "^7.4.2",
|
|
47
|
+
"react": "^18.3.1",
|
|
48
|
+
"react-dom": "^18.3.1",
|
|
49
|
+
"zustand": "^5.0.2"
|
|
50
|
+
},
|
|
51
|
+
"devDependencies": {
|
|
52
|
+
"@tauri-apps/cli": "^2.1.0",
|
|
53
|
+
"@types/react": "^18.3.12",
|
|
54
|
+
"@types/react-dom": "^18.3.1",
|
|
55
|
+
"@vitejs/plugin-react": "^4.3.4",
|
|
56
|
+
"autoprefixer": "^10.4.20",
|
|
57
|
+
"postcss": "^8.4.49",
|
|
58
|
+
"tailwindcss": "^3.4.16",
|
|
59
|
+
"typescript": "^5.7.2",
|
|
60
|
+
"vite": "^6.0.3"
|
|
61
|
+
}
|
|
62
|
+
}
|