clew-code 0.2.29 → 0.2.31
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 +20 -4
- package/bin/clew.cjs +2 -2
- package/dist/main.js +2015 -2015
- 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 +40 -0
- package/scripts/install.sh +65 -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,221 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
/**
|
|
3
|
+
* Code Graph Generator
|
|
4
|
+
*
|
|
5
|
+
* Scans src/ and builds a persistent structure map of the codebase.
|
|
6
|
+
* Uses ripgrep (fast, available in the project) for text patterns.
|
|
7
|
+
*
|
|
8
|
+
* Usage: bun run scripts/codegraph.ts
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { readFileSync, writeFileSync, mkdirSync } from 'fs'
|
|
12
|
+
import { join, dirname } from 'path'
|
|
13
|
+
import { execSync } from 'child_process'
|
|
14
|
+
|
|
15
|
+
const ROOT = join(import.meta.dirname, '..')
|
|
16
|
+
const SRC = join(ROOT, 'src')
|
|
17
|
+
const OUT = join(ROOT, '.clew', 'CODEGRAPH.md')
|
|
18
|
+
|
|
19
|
+
function rg(args: string): string {
|
|
20
|
+
try {
|
|
21
|
+
return execSync(`rg ${args}`, { cwd: ROOT, encoding: 'utf-8', timeout: 60000, stdio: ['pipe', 'pipe', 'ignore'] })
|
|
22
|
+
} catch (e: any) {
|
|
23
|
+
return e.stdout || ''
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
interface Module {
|
|
28
|
+
path: string
|
|
29
|
+
lines: number
|
|
30
|
+
exports: string[]
|
|
31
|
+
types: string[]
|
|
32
|
+
classNames: string[]
|
|
33
|
+
imports: string[]
|
|
34
|
+
todos: number
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// ── 1. File list ──
|
|
38
|
+
console.log('📁 Scanning files...')
|
|
39
|
+
const fileLines = rg('--count --sort path --type ts "^" src/').trim().split('\n').filter(Boolean)
|
|
40
|
+
|
|
41
|
+
const files = new Map<string, Module>()
|
|
42
|
+
|
|
43
|
+
for (const entry of fileLines) {
|
|
44
|
+
const colon = entry.lastIndexOf(':')
|
|
45
|
+
const relPath = entry.slice(0, colon).replace(/\\/g, '/').replace(/^src\//, '')
|
|
46
|
+
const lineCount = parseInt(entry.slice(colon + 1)) || 0
|
|
47
|
+
if (!relPath || relPath.startsWith('..')) continue
|
|
48
|
+
|
|
49
|
+
files.set(relPath, {
|
|
50
|
+
path: relPath,
|
|
51
|
+
lines: lineCount,
|
|
52
|
+
exports: [],
|
|
53
|
+
types: [],
|
|
54
|
+
classNames: [],
|
|
55
|
+
imports: [],
|
|
56
|
+
todos: 0,
|
|
57
|
+
})
|
|
58
|
+
}
|
|
59
|
+
console.log(` ${files.size} files`)
|
|
60
|
+
|
|
61
|
+
// ── 2. Exports via ripgrep (bulk) ──
|
|
62
|
+
console.log('🔍 Exports...')
|
|
63
|
+
|
|
64
|
+
type Field = 'exports' | 'types' | 'classNames'
|
|
65
|
+
const patterns: { rgPattern: string; field: Field }[] = [
|
|
66
|
+
{ rgPattern: 'export (async )?function [a-zA-Z_$][a-zA-Z0-9_$]*', field: 'exports' },
|
|
67
|
+
{ rgPattern: 'export const [a-zA-Z_$][a-zA-Z0-9_$]*', field: 'exports' },
|
|
68
|
+
{ rgPattern: 'export (abstract )?class [a-zA-Z_$][a-zA-Z0-9_$]*', field: 'classNames' },
|
|
69
|
+
{ rgPattern: 'export interface [a-zA-Z_$][a-zA-Z0-9_$]*', field: 'types' },
|
|
70
|
+
{ rgPattern: 'export type [a-zA-Z_$][a-zA-Z0-9_$]*', field: 'types' },
|
|
71
|
+
]
|
|
72
|
+
|
|
73
|
+
for (const p of patterns) {
|
|
74
|
+
// Single rg call per pattern — fast
|
|
75
|
+
const out = rg(`--with-filename --only-matching --no-line-number "${p.rgPattern}" src/`).trim()
|
|
76
|
+
for (const line of out.split('\n').filter(Boolean)) {
|
|
77
|
+
const fileSep = line.indexOf(':')
|
|
78
|
+
if (fileSep === -1) continue
|
|
79
|
+
const rawPath = line.slice(0, fileSep).replace(/\\/g, '/').replace(/^src\//, '')
|
|
80
|
+
const matched = line.slice(fileSep + 1).trim()
|
|
81
|
+
const name = matched.split(/\s+/).pop()
|
|
82
|
+
if (!name || !rawPath) continue
|
|
83
|
+
const f = files.get(rawPath)
|
|
84
|
+
if (f && !(f[p.field] as string[]).includes(name)) {
|
|
85
|
+
(f[p.field] as string[]).push(name)
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// ── 3. Imports + TODOs ──
|
|
91
|
+
console.log('📎 Dependencies...')
|
|
92
|
+
|
|
93
|
+
for (const [relPath, info] of files) {
|
|
94
|
+
try {
|
|
95
|
+
const content = readFileSync(join(SRC, relPath), 'utf-8')
|
|
96
|
+
|
|
97
|
+
// Internal imports
|
|
98
|
+
const srcImports: string[] = []
|
|
99
|
+
for (const line of content.split('\n')) {
|
|
100
|
+
const m = line.match(/from\s+['"](src\/[^'"]+)['"]/)
|
|
101
|
+
if (m) srcImports.push(m[1])
|
|
102
|
+
}
|
|
103
|
+
info.imports = [...new Set(srcImports)]
|
|
104
|
+
|
|
105
|
+
// TODOs
|
|
106
|
+
info.todos = (content.match(/\b(TODO|FIXME|HACK|XXX)\b/g) || []).length
|
|
107
|
+
} catch {
|
|
108
|
+
// skip unreadable files
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// Also add class names to exports
|
|
113
|
+
for (const f of files.values()) {
|
|
114
|
+
for (const cls of f.classNames) {
|
|
115
|
+
if (!f.exports.includes(cls)) f.exports.push(cls)
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// ── 4. Generate markdown ──
|
|
120
|
+
console.log('📝 Generating...')
|
|
121
|
+
|
|
122
|
+
const totalLines = [...files.values()].reduce((a, f) => a + f.lines, 0)
|
|
123
|
+
const totalExports = [...files.values()].reduce((a, f) => a + f.exports.length, 0)
|
|
124
|
+
const totalTodos = [...files.values()].reduce((a, f) => a + f.todos, 0)
|
|
125
|
+
|
|
126
|
+
const md: string[] = []
|
|
127
|
+
md.push('# Code Graph')
|
|
128
|
+
md.push('')
|
|
129
|
+
md.push(`_Auto-generated ${new Date().toISOString().slice(0, 10)}_`)
|
|
130
|
+
md.push('')
|
|
131
|
+
md.push(`- **Source files**: ${files.size}`)
|
|
132
|
+
md.push(`- **Total lines**: ${totalLines.toLocaleString()}`)
|
|
133
|
+
md.push(`- **Exports**: ${totalExports}`)
|
|
134
|
+
md.push(`- **TODOs**: ${totalTodos}`)
|
|
135
|
+
md.push('')
|
|
136
|
+
|
|
137
|
+
// Group by top directory
|
|
138
|
+
const dirMap = new Map<string, Module[]>()
|
|
139
|
+
for (const f of files.values()) {
|
|
140
|
+
const dir = f.path.split('/')[0]
|
|
141
|
+
if (!dirMap.has(dir)) dirMap.set(dir, [])
|
|
142
|
+
dirMap.get(dir)!.push(f)
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
for (const [dir, dirFiles] of [...dirMap.entries()].sort()) {
|
|
146
|
+
md.push(`## ${dir}/`)
|
|
147
|
+
md.push('')
|
|
148
|
+
dirFiles.sort((a, b) => b.lines - a.lines)
|
|
149
|
+
|
|
150
|
+
for (const f of dirFiles) {
|
|
151
|
+
if (f.lines < 25 && !f.exports.length && !f.types.length) continue
|
|
152
|
+
|
|
153
|
+
const badges = [`${f.lines} lines`]
|
|
154
|
+
if (f.todos) badges.push(`⚠️ ${f.todos}`)
|
|
155
|
+
|
|
156
|
+
md.push(`### \`${f.path}\``)
|
|
157
|
+
md.push(`_${badges.join(', ')}_`)
|
|
158
|
+
|
|
159
|
+
if (f.exports.length) {
|
|
160
|
+
const list = f.exports.slice(0, 10).join(', ')
|
|
161
|
+
md.push(`- exports: ${list}${f.exports.length > 10 ? ` [+${f.exports.length - 10}]` : ''}`)
|
|
162
|
+
}
|
|
163
|
+
if (f.types.length) {
|
|
164
|
+
const list = f.types.slice(0, 6).join(', ')
|
|
165
|
+
md.push(`- types: ${list}${f.types.length > 6 ? ` [+${f.types.length - 6}]` : ''}`)
|
|
166
|
+
}
|
|
167
|
+
if (f.imports.length) {
|
|
168
|
+
const list = f.imports.slice(0, 5).join(', ')
|
|
169
|
+
md.push(`- deps: ${list}${f.imports.length > 5 ? ` (+${f.imports.length - 5})` : ''}`)
|
|
170
|
+
}
|
|
171
|
+
md.push('')
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// Hot dependencies
|
|
176
|
+
const depCount = new Map<string, number>()
|
|
177
|
+
for (const f of files.values()) {
|
|
178
|
+
for (const dep of f.imports) {
|
|
179
|
+
depCount.set(dep, (depCount.get(dep) || 0) + 1)
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
const sortedDeps = [...depCount.entries()].sort((a, b) => b[1] - a[1]).slice(0, 15)
|
|
184
|
+
if (sortedDeps.length) {
|
|
185
|
+
md.push('---')
|
|
186
|
+
md.push('## Hot Dependencies')
|
|
187
|
+
md.push('')
|
|
188
|
+
for (const [dep, count] of sortedDeps) {
|
|
189
|
+
md.push(`- \`${dep}\` — imported by ${count} files`)
|
|
190
|
+
}
|
|
191
|
+
md.push('')
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// Large files
|
|
195
|
+
const largeFiles = [...files.values()].filter(f => f.lines > 300).sort((a, b) => b.lines - a.lines)
|
|
196
|
+
if (largeFiles.length) {
|
|
197
|
+
md.push('---')
|
|
198
|
+
md.push('## Large Files (>300 lines)')
|
|
199
|
+
md.push('')
|
|
200
|
+
for (const f of largeFiles) {
|
|
201
|
+
md.push(`- \`${f.path}\` — ${f.lines} lines${f.todos ? ', ⚠️' : ''}`)
|
|
202
|
+
}
|
|
203
|
+
md.push('')
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// TODO hotspots
|
|
207
|
+
const todoFiles = [...files.values()].filter(f => f.todos > 2).sort((a, b) => b.todos - a.todos).slice(0, 10)
|
|
208
|
+
if (todoFiles.length) {
|
|
209
|
+
md.push('---')
|
|
210
|
+
md.push('## TODO Hotspots')
|
|
211
|
+
md.push('')
|
|
212
|
+
for (const f of todoFiles) {
|
|
213
|
+
md.push(`- \`${f.path}\` — ${f.todos} TODOs`)
|
|
214
|
+
}
|
|
215
|
+
md.push('')
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
mkdirSync(dirname(OUT), { recursive: true })
|
|
219
|
+
writeFileSync(OUT, md.join('\n'), 'utf-8')
|
|
220
|
+
console.log(`\n✅ ${OUT}`)
|
|
221
|
+
console.log(` ${files.size} files | ${totalLines.toLocaleString()} lines | ${totalExports} exports`)
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
#
|
|
3
|
+
# Comments on a GitHub issue with a list of potential duplicates.
|
|
4
|
+
# Usage: ./comment-on-duplicates.sh --potential-duplicates 456 789 101
|
|
5
|
+
#
|
|
6
|
+
# The base issue number is read from the workflow event payload.
|
|
7
|
+
#
|
|
8
|
+
|
|
9
|
+
set -euo pipefail
|
|
10
|
+
|
|
11
|
+
REPO="JonusNattapong/ClaudeCode"
|
|
12
|
+
|
|
13
|
+
# Read from event payload so the issue number is bound to the triggering event.
|
|
14
|
+
# Falls back to workflow_dispatch inputs for manual runs.
|
|
15
|
+
BASE_ISSUE=$(jq -r '.issue.number // .inputs.issue_number // empty' "${GITHUB_EVENT_PATH:?GITHUB_EVENT_PATH not set}")
|
|
16
|
+
if ! [[ "$BASE_ISSUE" =~ ^[0-9]+$ ]]; then
|
|
17
|
+
echo "Error: no issue number in event payload" >&2
|
|
18
|
+
exit 1
|
|
19
|
+
fi
|
|
20
|
+
|
|
21
|
+
DUPLICATES=()
|
|
22
|
+
|
|
23
|
+
# Parse arguments
|
|
24
|
+
while [[ $# -gt 0 ]]; do
|
|
25
|
+
case $1 in
|
|
26
|
+
--potential-duplicates)
|
|
27
|
+
shift
|
|
28
|
+
while [[ $# -gt 0 && ! "$1" =~ ^-- ]]; do
|
|
29
|
+
DUPLICATES+=("$1")
|
|
30
|
+
shift
|
|
31
|
+
done
|
|
32
|
+
;;
|
|
33
|
+
*)
|
|
34
|
+
echo "Error: unknown argument (only --potential-duplicates is accepted)" >&2
|
|
35
|
+
exit 1
|
|
36
|
+
;;
|
|
37
|
+
esac
|
|
38
|
+
done
|
|
39
|
+
|
|
40
|
+
# Validate duplicates
|
|
41
|
+
if [[ ${#DUPLICATES[@]} -eq 0 ]]; then
|
|
42
|
+
echo "Error: --potential-duplicates requires at least one issue number" >&2
|
|
43
|
+
exit 1
|
|
44
|
+
fi
|
|
45
|
+
|
|
46
|
+
if [[ ${#DUPLICATES[@]} -gt 3 ]]; then
|
|
47
|
+
echo "Error: --potential-duplicates accepts at most 3 issues" >&2
|
|
48
|
+
exit 1
|
|
49
|
+
fi
|
|
50
|
+
|
|
51
|
+
for dup in "${DUPLICATES[@]}"; do
|
|
52
|
+
if ! [[ "$dup" =~ ^[0-9]+$ ]]; then
|
|
53
|
+
echo "Error: duplicate issue must be a number, got: $dup" >&2
|
|
54
|
+
exit 1
|
|
55
|
+
fi
|
|
56
|
+
done
|
|
57
|
+
|
|
58
|
+
# Validate that base issue exists
|
|
59
|
+
if ! gh issue view "$BASE_ISSUE" --repo "$REPO" &>/dev/null; then
|
|
60
|
+
echo "Error: issue #$BASE_ISSUE does not exist in $REPO" >&2
|
|
61
|
+
exit 1
|
|
62
|
+
fi
|
|
63
|
+
|
|
64
|
+
# Validate that all duplicate issues exist
|
|
65
|
+
for dup in "${DUPLICATES[@]}"; do
|
|
66
|
+
if ! gh issue view "$dup" --repo "$REPO" &>/dev/null; then
|
|
67
|
+
echo "Error: issue #$dup does not exist in $REPO" >&2
|
|
68
|
+
exit 1
|
|
69
|
+
fi
|
|
70
|
+
done
|
|
71
|
+
|
|
72
|
+
# Build comment body
|
|
73
|
+
COUNT=${#DUPLICATES[@]}
|
|
74
|
+
if [[ $COUNT -eq 1 ]]; then
|
|
75
|
+
HEADER="Found 1 possible duplicate issue:"
|
|
76
|
+
else
|
|
77
|
+
HEADER="Found $COUNT possible duplicate issues:"
|
|
78
|
+
fi
|
|
79
|
+
|
|
80
|
+
BODY="$HEADER"$'\n\n'
|
|
81
|
+
INDEX=1
|
|
82
|
+
for dup in "${DUPLICATES[@]}"; do
|
|
83
|
+
BODY+="$INDEX. https://github.com/$REPO/issues/$dup"$'\n'
|
|
84
|
+
((INDEX++))
|
|
85
|
+
done
|
|
86
|
+
|
|
87
|
+
BODY+=$'\n'"This issue will be automatically closed as a duplicate in 3 days."$'\n\n'
|
|
88
|
+
BODY+="- If your issue is a duplicate, please close it and 👍 the existing issue instead"$'\n'
|
|
89
|
+
BODY+="- To prevent auto-closure, add a comment or 👎 this comment"$'\n\n'
|
|
90
|
+
BODY+="🤖 Generated with [Claude Code](https://claude.ai/code)"
|
|
91
|
+
|
|
92
|
+
# Post the comment
|
|
93
|
+
gh issue comment "$BASE_ISSUE" --repo "$REPO" --body "$BODY"
|
|
94
|
+
|
|
95
|
+
echo "Posted duplicate comment on issue #$BASE_ISSUE"
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
#
|
|
3
|
+
# Edits labels on a GitHub issue.
|
|
4
|
+
# Usage: ./edit-issue-labels.sh --add-label bug --add-label needs-triage --remove-label untriaged
|
|
5
|
+
#
|
|
6
|
+
# The issue number is read from the workflow event payload.
|
|
7
|
+
#
|
|
8
|
+
|
|
9
|
+
set -euo pipefail
|
|
10
|
+
|
|
11
|
+
# Read from event payload so the issue number is bound to the triggering event.
|
|
12
|
+
# Falls back to workflow_dispatch inputs for manual runs.
|
|
13
|
+
ISSUE=$(jq -r '.issue.number // .inputs.issue_number // empty' "${GITHUB_EVENT_PATH:?GITHUB_EVENT_PATH not set}")
|
|
14
|
+
if ! [[ "$ISSUE" =~ ^[0-9]+$ ]]; then
|
|
15
|
+
echo "Error: no issue number in event payload" >&2
|
|
16
|
+
exit 1
|
|
17
|
+
fi
|
|
18
|
+
|
|
19
|
+
ADD_LABELS=()
|
|
20
|
+
REMOVE_LABELS=()
|
|
21
|
+
|
|
22
|
+
# Parse arguments
|
|
23
|
+
while [[ $# -gt 0 ]]; do
|
|
24
|
+
case $1 in
|
|
25
|
+
--add-label)
|
|
26
|
+
ADD_LABELS+=("$2")
|
|
27
|
+
shift 2
|
|
28
|
+
;;
|
|
29
|
+
--remove-label)
|
|
30
|
+
REMOVE_LABELS+=("$2")
|
|
31
|
+
shift 2
|
|
32
|
+
;;
|
|
33
|
+
*)
|
|
34
|
+
echo "Error: unknown argument (only --add-label and --remove-label are accepted)" >&2
|
|
35
|
+
exit 1
|
|
36
|
+
;;
|
|
37
|
+
esac
|
|
38
|
+
done
|
|
39
|
+
|
|
40
|
+
if [[ ${#ADD_LABELS[@]} -eq 0 && ${#REMOVE_LABELS[@]} -eq 0 ]]; then
|
|
41
|
+
exit 1
|
|
42
|
+
fi
|
|
43
|
+
|
|
44
|
+
# Fetch valid labels from the repo
|
|
45
|
+
VALID_LABELS=$(gh label list --limit 500 --json name --jq '.[].name')
|
|
46
|
+
|
|
47
|
+
# Filter to only labels that exist in the repo
|
|
48
|
+
FILTERED_ADD=()
|
|
49
|
+
for label in "${ADD_LABELS[@]}"; do
|
|
50
|
+
if echo "$VALID_LABELS" | grep -qxF "$label"; then
|
|
51
|
+
FILTERED_ADD+=("$label")
|
|
52
|
+
fi
|
|
53
|
+
done
|
|
54
|
+
|
|
55
|
+
FILTERED_REMOVE=()
|
|
56
|
+
for label in "${REMOVE_LABELS[@]}"; do
|
|
57
|
+
if echo "$VALID_LABELS" | grep -qxF "$label"; then
|
|
58
|
+
FILTERED_REMOVE+=("$label")
|
|
59
|
+
fi
|
|
60
|
+
done
|
|
61
|
+
|
|
62
|
+
if [[ ${#FILTERED_ADD[@]} -eq 0 && ${#FILTERED_REMOVE[@]} -eq 0 ]]; then
|
|
63
|
+
exit 0
|
|
64
|
+
fi
|
|
65
|
+
|
|
66
|
+
# Build gh command arguments
|
|
67
|
+
GH_ARGS=("issue" "edit" "$ISSUE")
|
|
68
|
+
|
|
69
|
+
for label in "${FILTERED_ADD[@]}"; do
|
|
70
|
+
GH_ARGS+=("--add-label" "$label")
|
|
71
|
+
done
|
|
72
|
+
|
|
73
|
+
for label in "${FILTERED_REMOVE[@]}"; do
|
|
74
|
+
GH_ARGS+=("--remove-label" "$label")
|
|
75
|
+
done
|
|
76
|
+
|
|
77
|
+
gh "${GH_ARGS[@]}"
|
|
78
|
+
|
|
79
|
+
if [[ ${#FILTERED_ADD[@]} -gt 0 ]]; then
|
|
80
|
+
echo "Added: ${FILTERED_ADD[*]}"
|
|
81
|
+
fi
|
|
82
|
+
if [[ ${#FILTERED_REMOVE[@]} -gt 0 ]]; then
|
|
83
|
+
echo "Removed: ${FILTERED_REMOVE[*]}"
|
|
84
|
+
fi
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
|
|
4
|
+
function walk(dir) {
|
|
5
|
+
const r = [];
|
|
6
|
+
for (const e of fs.readdirSync(dir, {withFileTypes:true})) {
|
|
7
|
+
const f = path.join(dir, e.name);
|
|
8
|
+
if (e.isDirectory()) {
|
|
9
|
+
if (!e.name.startsWith('.') && e.name !== 'node_modules' && e.name !== 'dist') r.push(...walk(f));
|
|
10
|
+
} else if (e.name.endsWith('.ts') || e.name.endsWith('.tsx')) r.push(f);
|
|
11
|
+
}
|
|
12
|
+
return r;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const files = walk('src');
|
|
16
|
+
let count = 0;
|
|
17
|
+
for (const f of files) {
|
|
18
|
+
let c = fs.readFileSync(f, 'utf-8');
|
|
19
|
+
const orig = c;
|
|
20
|
+
|
|
21
|
+
c = c.replace(/\/peer\//g, '/swarm/');
|
|
22
|
+
c = c.replace(/\bPeer(Broadcast|Disconnect|Discover|Join|ListMessages|ListRoles|Ping|Run|SendMessage|SetName|SetRole|Share|Spawn|Info|Help|Server|Store|Discovery|StatusLine|Type)Tool\b/g, 'Swarm$1Tool');
|
|
23
|
+
c = c.replace(/\bPeer(Broadcast|Disconnect|Discover|Join|ListMessages|ListRoles|Ping|Run|SendMessage|SetName|SetRole|Share|Spawn|Info|Help|Server|Store|Discovery|StatusLine|Type)\b/g, 'Swarm$1');
|
|
24
|
+
c = c.replace(/\bProcessPeer(Provider|Tool)/g, 'ProcessSwarm$1');
|
|
25
|
+
c = c.replace(/\bCodexPeer/g, 'CodexSwarm');
|
|
26
|
+
c = c.replace(/\bgetProcessPeerProvider\b/g, 'getProcessSwarmProvider');
|
|
27
|
+
c = c.replace(/\bgetProcessPeerProviderIds\b/g, 'getProcessSwarmProviderIds');
|
|
28
|
+
c = c.replace(/\bmyPeerId\b/g, 'mySwarmId');
|
|
29
|
+
c = c.replace(/\bpeerId\b/g, 'swarmId');
|
|
30
|
+
c = c.replace(/\bPEER_(\w+)_TOOL_NAME\b/g, 'SWARM_$1_TOOL_NAME');
|
|
31
|
+
c = c.replace(/\bpeer(Server|Store|Discovery|Port|Name|Info|List|Count|Connection|Sharing|Health)\b/g, 'swarm$1');
|
|
32
|
+
c = c.replace(/\bgetGlobalPeer(Server|Store)\b/g, 'getGlobalSwarm$1');
|
|
33
|
+
c = c.replace(/\bnotifyPeerFeedback\b/g, 'notifySwarmFeedback');
|
|
34
|
+
c = c.replace(/\bsetPeerFeedbackHandler\b/g, 'setSwarmFeedbackHandler');
|
|
35
|
+
c = c.replace(/\bPeerFeedback\b/g, 'SwarmFeedback');
|
|
36
|
+
c = c.replace(/\bpeer-feedback/g, 'swarm-feedback');
|
|
37
|
+
c = c.replace(/\bpeerFeedback/g, 'swarmFeedback');
|
|
38
|
+
c = c.replace(/\/peer\b/g, '/swarm');
|
|
39
|
+
c = c.replace(/['"]peer-/g, (m) => m.replace('peer-', 'swarm-'));
|
|
40
|
+
c = c.replace(/\bspawnPeerTerminal\b/g, 'spawnSwarmTerminal');
|
|
41
|
+
c = c.replace(/\bparseProcessPeerRunArgs\b/g, 'parseProcessSwarmRunArgs');
|
|
42
|
+
c = c.replace(/\bProcessPeerRunArgs\b/g, 'ProcessSwarmRunArgs');
|
|
43
|
+
|
|
44
|
+
if (c !== orig) { fs.writeFileSync(f, c, 'utf-8'); count++; }
|
|
45
|
+
}
|
|
46
|
+
console.log('Updated:', count, 'files');
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Fix mojibake encoding in all docs HTML files.
|
|
3
|
+
*
|
|
4
|
+
* Problem: Files were double-encoded — original UTF-8 bytes were
|
|
5
|
+
* misinterpreted as CP1252 then re-encoded as UTF-8, producing
|
|
6
|
+
* characters like — instead of — and ค instead of ค.
|
|
7
|
+
*
|
|
8
|
+
* This script reverses the damage by mapping each Unicode char
|
|
9
|
+
* back to its CP1252 byte and re-decoding as UTF-8.
|
|
10
|
+
* Legitimate Unicode characters (Thai, arrows, emoji) pass through.
|
|
11
|
+
*/
|
|
12
|
+
import fs from 'node:fs';
|
|
13
|
+
import path from 'node:path';
|
|
14
|
+
|
|
15
|
+
const cp1252 = new Map([
|
|
16
|
+
[0x20ac, 0x80], [0x201a, 0x82], [0x0192, 0x83], [0x201e, 0x84],
|
|
17
|
+
[0x2026, 0x85], [0x2020, 0x86], [0x2021, 0x87], [0x02c6, 0x88],
|
|
18
|
+
[0x2030, 0x89], [0x0160, 0x8a], [0x2039, 0x8b], [0x0152, 0x8c],
|
|
19
|
+
[0x017d, 0x8e], [0x2018, 0x91], [0x2019, 0x92], [0x201c, 0x93],
|
|
20
|
+
[0x201d, 0x94], [0x2022, 0x95], [0x2013, 0x96], [0x2014, 0x97],
|
|
21
|
+
[0x02dc, 0x98], [0x2122, 0x99], [0x0161, 0x9a], [0x203a, 0x9b],
|
|
22
|
+
[0x0153, 0x9c], [0x017e, 0x9e], [0x0178, 0x9f],
|
|
23
|
+
]);
|
|
24
|
+
|
|
25
|
+
function fixMojibake(text) {
|
|
26
|
+
if (text.charCodeAt(0) === 0xfeff) text = text.slice(1); // strip BOM
|
|
27
|
+
|
|
28
|
+
const bytes = [];
|
|
29
|
+
for (let i = 0; i < text.length; i++) {
|
|
30
|
+
const code = text.charCodeAt(i);
|
|
31
|
+
if (code <= 0xff) {
|
|
32
|
+
bytes.push(code);
|
|
33
|
+
} else if (cp1252.has(code)) {
|
|
34
|
+
bytes.push(cp1252.get(code));
|
|
35
|
+
} else {
|
|
36
|
+
const buf = Buffer.from(text[i], 'utf-8');
|
|
37
|
+
for (const b of buf) bytes.push(b);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
return Buffer.from(bytes).toString('utf-8');
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Walk docs/ recursively
|
|
44
|
+
function walk(dir) {
|
|
45
|
+
const results = [];
|
|
46
|
+
const list = fs.readdirSync(dir);
|
|
47
|
+
for (const entry of list) {
|
|
48
|
+
const full = path.join(dir, entry);
|
|
49
|
+
const stat = fs.statSync(full);
|
|
50
|
+
if (stat.isDirectory()) {
|
|
51
|
+
results.push(...walk(full));
|
|
52
|
+
} else if (entry.endsWith('.html')) {
|
|
53
|
+
results.push(full);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
return results;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const files = walk('docs');
|
|
60
|
+
let changed = 0;
|
|
61
|
+
let errors = 0;
|
|
62
|
+
|
|
63
|
+
for (const f of files) {
|
|
64
|
+
try {
|
|
65
|
+
const content = fs.readFileSync(f, 'utf-8');
|
|
66
|
+
const fixed = fixMojibake(content);
|
|
67
|
+
|
|
68
|
+
// skip if no change
|
|
69
|
+
if (fixed === content) {
|
|
70
|
+
console.log(` ✓ ${f} — already clean`);
|
|
71
|
+
continue;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
fs.writeFileSync(f, fixed, 'utf-8');
|
|
75
|
+
console.log(` ✔ ${f}`);
|
|
76
|
+
changed++;
|
|
77
|
+
} catch (err) {
|
|
78
|
+
console.error(` ✘ ${f}: ${err.message}`);
|
|
79
|
+
errors++;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
console.log(`\nDone: ${changed} files fixed, ${errors} errors`);
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Normalize all HTML files to consistent structure:
|
|
3
|
+
* - Empty header (JS-injected)
|
|
4
|
+
* - main.js script at end of body
|
|
5
|
+
*/
|
|
6
|
+
import fs from 'node:fs';
|
|
7
|
+
import path from 'node:path';
|
|
8
|
+
import { fileURLToPath } from 'node:url';
|
|
9
|
+
|
|
10
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
11
|
+
const docsDir = path.resolve(__dirname, '../docs');
|
|
12
|
+
|
|
13
|
+
function walk(dir) {
|
|
14
|
+
const results = [];
|
|
15
|
+
const list = fs.readdirSync(dir);
|
|
16
|
+
for (const entry of list) {
|
|
17
|
+
const full = path.join(dir, entry);
|
|
18
|
+
const stat = fs.statSync(full);
|
|
19
|
+
if (stat.isDirectory()) results.push(...walk(full));
|
|
20
|
+
else if (entry.endsWith('.html')) results.push(full);
|
|
21
|
+
}
|
|
22
|
+
return results;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function relativePath(from, to) {
|
|
26
|
+
const rel = path.relative(path.dirname(from), to);
|
|
27
|
+
return rel.startsWith('.') ? rel : './' + rel;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const files = walk(docsDir);
|
|
31
|
+
let fixed = 0;
|
|
32
|
+
|
|
33
|
+
for (const filePath of files) {
|
|
34
|
+
let c = fs.readFileSync(filePath, 'utf-8');
|
|
35
|
+
const name = path.basename(filePath);
|
|
36
|
+
|
|
37
|
+
// Skip landing page (different structure by design)
|
|
38
|
+
if (name === 'index.html' || name === 'index.th.html') continue;
|
|
39
|
+
|
|
40
|
+
// Fix 1: Replace inline header with empty header
|
|
41
|
+
if (!c.includes('<header class="header"></header>')) {
|
|
42
|
+
c = c.replace(/<header class="header">[\s\S]*?<\/header>\s*/,
|
|
43
|
+
'<header class="header"></header>\n');
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Fix 2: Add main.js if missing
|
|
47
|
+
if (!c.includes('main.js')) {
|
|
48
|
+
const jsRelPath = relativePath(filePath, path.join(docsDir, 'js/main.js'));
|
|
49
|
+
c = c.replace('</body>', `<script src="${jsRelPath}"></script>\n</body>`);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Fix 3: Ensure Google Fonts includes Noto Sans Thai for .th.html files
|
|
53
|
+
if (name.endsWith('.th.html') && !c.includes('Noto+Sans+Thai')) {
|
|
54
|
+
c = c.replace(
|
|
55
|
+
/family=JetBrains\+Mono:[^"'&]+/,
|
|
56
|
+
'$&, &family=Noto+Sans+Thai:wght@400;500;600;700'
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Fix 4: Clean up old lang-wrap styles in features/internals
|
|
61
|
+
c = c.replace(/<div class="lang-wrap">[\s\S]*?<\/div>\s*<\/div>\s*/, '');
|
|
62
|
+
|
|
63
|
+
fs.writeFileSync(filePath, c, 'utf-8');
|
|
64
|
+
fixed++;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
console.log(`Normalized ${fixed} files`);
|