clew-code 0.2.29 → 0.2.30
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/bin/clew.cjs +2 -2
- package/package.json +5 -2
- package/scripts/auto-close-duplicates.ts +277 -0
- package/scripts/backfill-duplicate-comments.ts +213 -0
- package/scripts/codegraph.ts +221 -0
- package/scripts/comment-on-duplicates.sh +95 -0
- package/scripts/edit-issue-labels.sh +84 -0
- package/scripts/final-peer-rename.mjs +46 -0
- package/scripts/fix-encoding.mjs +83 -0
- package/scripts/fix-inconsistencies.mjs +67 -0
- package/scripts/generate-docs.ts +307 -0
- package/scripts/gh.sh +96 -0
- package/scripts/install.ps1 +37 -0
- package/scripts/install.sh +49 -0
- package/scripts/issue-lifecycle.ts +38 -0
- package/scripts/lifecycle-comment.ts +53 -0
- package/scripts/normalize-html.mjs +37 -0
- package/scripts/preload.ts +159 -0
- package/scripts/rename-content-peer-to-swarm.mjs +67 -0
- package/scripts/rename-hooks-mesh.mjs +6 -0
- package/scripts/rename-peer-to-swarm.mjs +62 -0
- package/scripts/run_devcontainer_claude_code.ps1 +152 -0
- package/scripts/scrape.py +82 -0
- package/scripts/session.ts +195 -0
- package/scripts/sweep.ts +168 -0
- package/scripts/write-discovery-test.cjs +93 -0
- package/scripts/write-discovery-test2.cjs +65 -0
- package/scripts/write-server-final.cjs +108 -0
- package/scripts/write-server-test.cjs +69 -0
- package/scripts/write-server-test2.cjs +99 -0
- package/scripts/write-server-test3.cjs +59 -0
- package/scripts/write-server-test4.cjs +108 -0
- package/scripts/write-server-v2.cjs +142 -0
- package/scripts/write-server-v2b.cjs +129 -0
- package/scripts/write-server-v2c.cjs +136 -0
- package/scripts/write-store-test.cjs +12 -0
- package/scripts/write-store-test2.cjs +163 -0
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
/**
|
|
3
|
+
* Session Bridge
|
|
4
|
+
*
|
|
5
|
+
* Saves & restores session context so Claude Code can pick up
|
|
6
|
+
* where it left off across sessions.
|
|
7
|
+
*
|
|
8
|
+
* Usage:
|
|
9
|
+
* bun run session save "กำลัง refactor bridge reconnect logic"
|
|
10
|
+
* bun run session list # ดู sessions ล่าสุด
|
|
11
|
+
* bun run session restore # โหลด session ล่าสุดกลับมา
|
|
12
|
+
* bun run session drop # ลบ current session
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { readFileSync, writeFileSync, mkdirSync, readdirSync, existsSync } from 'fs'
|
|
16
|
+
import { join, dirname, basename } from 'path'
|
|
17
|
+
import { execSync } from 'child_process'
|
|
18
|
+
|
|
19
|
+
const ROOT = join(import.meta.dirname, '..')
|
|
20
|
+
const SESSIONS_DIR = join(ROOT, '.clew', 'sessions')
|
|
21
|
+
const CURRENT_LINK = join(SESSIONS_DIR, 'CURRENT.md')
|
|
22
|
+
const MAX_SESSIONS = 20
|
|
23
|
+
|
|
24
|
+
const cmd = process.argv[2]
|
|
25
|
+
const message = process.argv.slice(3).join(' ')
|
|
26
|
+
|
|
27
|
+
function run(cmd: string): string {
|
|
28
|
+
try {
|
|
29
|
+
return execSync(cmd, { cwd: ROOT, encoding: 'utf-8', timeout: 10000, stdio: ['pipe', 'pipe', 'ignore'] })
|
|
30
|
+
} catch (e: any) {
|
|
31
|
+
return e.stdout || ''
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function ts(): string {
|
|
36
|
+
return new Date().toISOString().replace('T', ' ').slice(0, 19)
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function sessionId(): string {
|
|
40
|
+
return new Date().toISOString().slice(0, 10) + '-' + Date.now().toString(36)
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function getModifiedFiles(): string[] {
|
|
44
|
+
try {
|
|
45
|
+
const out = run('git diff --name-only --relative').trim()
|
|
46
|
+
const staged = run('git diff --cached --name-only --relative').trim()
|
|
47
|
+
const files = [...new Set([...out.split('\n'), ...staged.split('\n')])].filter(Boolean)
|
|
48
|
+
return files.filter(f => f.startsWith('src/'))
|
|
49
|
+
} catch {
|
|
50
|
+
return []
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function getCurrentBranch(): string {
|
|
55
|
+
try {
|
|
56
|
+
return run('git branch --show-current').trim()
|
|
57
|
+
} catch {
|
|
58
|
+
return 'unknown'
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function getRecentCommits(count = 5): string[] {
|
|
63
|
+
try {
|
|
64
|
+
return run(`git log --oneline -${count} -- .`).trim().split('\n').filter(Boolean)
|
|
65
|
+
} catch {
|
|
66
|
+
return []
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// ── Commands ──
|
|
71
|
+
|
|
72
|
+
if (cmd === 'save') {
|
|
73
|
+
const id = sessionId()
|
|
74
|
+
const branch = getCurrentBranch()
|
|
75
|
+
const modified = getModifiedFiles()
|
|
76
|
+
const commits = getRecentCommits(3)
|
|
77
|
+
const content = message || '(no description)'
|
|
78
|
+
|
|
79
|
+
mkdirSync(SESSIONS_DIR, { recursive: true })
|
|
80
|
+
|
|
81
|
+
const md = [
|
|
82
|
+
`# Session: ${id}`,
|
|
83
|
+
``,
|
|
84
|
+
`- **Date**: ${ts()}`,
|
|
85
|
+
`- **Branch**: ${branch}`,
|
|
86
|
+
`- **Description**: ${content}`,
|
|
87
|
+
``,
|
|
88
|
+
]
|
|
89
|
+
|
|
90
|
+
if (modified.length) {
|
|
91
|
+
md.push(`## Modified Files`, ``)
|
|
92
|
+
for (const f of modified) md.push(`- \`${f}\``)
|
|
93
|
+
md.push(``)
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
if (commits.length) {
|
|
97
|
+
md.push(`## Recent Commits`, ``)
|
|
98
|
+
md.push('```')
|
|
99
|
+
for (const c of commits) md.push(c)
|
|
100
|
+
md.push('```')
|
|
101
|
+
md.push(``)
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
md.push(`## Notes`, ``)
|
|
105
|
+
md.push(`(add key decisions, pending items, or anything to remember)`)
|
|
106
|
+
md.push(``)
|
|
107
|
+
|
|
108
|
+
const filepath = join(SESSIONS_DIR, `${id}.md`)
|
|
109
|
+
writeFileSync(filepath, md.join('\n'), 'utf-8')
|
|
110
|
+
writeFileSync(CURRENT_LINK, `# Current Session\n\n${content}\n\nSee: ${id}.md`, 'utf-8')
|
|
111
|
+
|
|
112
|
+
console.log(`✅ Session saved: ${id}`)
|
|
113
|
+
console.log(` ${filepath}`)
|
|
114
|
+
if (modified.length) console.log(` ${modified.length} modified file(s)`)
|
|
115
|
+
|
|
116
|
+
} else if (cmd === 'list') {
|
|
117
|
+
mkdirSync(SESSIONS_DIR, { recursive: true })
|
|
118
|
+
const files = readdirSync(SESSIONS_DIR)
|
|
119
|
+
.filter(f => f.endsWith('.md') && f !== 'CURRENT.md')
|
|
120
|
+
.sort()
|
|
121
|
+
.reverse()
|
|
122
|
+
.slice(0, MAX_SESSIONS)
|
|
123
|
+
|
|
124
|
+
if (files.length === 0) {
|
|
125
|
+
console.log('📭 No saved sessions')
|
|
126
|
+
process.exit(0)
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
console.log(`📋 Recent sessions (${files.length}):`)
|
|
130
|
+
console.log('')
|
|
131
|
+
for (const f of files) {
|
|
132
|
+
const content = readFileSync(join(SESSIONS_DIR, f), 'utf-8')
|
|
133
|
+
const firstLine = content.split('\n')[0] || f
|
|
134
|
+
const desc = content.match(/\*\*Description\*\*:\s*(.+)/)?.[1] || '(no description)'
|
|
135
|
+
const date = content.match(/\*\*Date\*\*:\s*(.+)/)?.[1] || '?'
|
|
136
|
+
console.log(` ${firstLine.replace('# ', '')}`)
|
|
137
|
+
console.log(` ${date} — ${desc}`)
|
|
138
|
+
console.log('')
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
} else if (cmd === 'restore') {
|
|
142
|
+
if (!existsSync(CURRENT_LINK)) {
|
|
143
|
+
console.log('📭 No current session to restore')
|
|
144
|
+
process.exit(0)
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const content = readFileSync(CURRENT_LINK, 'utf-8')
|
|
148
|
+
const refMatch = content.match(/See:\s+(.+\.md)/)
|
|
149
|
+
if (!refMatch) {
|
|
150
|
+
console.log('📭 No session file referenced')
|
|
151
|
+
process.exit(0)
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
const sessionFile = join(SESSIONS_DIR, refMatch[1])
|
|
155
|
+
if (!existsSync(sessionFile)) {
|
|
156
|
+
console.log(`📭 Session file not found: ${sessionFile}`)
|
|
157
|
+
process.exit(0)
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
const session = readFileSync(sessionFile, 'utf-8')
|
|
161
|
+
console.log('📋 Restoring session...')
|
|
162
|
+
console.log('')
|
|
163
|
+
console.log(session)
|
|
164
|
+
|
|
165
|
+
} else if (cmd === 'drop') {
|
|
166
|
+
if (existsSync(CURRENT_LINK)) {
|
|
167
|
+
const content = readFileSync(CURRENT_LINK, 'utf-8')
|
|
168
|
+
const refMatch = content.match(/See:\s+(.+\.md)/)
|
|
169
|
+
if (refMatch) {
|
|
170
|
+
const sessionFile = join(SESSIONS_DIR, refMatch[1])
|
|
171
|
+
if (existsSync(sessionFile)) {
|
|
172
|
+
try { execSync(`rm "${sessionFile}"`) } catch {}
|
|
173
|
+
console.log(`🗑️ Removed: ${refMatch[1]}`)
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
try { execSync(`rm "${CURRENT_LINK}"`) } catch {}
|
|
177
|
+
console.log('✅ Session dropped')
|
|
178
|
+
} else {
|
|
179
|
+
console.log('📭 No current session')
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
} else {
|
|
183
|
+
console.log(`
|
|
184
|
+
Usage:
|
|
185
|
+
bun run session save "<description>" — save current state
|
|
186
|
+
bun run session list — list recent sessions
|
|
187
|
+
bun run session restore — restore latest session
|
|
188
|
+
bun run session drop — drop current session
|
|
189
|
+
|
|
190
|
+
Examples:
|
|
191
|
+
bun run session save "adding auth middleware"
|
|
192
|
+
bun run session save "fixing bridge reconnect bug"
|
|
193
|
+
bun run session list
|
|
194
|
+
`)
|
|
195
|
+
}
|
package/scripts/sweep.ts
ADDED
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
|
|
3
|
+
import { lifecycle, STALE_UPVOTE_THRESHOLD } from "./issue-lifecycle.ts";
|
|
4
|
+
|
|
5
|
+
// --
|
|
6
|
+
|
|
7
|
+
const NEW_ISSUE = "https://github.com/JonusNattapong/ClaudeCode/issues/new/choose";
|
|
8
|
+
const DRY_RUN = process.argv.includes("--dry-run");
|
|
9
|
+
|
|
10
|
+
const CLOSE_MESSAGE = (reason: string) =>
|
|
11
|
+
`Closing for now — ${reason}. Please [open a new issue](${NEW_ISSUE}) if this is still relevant.`;
|
|
12
|
+
|
|
13
|
+
// --
|
|
14
|
+
|
|
15
|
+
async function githubRequest<T>(
|
|
16
|
+
endpoint: string,
|
|
17
|
+
method = "GET",
|
|
18
|
+
body?: unknown
|
|
19
|
+
): Promise<T> {
|
|
20
|
+
const token = process.env.GITHUB_TOKEN;
|
|
21
|
+
if (!token) throw new Error("GITHUB_TOKEN required");
|
|
22
|
+
|
|
23
|
+
const response = await fetch(`https://api.github.com${endpoint}`, {
|
|
24
|
+
method,
|
|
25
|
+
headers: {
|
|
26
|
+
Authorization: `Bearer ${token}`,
|
|
27
|
+
Accept: "application/vnd.github.v3+json",
|
|
28
|
+
"User-Agent": "sweep",
|
|
29
|
+
...(body && { "Content-Type": "application/json" }),
|
|
30
|
+
},
|
|
31
|
+
...(body && { body: JSON.stringify(body) }),
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
if (!response.ok) {
|
|
35
|
+
if (response.status === 404) return {} as T;
|
|
36
|
+
const text = await response.text();
|
|
37
|
+
throw new Error(`GitHub API ${response.status}: ${text}`);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
return response.json();
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// --
|
|
44
|
+
|
|
45
|
+
async function markStale(owner: string, repo: string) {
|
|
46
|
+
const staleDays = lifecycle.find((l) => l.label === "stale")!.days;
|
|
47
|
+
const cutoff = new Date();
|
|
48
|
+
cutoff.setDate(cutoff.getDate() - staleDays);
|
|
49
|
+
|
|
50
|
+
let labeled = 0;
|
|
51
|
+
|
|
52
|
+
console.log(`\n=== marking stale (${staleDays}d inactive) ===`);
|
|
53
|
+
|
|
54
|
+
for (let page = 1; page <= 10; page++) {
|
|
55
|
+
const issues = await githubRequest<any[]>(
|
|
56
|
+
`/repos/${owner}/${repo}/issues?state=open&sort=updated&direction=asc&per_page=100&page=${page}`
|
|
57
|
+
);
|
|
58
|
+
if (issues.length === 0) break;
|
|
59
|
+
|
|
60
|
+
for (const issue of issues) {
|
|
61
|
+
if (issue.pull_request) continue;
|
|
62
|
+
if (issue.locked) continue;
|
|
63
|
+
if (issue.assignees?.length > 0) continue;
|
|
64
|
+
|
|
65
|
+
const updatedAt = new Date(issue.updated_at);
|
|
66
|
+
if (updatedAt > cutoff) return labeled;
|
|
67
|
+
|
|
68
|
+
const alreadyStale = issue.labels?.some(
|
|
69
|
+
(l: any) => l.name === "stale" || l.name === "autoclose"
|
|
70
|
+
);
|
|
71
|
+
if (alreadyStale) continue;
|
|
72
|
+
|
|
73
|
+
const thumbsUp = issue.reactions?.["+1"] ?? 0;
|
|
74
|
+
if (thumbsUp >= STALE_UPVOTE_THRESHOLD) continue;
|
|
75
|
+
|
|
76
|
+
const base = `/repos/${owner}/${repo}/issues/${issue.number}`;
|
|
77
|
+
|
|
78
|
+
if (DRY_RUN) {
|
|
79
|
+
const age = Math.floor((Date.now() - updatedAt.getTime()) / 86400000);
|
|
80
|
+
console.log(`#${issue.number}: would label stale (${age}d inactive) — ${issue.title}`);
|
|
81
|
+
} else {
|
|
82
|
+
await githubRequest(`${base}/labels`, "POST", { labels: ["stale"] });
|
|
83
|
+
console.log(`#${issue.number}: labeled stale — ${issue.title}`);
|
|
84
|
+
}
|
|
85
|
+
labeled++;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
return labeled;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
async function closeExpired(owner: string, repo: string) {
|
|
93
|
+
let closed = 0;
|
|
94
|
+
|
|
95
|
+
for (const { label, days, reason } of lifecycle) {
|
|
96
|
+
const cutoff = new Date();
|
|
97
|
+
cutoff.setDate(cutoff.getDate() - days);
|
|
98
|
+
console.log(`\n=== ${label} (${days}d timeout) ===`);
|
|
99
|
+
|
|
100
|
+
for (let page = 1; page <= 10; page++) {
|
|
101
|
+
const issues = await githubRequest<any[]>(
|
|
102
|
+
`/repos/${owner}/${repo}/issues?state=open&labels=${label}&sort=updated&direction=asc&per_page=100&page=${page}`
|
|
103
|
+
);
|
|
104
|
+
if (issues.length === 0) break;
|
|
105
|
+
|
|
106
|
+
for (const issue of issues) {
|
|
107
|
+
if (issue.pull_request) continue;
|
|
108
|
+
if (issue.locked) continue;
|
|
109
|
+
|
|
110
|
+
const thumbsUp = issue.reactions?.["+1"] ?? 0;
|
|
111
|
+
if (thumbsUp >= STALE_UPVOTE_THRESHOLD) continue;
|
|
112
|
+
|
|
113
|
+
const base = `/repos/${owner}/${repo}/issues/${issue.number}`;
|
|
114
|
+
|
|
115
|
+
const events = await githubRequest<any[]>(`${base}/events?per_page=100`);
|
|
116
|
+
|
|
117
|
+
const labeledAt = events
|
|
118
|
+
.filter((e) => e.event === "labeled" && e.label?.name === label)
|
|
119
|
+
.map((e) => new Date(e.created_at))
|
|
120
|
+
.pop();
|
|
121
|
+
|
|
122
|
+
if (!labeledAt || labeledAt > cutoff) continue;
|
|
123
|
+
|
|
124
|
+
// Skip if a non-bot user commented after the label was applied.
|
|
125
|
+
// The triage workflow should remove lifecycle labels on human
|
|
126
|
+
// activity, but check here too as a safety net.
|
|
127
|
+
const comments = await githubRequest<any[]>(
|
|
128
|
+
`${base}/comments?since=${labeledAt.toISOString()}&per_page=100`
|
|
129
|
+
);
|
|
130
|
+
const hasHumanComment = comments.some(
|
|
131
|
+
(c) => c.user && c.user.type !== "Bot"
|
|
132
|
+
);
|
|
133
|
+
if (hasHumanComment) {
|
|
134
|
+
console.log(
|
|
135
|
+
`#${issue.number}: skipping (human activity after ${label} label)`
|
|
136
|
+
);
|
|
137
|
+
continue;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
if (DRY_RUN) {
|
|
141
|
+
const age = Math.floor((Date.now() - labeledAt.getTime()) / 86400000);
|
|
142
|
+
console.log(`#${issue.number}: would close (${label}, ${age}d old) — ${issue.title}`);
|
|
143
|
+
} else {
|
|
144
|
+
await githubRequest(`${base}/comments`, "POST", { body: CLOSE_MESSAGE(reason) });
|
|
145
|
+
await githubRequest(base, "PATCH", { state: "closed", state_reason: "not_planned" });
|
|
146
|
+
console.log(`#${issue.number}: closed (${label})`);
|
|
147
|
+
}
|
|
148
|
+
closed++;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
return closed;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// --
|
|
157
|
+
|
|
158
|
+
const owner = process.env.GITHUB_REPOSITORY_OWNER;
|
|
159
|
+
const repo = process.env.GITHUB_REPOSITORY_NAME;
|
|
160
|
+
if (!owner || !repo)
|
|
161
|
+
throw new Error("GITHUB_REPOSITORY_OWNER and GITHUB_REPOSITORY_NAME required");
|
|
162
|
+
|
|
163
|
+
if (DRY_RUN) console.log("DRY RUN — no changes will be made\n");
|
|
164
|
+
|
|
165
|
+
const labeled = await markStale(owner, repo);
|
|
166
|
+
const closed = await closeExpired(owner, repo);
|
|
167
|
+
|
|
168
|
+
console.log(`\nDone: ${labeled} ${DRY_RUN ? "would be labeled" : "labeled"} stale, ${closed} ${DRY_RUN ? "would be closed" : "closed"}`);
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const p = 'D:/Projects/Github/clew-code/src/mesh/MeshDiscovery.test.ts';
|
|
3
|
+
const code = `import { describe, expect, test, beforeEach, afterEach } from 'bun:test';
|
|
4
|
+
import { MeshDiscovery, getGlobalDiscovery } from './MeshDiscovery.js';
|
|
5
|
+
|
|
6
|
+
describe('MeshDiscovery', () => {
|
|
7
|
+
let discovery;
|
|
8
|
+
beforeEach(() => { process.env.CLEW_MESH_LAN = undefined; });
|
|
9
|
+
afterEach(() => { if (discovery) discovery.close(); });
|
|
10
|
+
|
|
11
|
+
test('constructor generates localId', () => {
|
|
12
|
+
discovery = new MeshDiscovery();
|
|
13
|
+
expect(discovery.meshId).toBeTruthy();
|
|
14
|
+
expect(discovery.meshId.split('-').length).toBeGreaterThanOrEqual(3);
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
test('constructor accepts callbacks', () => {
|
|
18
|
+
let c = false;
|
|
19
|
+
discovery = new MeshDiscovery({ onPeerDiscovered: () => { c = true; } });
|
|
20
|
+
expect(discovery).toBeDefined();
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
test('setLocalName updates hostname and ID', () => {
|
|
24
|
+
discovery = new MeshDiscovery();
|
|
25
|
+
const oid = discovery.meshId;
|
|
26
|
+
discovery.setLocalName('custom');
|
|
27
|
+
expect(discovery.hostname).toBe('custom');
|
|
28
|
+
expect(discovery.meshId).not.toBe(oid);
|
|
29
|
+
expect(discovery.meshId).toContain('custom');
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
test('startAdvertising sets isSharing true', async () => {
|
|
33
|
+
discovery = new MeshDiscovery();
|
|
34
|
+
expect(discovery.isSharing).toBeFalse();
|
|
35
|
+
await discovery.startAdvertising(9999, '/cwd');
|
|
36
|
+
expect(discovery.isSharing).toBeTrue();
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
test('stopAdvertising sets isSharing false', async () => {
|
|
40
|
+
discovery = new MeshDiscovery();
|
|
41
|
+
await discovery.startAdvertising(9999, '/cwd');
|
|
42
|
+
discovery.stopAdvertising();
|
|
43
|
+
expect(discovery.isSharing).toBeFalse();
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
test('startAdvertising idempotent', async () => {
|
|
47
|
+
discovery = new MeshDiscovery();
|
|
48
|
+
await discovery.startAdvertising(9999, '/cwd');
|
|
49
|
+
await discovery.startAdvertising(8888, '/other');
|
|
50
|
+
expect(discovery.isSharing).toBeTrue();
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
test('stopAdvertising idempotent when not advertising', () => {
|
|
54
|
+
discovery = new MeshDiscovery();
|
|
55
|
+
discovery.stopAdvertising();
|
|
56
|
+
expect(discovery.isSharing).toBeFalse();
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
test('getPeers returns empty initially', () => {
|
|
60
|
+
discovery = new MeshDiscovery();
|
|
61
|
+
expect(discovery.getPeers()).toEqual([]);
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
test('getPeer returns undefined for unknown', () => {
|
|
65
|
+
discovery = new MeshDiscovery();
|
|
66
|
+
expect(discovery.getPeer('nobody')).toBeUndefined();
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
test('discoverPeers returns array', async () => {
|
|
70
|
+
discovery = new MeshDiscovery();
|
|
71
|
+
const peers = await discovery.discoverPeers(100);
|
|
72
|
+
expect(Array.isArray(peers)).toBeTrue();
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
test('discoverMeshs alias', async () => {
|
|
76
|
+
discovery = new MeshDiscovery();
|
|
77
|
+
const peers = await discovery.discoverMeshs(100);
|
|
78
|
+
expect(Array.isArray(peers)).toBeTrue();
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
test('close stops advertising', () => {
|
|
82
|
+
discovery = new MeshDiscovery();
|
|
83
|
+
discovery.close();
|
|
84
|
+
expect(discovery.isSharing).toBeFalse();
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
test('getGlobalDiscovery singleton', () => {
|
|
88
|
+
expect(getGlobalDiscovery()).toBe(getGlobalDiscovery());
|
|
89
|
+
});
|
|
90
|
+
});
|
|
91
|
+
`;
|
|
92
|
+
fs.writeFileSync(p, code, 'utf8');
|
|
93
|
+
console.log('MeshDiscovery.test.ts written');
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const p = 'D:/Projects/Github/clew-code/src/mesh/MeshDiscovery.test.ts';
|
|
3
|
+
const code = `import { describe, expect, test, beforeEach, afterEach } from 'bun:test';
|
|
4
|
+
import { MeshDiscovery, getGlobalDiscovery } from './MeshDiscovery.js';
|
|
5
|
+
|
|
6
|
+
describe('MeshDiscovery', () => {
|
|
7
|
+
let discovery;
|
|
8
|
+
|
|
9
|
+
afterEach(() => {
|
|
10
|
+
if (discovery) discovery.close();
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
test('constructor generates localId', () => {
|
|
14
|
+
discovery = new MeshDiscovery();
|
|
15
|
+
expect(discovery.meshId).toBeTruthy();
|
|
16
|
+
expect(discovery.meshId.split('-').length).toBeGreaterThanOrEqual(3);
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
test('constructor accepts callbacks', () => {
|
|
20
|
+
let c = false;
|
|
21
|
+
discovery = new MeshDiscovery({ onPeerDiscovered: () => { c = true; } });
|
|
22
|
+
expect(discovery).toBeDefined();
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
test('setLocalName updates hostname and ID', () => {
|
|
26
|
+
discovery = new MeshDiscovery();
|
|
27
|
+
const oid = discovery.meshId;
|
|
28
|
+
discovery.setLocalName('custom');
|
|
29
|
+
expect(discovery.hostname).toBe('custom');
|
|
30
|
+
expect(discovery.meshId).not.toBe(oid);
|
|
31
|
+
expect(discovery.meshId).toContain('custom');
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
test('startAdvertising then close', async () => {
|
|
35
|
+
discovery = new MeshDiscovery();
|
|
36
|
+
expect(discovery.isSharing).toBeFalse();
|
|
37
|
+
await discovery.startAdvertising(9999, '/cwd');
|
|
38
|
+
expect(discovery.isSharing).toBeTrue();
|
|
39
|
+
const mid = discovery.meshId;
|
|
40
|
+
expect(mid).toBeTruthy();
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
test('getPeers empty initially', () => {
|
|
44
|
+
discovery = new MeshDiscovery();
|
|
45
|
+
expect(discovery.getPeers()).toEqual([]);
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
test('getPeer undefined for unknown', () => {
|
|
49
|
+
discovery = new MeshDiscovery();
|
|
50
|
+
expect(discovery.getPeer('nobody')).toBeUndefined();
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
test('close stops advertising', () => {
|
|
54
|
+
discovery = new MeshDiscovery();
|
|
55
|
+
discovery.close();
|
|
56
|
+
expect(discovery.isSharing).toBeFalse();
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
test('getGlobalDiscovery singleton', () => {
|
|
60
|
+
expect(getGlobalDiscovery()).toBe(getGlobalDiscovery());
|
|
61
|
+
});
|
|
62
|
+
});
|
|
63
|
+
`;
|
|
64
|
+
fs.writeFileSync(p, code, 'utf8');
|
|
65
|
+
console.log('MeshDiscovery.test.ts rewritten');
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const p = 'D:/Projects/Github/clew-code/src/mesh/MeshServer.test.ts';
|
|
3
|
+
const code = `import { describe, expect, test, beforeEach, afterEach } from 'bun:test';
|
|
4
|
+
import { MeshServer, getGlobalMeshServer } from './MeshServer.js';
|
|
5
|
+
|
|
6
|
+
function mi(o) { return { id:'tp', hostname:'th', ip:'127.0.0.1', port:0, cwd:'/r', version:'t', lastSeen:Date.now(), status:'online', ...o }; }
|
|
7
|
+
|
|
8
|
+
describe('MeshServer', () => {
|
|
9
|
+
let server;
|
|
10
|
+
beforeEach(() => { server = new MeshServer(); });
|
|
11
|
+
afterEach(() => { server.stop(); });
|
|
12
|
+
|
|
13
|
+
test('constructor creates instance', () => {
|
|
14
|
+
expect(server).toBeInstanceOf(MeshServer);
|
|
15
|
+
expect(server.port).toBe(0);
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
test('constructor accepts callbacks', () => {
|
|
19
|
+
const s = new MeshServer({ onMessage: () => {} });
|
|
20
|
+
expect(s).toBeInstanceOf(MeshServer);
|
|
21
|
+
s.stop();
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
test('getGlobalMeshServer singleton', () => {
|
|
25
|
+
expect(getGlobalMeshServer()).toBe(getGlobalMeshServer());
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
test('setCallbacks merges', () => {
|
|
29
|
+
server.setCallbacks({ onMessage: () => {} });
|
|
30
|
+
server.setCallbacks({ onTodo: () => {} });
|
|
31
|
+
expect(server).toBeInstanceOf(MeshServer);
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
test('isBusy/queueDepth initially false/0', () => {
|
|
35
|
+
expect(server.isBusy).toBeFalse();
|
|
36
|
+
expect(server.queueDepth).toBe(0);
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
test('getTodos empty, updateTodoStatus false for unknown', () => {
|
|
40
|
+
expect(server.getTodos()).toEqual([]);
|
|
41
|
+
expect(server.updateTodoStatus('x','done')).toBeFalse();
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
test('start returns port', async () => {
|
|
45
|
+
const port = await server.start(mi());
|
|
46
|
+
expect(typeof port).toBe('number');
|
|
47
|
+
expect(port).toBeGreaterThan(0);
|
|
48
|
+
expect(server.port).toBe(port);
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
test('start idempotent', async () => {
|
|
52
|
+
const p1 = await server.start(mi());
|
|
53
|
+
const p2 = await server.start(mi());
|
|
54
|
+
expect(p1).toBe(p2);
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
test('stop does not throw', () => {
|
|
58
|
+
expect(() => server.stop()).not.toThrow();
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
test('start-stop-start works', async () => {
|
|
62
|
+
await server.start(mi());
|
|
63
|
+
server.stop();
|
|
64
|
+
const p = await server.start(mi());
|
|
65
|
+
expect(p).toBeGreaterThan(0);
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
test('GET /mesh-info', async () => {
|
|
69
|
+
const port = await server.start(mi({ id:'info-test' }));
|
|
70
|
+
const r = await fetch(\`http://127.0.0.1:\${port}/mesh-info\`);
|
|
71
|
+
expect(r.status).toBe(200);
|
|
72
|
+
const d = await r.json();
|
|
73
|
+
expect(d.id).toBe('info-test');
|
|
74
|
+
expect(d.isBusy).toBeFalse();
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
test('POST /mesh-msg fires callback', async () => {
|
|
78
|
+
let m = null;
|
|
79
|
+
server.setCallbacks({ onMessage: msg => { m = msg; } });
|
|
80
|
+
const port = await server.start(mi());
|
|
81
|
+
const r = await fetch(\`http://127.0.0.1:\${port}/mesh-msg\`, {
|
|
82
|
+
method:'POST', body: JSON.stringify({ from:'p2', text:'hello' }),
|
|
83
|
+
});
|
|
84
|
+
expect(r.status).toBe(200);
|
|
85
|
+
expect((await r.json()).ok).toBeTrue();
|
|
86
|
+
expect(m.from).toBe('p2');
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
test('POST /mesh-msg chunk info', async () => {
|
|
90
|
+
let m = null;
|
|
91
|
+
server.setCallbacks({ onMessage: msg => { m = msg; } });
|
|
92
|
+
const port = await server.start(mi());
|
|
93
|
+
await fetch(\`http://127.0.0.1:\${port}/mesh-msg\`, {
|
|
94
|
+
method:'POST', body: JSON.stringify({ from:'p2', chunkGroup:'g1', chunkIndex:0 }),
|
|
95
|
+
});
|
|
96
|
+
expect(m.chunkGroup).toBe('g1');
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
test('POST /mesh-msg defaults', async () => {
|
|
100
|
+
let m = null;
|
|
101
|
+
server.setCallbacks({ onMessage: msg => { m = msg; } });
|
|
102
|
+
const port = await server.start(mi());
|
|
103
|
+
await fetch(\`http://127.0.0.1:\${port}/mesh-msg\`, { method:'POST', body: JSON.stringify({}) });
|
|
104
|
+
expect(m.from).toBe('unknown');
|
|
105
|
+
});
|
|
106
|
+
`;
|
|
107
|
+
fs.writeFileSync(p, code, 'utf8');
|
|
108
|
+
console.log('MeshServer.test.ts p1 written');
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const p = 'D:/Projects/Github/clew-code/src/mesh/MeshServer.test.ts';
|
|
3
|
+
const code = `import { describe, expect, test, beforeEach, afterEach } from 'bun:test';
|
|
4
|
+
import { MeshServer, getGlobalMeshServer } from './MeshServer.js';
|
|
5
|
+
|
|
6
|
+
function mi(o) { return { id:'tp', hostname:'th', ip:'127.0.0.1', port:0, cwd:'/r', version:'t', lastSeen:Date.now(), status:'online', ...o }; }
|
|
7
|
+
|
|
8
|
+
describe('MeshServer', () => {
|
|
9
|
+
let server;
|
|
10
|
+
beforeEach(() => { server = new MeshServer(); });
|
|
11
|
+
afterEach(() => { server.stop(); });
|
|
12
|
+
|
|
13
|
+
test('constructor creates instance', () => {
|
|
14
|
+
expect(server).toBeInstanceOf(MeshServer);
|
|
15
|
+
expect(server.port).toBe(0);
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
test('constructor accepts callbacks', () => {
|
|
19
|
+
const s = new MeshServer({ onMessage: () => {} });
|
|
20
|
+
expect(s).toBeInstanceOf(MeshServer);
|
|
21
|
+
s.stop();
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
test('getGlobalMeshServer singleton', () => {
|
|
25
|
+
expect(getGlobalMeshServer()).toBe(getGlobalMeshServer());
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
test('setCallbacks merges', () => {
|
|
29
|
+
server.setCallbacks({ onMessage: () => {} });
|
|
30
|
+
server.setCallbacks({ onTodo: () => {} });
|
|
31
|
+
expect(server).toBeInstanceOf(MeshServer);
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
test('isBusy/queueDepth initially false/0', () => {
|
|
35
|
+
expect(server.isBusy).toBeFalse();
|
|
36
|
+
expect(server.queueDepth).toBe(0);
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
test('getTodos empty, updateTodoStatus false for unknown', () => {
|
|
40
|
+
expect(server.getTodos()).toEqual([]);
|
|
41
|
+
expect(server.updateTodoStatus('x','done')).toBeFalse();
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
test('start returns port', async () => {
|
|
45
|
+
const port = await server.start(mi());
|
|
46
|
+
expect(typeof port).toBe('number');
|
|
47
|
+
expect(port).toBeGreaterThan(0);
|
|
48
|
+
expect(server.port).toBe(port);
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
test('start idempotent', async () => {
|
|
52
|
+
const p1 = await server.start(mi());
|
|
53
|
+
const p2 = await server.start(mi());
|
|
54
|
+
expect(p1).toBe(p2);
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
test('stop does not throw', () => {
|
|
58
|
+
expect(() => server.stop()).not.toThrow();
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
test('start-stop-start works', async () => {
|
|
62
|
+
await server.start(mi());
|
|
63
|
+
server.stop();
|
|
64
|
+
const p = await server.start(mi());
|
|
65
|
+
expect(p).toBeGreaterThan(0);
|
|
66
|
+
});
|
|
67
|
+
`;
|
|
68
|
+
fs.writeFileSync(p, code, 'utf8');
|
|
69
|
+
console.log('MeshServer.test.ts part 1 written');
|