atris 3.30.12 → 3.32.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +16 -3
- package/FOR_AGENTS.md +81 -0
- package/README.md +6 -4
- package/atris/CLAUDE.md +8 -0
- package/atris/atris.md +51 -19
- package/atris/skills/README.md +1 -0
- package/atris/skills/blocks/SKILL.md +134 -0
- package/atris/skills/clawhub/philosophy-of-work/SKILL.md +56 -0
- package/atris/skills/youtube/SKILL.md +31 -11
- package/atris.md +15 -0
- package/ax +189 -7
- package/bin/atris.js +273 -225
- package/commands/autoland.js +379 -0
- package/commands/autopilot-front.js +273 -0
- package/commands/autopilot.js +94 -4
- package/commands/business.js +1 -1
- package/commands/clean.js +72 -9
- package/commands/codex-goal.js +72 -22
- package/commands/computer.js +48 -3
- package/commands/gm.js +1 -1
- package/commands/harvest.js +179 -0
- package/commands/init.js +22 -1
- package/commands/land.js +442 -0
- package/commands/loop-front.js +122 -4
- package/commands/member.js +551 -19
- package/commands/mission.js +3674 -278
- package/commands/play.js +1 -1
- package/commands/pulse.js +65 -7
- package/commands/run-front.js +144 -0
- package/commands/run.js +10 -7
- package/commands/sign.js +90 -0
- package/commands/strings.js +301 -0
- package/commands/task.js +782 -108
- package/commands/truth.js +171 -0
- package/commands/xp.js +32 -8
- package/commands/youtube.js +72 -5
- package/decks/README.md +6 -12
- package/lib/auto-accept-certified.js +10 -0
- package/lib/autoland.js +391 -0
- package/lib/context-gatherer.js +0 -8
- package/lib/mission-artifact.js +504 -0
- package/lib/mission-room.js +846 -0
- package/lib/next-moves.js +212 -6
- package/lib/operator-next.js +7 -0
- package/lib/pulse.js +78 -4
- package/lib/runner-command.js +51 -20
- package/lib/runs-prune.js +242 -0
- package/lib/task-db.js +19 -2
- package/lib/task-proof.js +1 -1
- package/package.json +4 -4
- package/decks/atris-seed-pitch-v3.json +0 -118
- package/decks/atris-seed-pitch-v4-skeleton.json +0 -106
- package/decks/atris-seed-pitch-v5.json +0 -109
- package/decks/atris-seed-pitch-v6.json +0 -137
- package/decks/atris-seed-pitch-v7.json +0 -133
- package/decks/mark-pincus-narrative.json +0 -102
- package/decks/mark-pincus-sourcery.json +0 -94
- package/decks/yash-applied-compute-detailed.json +0 -150
- package/decks/yash-applied-compute-generalist.json +0 -82
- package/decks/yash-applied-compute-narrative.json +0 -54
- package/lib/ax-chat-input.js +0 -164
- package/lib/ax-goal.js +0 -307
- package/lib/ax-prefs.js +0 -63
- package/lib/ax-shimmer.js +0 -63
|
@@ -0,0 +1,301 @@
|
|
|
1
|
+
// atris strings — a content design system built from live codebase content (no LLM).
|
|
2
|
+
//
|
|
3
|
+
// The missing pillar next to `atris slop`: slop catches HOW copy is written (tells,
|
|
4
|
+
// hype, em-dashes); strings governs WHAT words you ship. It scans the repo for
|
|
5
|
+
// user-facing strings, builds a terminology registry in .atris/strings.json, flags
|
|
6
|
+
// the same string written three different ways (the "unnecessary variant" tell), and
|
|
7
|
+
// enforces preferred terms at the commit/PR gate so you rename "live" -> "active"
|
|
8
|
+
// once and it holds everywhere.
|
|
9
|
+
//
|
|
10
|
+
// Zero external deps (Node built-ins only) — repo contract. Deterministic: a finding
|
|
11
|
+
// is a fact (file:line + term), not a taste opinion, so it drops into CI + the gate.
|
|
12
|
+
//
|
|
13
|
+
// Usage:
|
|
14
|
+
// atris strings scan [path] # extract UI strings -> .atris/strings.json
|
|
15
|
+
// atris strings variants # the same string written N different ways
|
|
16
|
+
// atris strings term --ban live --prefer active --why "..." # codify a rule
|
|
17
|
+
// atris strings check --staged # gate: banned terms in changed lines (exit 1)
|
|
18
|
+
// atris strings list # dump the registry
|
|
19
|
+
//
|
|
20
|
+
// Exit code: 0 = clean, 1 = violation/variants found, 2 = bad usage.
|
|
21
|
+
|
|
22
|
+
const fs = require('fs');
|
|
23
|
+
const path = require('path');
|
|
24
|
+
const { gitChangedLines } = require('./slop'); // reuse the diff parser — DRY
|
|
25
|
+
|
|
26
|
+
const CODE_EXTS = new Set(['.tsx', '.jsx', '.ts', '.js', '.mjs', '.vue', '.svelte', '.astro', '.html']);
|
|
27
|
+
const TEXT_EXTS = new Set([...CODE_EXTS, '.md', '.mdx', '.txt']);
|
|
28
|
+
const SKIP_DIRS = new Set(['node_modules', '.git', 'dist', 'build', '.next', '.astro', 'coverage', '.cache', 'out', 'vendor']);
|
|
29
|
+
|
|
30
|
+
const REGISTRY_FILE = path.join('.atris', 'strings.json');
|
|
31
|
+
const MAX_LOCATIONS = 12; // cap stored locations per string so the registry stays readable
|
|
32
|
+
|
|
33
|
+
function walk(target, out, exts) {
|
|
34
|
+
let stat;
|
|
35
|
+
try { stat = fs.statSync(target); } catch { return out; }
|
|
36
|
+
if (stat.isFile()) {
|
|
37
|
+
if (exts.has(path.extname(target))) out.push(target);
|
|
38
|
+
return out;
|
|
39
|
+
}
|
|
40
|
+
if (stat.isDirectory()) {
|
|
41
|
+
if (SKIP_DIRS.has(path.basename(target))) return out;
|
|
42
|
+
for (const name of fs.readdirSync(target)) {
|
|
43
|
+
if (name.startsWith('.') && name !== '.') continue;
|
|
44
|
+
walk(path.join(target, name), out, exts);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
return out;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Is this captured literal a user-facing string, or just code/classes/identifiers?
|
|
51
|
+
// High precision on purpose: a noisy registry gets ignored. minWords lets JSX text
|
|
52
|
+
// nodes (>Settings<) keep single-word labels while quoted literals require a phrase.
|
|
53
|
+
function isUserFacing(s, minWords) {
|
|
54
|
+
if (s.length < 2 || s.length > 200) return false;
|
|
55
|
+
if (!/[A-Za-z]/.test(s)) return false;
|
|
56
|
+
if (/[<>{}=]|\$\{|=>|&&|\|\||::|\/\/|\/\*|\*\//.test(s)) return false; // code
|
|
57
|
+
if (/^https?:/i.test(s) || /\bwww\.[a-z]/i.test(s)) return false; // urls
|
|
58
|
+
if (/^[./~#@\\]/.test(s)) return false; // path/anchor/hex/handle
|
|
59
|
+
if (/^#[0-9a-fA-F]{3,8}$/.test(s)) return false; // hex color
|
|
60
|
+
if (/^[A-Z0-9_]{2,}$/.test(s)) return false; // CONSTANT_CASE
|
|
61
|
+
const words = s.split(/\s+/);
|
|
62
|
+
// className / token-list reject: multi-token where most tokens look like classes
|
|
63
|
+
if (words.length > 1
|
|
64
|
+
&& words.every((w) => /^[\w:[\]\-./%@]+$/.test(w))
|
|
65
|
+
&& words.filter((w) => w.includes('-')).length >= Math.ceil(words.length / 2)) return false;
|
|
66
|
+
if (words.length === 1) {
|
|
67
|
+
if (minWords > 1) return false; // quoted single tokens are usually identifiers
|
|
68
|
+
return /^[A-Z][a-zA-Z]{2,}$/.test(s); // a real Label word, e.g. "Settings"
|
|
69
|
+
}
|
|
70
|
+
const prose = (s.match(/[A-Za-z ]/g) || []).length;
|
|
71
|
+
return prose / s.length >= 0.55; // reads like a sentence, not a payload
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Extract candidate user-facing strings from one file's text.
|
|
75
|
+
function extractStrings(text) {
|
|
76
|
+
const out = [];
|
|
77
|
+
const lines = text.split('\n');
|
|
78
|
+
for (let i = 0; i < lines.length; i++) {
|
|
79
|
+
const line = lines[i];
|
|
80
|
+
const push = (raw, minWords) => { const s = raw.trim(); if (isUserFacing(s, minWords)) out.push({ text: s, line: i + 1 }); };
|
|
81
|
+
let m;
|
|
82
|
+
const jsx = />([^<>{}\n]{2,})</g; while ((m = jsx.exec(line))) push(m[1], 1); // JSX/HTML text node
|
|
83
|
+
const dq = /"([^"\\\n]{2,}?)"/g; while ((m = dq.exec(line))) push(m[1], 2); // "double"
|
|
84
|
+
const sq = /'([^'\\\n]{2,}?)'/g; while ((m = sq.exec(line))) push(m[1], 2); // 'single'
|
|
85
|
+
const tq = /`([^`$\\\n]{2,}?)`/g; while ((m = tq.exec(line))) push(m[1], 2); // `template` (no ${})
|
|
86
|
+
}
|
|
87
|
+
return out;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Canonical form for variant detection: same meaning, different surface (case / punct / spacing).
|
|
91
|
+
function normalize(s) {
|
|
92
|
+
return s.toLowerCase()
|
|
93
|
+
.replace(/…/g, '').replace(/\.\.\.$/, '')
|
|
94
|
+
.replace(/\s+/g, ' ')
|
|
95
|
+
.replace(/[.!?,;:]+$/, '')
|
|
96
|
+
.trim();
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function loadRegistry(root = process.cwd()) {
|
|
100
|
+
try { return JSON.parse(fs.readFileSync(path.join(root, REGISTRY_FILE), 'utf8')); }
|
|
101
|
+
catch { return { version: 1, scannedAt: null, root: null, strings: [], terms: [] }; }
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function saveRegistry(reg, root = process.cwd()) {
|
|
105
|
+
const file = path.join(root, REGISTRY_FILE);
|
|
106
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
107
|
+
fs.writeFileSync(file, JSON.stringify(reg, null, 2) + '\n');
|
|
108
|
+
return file;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// Group strings by normalized form; a cluster with >1 distinct surface form is an inconsistency.
|
|
112
|
+
function variantClusters(strings) {
|
|
113
|
+
const byNorm = new Map();
|
|
114
|
+
for (const s of strings) {
|
|
115
|
+
if (!byNorm.has(s.norm)) byNorm.set(s.norm, []);
|
|
116
|
+
byNorm.get(s.norm).push(s);
|
|
117
|
+
}
|
|
118
|
+
const clusters = [];
|
|
119
|
+
for (const [norm, group] of byNorm) {
|
|
120
|
+
const surfaces = [...new Set(group.map((g) => g.text))];
|
|
121
|
+
if (surfaces.length > 1) {
|
|
122
|
+
clusters.push({ norm, surfaces, count: group.reduce((n, g) => n + g.count, 0) });
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
return clusters.sort((a, b) => b.count - a.count);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function scan(argv) {
|
|
129
|
+
const json = argv.includes('--json');
|
|
130
|
+
const target = argv.find((a) => !a.startsWith('-')) || '.';
|
|
131
|
+
const files = walk(path.resolve(target), [], CODE_EXTS);
|
|
132
|
+
|
|
133
|
+
const byText = new Map(); // text -> { text, norm, count, locations[] }
|
|
134
|
+
for (const file of files) {
|
|
135
|
+
let text; try { text = fs.readFileSync(file, 'utf8'); } catch { continue; }
|
|
136
|
+
const rel = path.relative(process.cwd(), file);
|
|
137
|
+
for (const hit of extractStrings(text)) {
|
|
138
|
+
let entry = byText.get(hit.text);
|
|
139
|
+
if (!entry) { entry = { text: hit.text, norm: normalize(hit.text), count: 0, locations: [] }; byText.set(hit.text, entry); }
|
|
140
|
+
entry.count++;
|
|
141
|
+
if (entry.locations.length < MAX_LOCATIONS) entry.locations.push(`${rel}:${hit.line}`);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
const reg = loadRegistry();
|
|
146
|
+
reg.scannedAt = new Date().toISOString();
|
|
147
|
+
reg.root = path.relative(process.cwd(), path.resolve(target)) || '.';
|
|
148
|
+
reg.strings = [...byText.values()].sort((a, b) => b.count - a.count);
|
|
149
|
+
const file = saveRegistry(reg);
|
|
150
|
+
const clusters = variantClusters(reg.strings);
|
|
151
|
+
|
|
152
|
+
if (json) {
|
|
153
|
+
console.log(JSON.stringify({
|
|
154
|
+
ok: true, scanned: files.length, strings: reg.strings.length,
|
|
155
|
+
occurrences: reg.strings.reduce((n, s) => n + s.count, 0),
|
|
156
|
+
variantClusters: clusters.length, registry: path.relative(process.cwd(), file),
|
|
157
|
+
}, null, 2));
|
|
158
|
+
return 0;
|
|
159
|
+
}
|
|
160
|
+
console.log(`\n scanned ${files.length} file${files.length === 1 ? '' : 's'} -> ${reg.strings.length} unique string${reg.strings.length === 1 ? '' : 's'}`);
|
|
161
|
+
console.log(` registry: ${path.relative(process.cwd(), file)}`);
|
|
162
|
+
if (clusters.length) console.log(`\n ⚠ ${clusters.length} variant cluster${clusters.length === 1 ? '' : 's'} (same string, different casing/punctuation) — run: atris strings variants`);
|
|
163
|
+
else console.log(`\n ✓ no inconsistent variants`);
|
|
164
|
+
console.log('');
|
|
165
|
+
return 0;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function variants(argv) {
|
|
169
|
+
const json = argv.includes('--json');
|
|
170
|
+
const reg = loadRegistry();
|
|
171
|
+
if (!reg.strings.length) { console.error(' no registry yet — run: atris strings scan'); return 2; }
|
|
172
|
+
const clusters = variantClusters(reg.strings);
|
|
173
|
+
if (json) { console.log(JSON.stringify({ ok: clusters.length === 0, clusters }, null, 2)); return clusters.length ? 1 : 0; }
|
|
174
|
+
if (!clusters.length) { console.log(`\n ✓ clean — every string is written one way\n`); return 0; }
|
|
175
|
+
console.log(`\n ${clusters.length} variant cluster${clusters.length === 1 ? '' : 's'} — same string, inconsistent surface form:\n`);
|
|
176
|
+
for (const c of clusters) {
|
|
177
|
+
console.log(` ⚠ ${c.surfaces.map((s) => JSON.stringify(s)).join(' vs ')} (${c.count}×)`);
|
|
178
|
+
}
|
|
179
|
+
console.log(`\n pick one per cluster, then: atris strings term --ban "<wrong>" --prefer "<right>"\n`);
|
|
180
|
+
return 1;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function term(argv) {
|
|
184
|
+
const get = (flag) => { const i = argv.indexOf(flag); return i >= 0 ? argv[i + 1] : null; };
|
|
185
|
+
const ban = get('--ban');
|
|
186
|
+
const prefer = get('--prefer');
|
|
187
|
+
const why = get('--why') || '';
|
|
188
|
+
if (argv.includes('--list') || (!ban && !argv.includes('--remove'))) {
|
|
189
|
+
const reg = loadRegistry();
|
|
190
|
+
if (!reg.terms.length) { console.log('\n no terms yet — add one: atris strings term --ban "live" --prefer "active"\n'); return 0; }
|
|
191
|
+
console.log('\n preferred terms (enforced by: atris strings check):\n');
|
|
192
|
+
for (const t of reg.terms) console.log(` ✗ "${t.ban}" → "${t.prefer}"${t.why ? ` (${t.why})` : ''}`);
|
|
193
|
+
console.log('');
|
|
194
|
+
return 0;
|
|
195
|
+
}
|
|
196
|
+
const reg = loadRegistry();
|
|
197
|
+
if (argv.includes('--remove')) {
|
|
198
|
+
const before = reg.terms.length;
|
|
199
|
+
reg.terms = reg.terms.filter((t) => t.ban.toLowerCase() !== String(get('--remove') || ban || '').toLowerCase());
|
|
200
|
+
saveRegistry(reg);
|
|
201
|
+
console.log(` ${before === reg.terms.length ? 'no match' : 'removed'}: "${get('--remove') || ban}"`);
|
|
202
|
+
return 0;
|
|
203
|
+
}
|
|
204
|
+
if (!ban || !prefer) { console.error(' usage: atris strings term --ban <word> --prefer <word> [--why "..."]'); return 2; }
|
|
205
|
+
reg.terms = reg.terms.filter((t) => t.ban.toLowerCase() !== ban.toLowerCase());
|
|
206
|
+
reg.terms.push({ ban, prefer, why });
|
|
207
|
+
const file = saveRegistry(reg);
|
|
208
|
+
console.log(` ✓ "${ban}" → "${prefer}" added to ${path.relative(process.cwd(), file)}`);
|
|
209
|
+
console.log(` enforce it: atris strings check --staged`);
|
|
210
|
+
return 0;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
function check(argv) {
|
|
214
|
+
const json = argv.includes('--json');
|
|
215
|
+
const quiet = argv.includes('--quiet');
|
|
216
|
+
const staged = argv.includes('--staged');
|
|
217
|
+
const diffMode = staged || argv.includes('--diff');
|
|
218
|
+
const reg = loadRegistry();
|
|
219
|
+
if (!reg.terms.length) {
|
|
220
|
+
if (json) { console.log(JSON.stringify({ ok: true, terms: 0, findings: [] }, null, 2)); }
|
|
221
|
+
else if (!quiet) console.log('\n no terms to enforce — add one: atris strings term --ban "live" --prefer "active"\n');
|
|
222
|
+
return 0;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
let files, changed = null;
|
|
226
|
+
if (diffMode) {
|
|
227
|
+
changed = gitChangedLines(staged);
|
|
228
|
+
files = [...changed.keys()].filter((f) => TEXT_EXTS.has(path.extname(f)) && fs.existsSync(f));
|
|
229
|
+
} else {
|
|
230
|
+
const target = argv.find((a) => !a.startsWith('-')) || '.';
|
|
231
|
+
files = walk(path.resolve(target), [], TEXT_EXTS);
|
|
232
|
+
}
|
|
233
|
+
const regAbs = path.resolve(process.cwd(), REGISTRY_FILE);
|
|
234
|
+
|
|
235
|
+
const matchers = reg.terms.map((t) => ({ ...t, re: new RegExp(`\\b${t.ban.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}\\b`, 'i') }));
|
|
236
|
+
const findings = [];
|
|
237
|
+
for (const file of files) {
|
|
238
|
+
if (path.resolve(file) === regAbs) continue; // never flag the registry itself
|
|
239
|
+
let text; try { text = fs.readFileSync(file, 'utf8'); } catch { continue; }
|
|
240
|
+
const lines = text.split('\n');
|
|
241
|
+
for (let i = 0; i < lines.length; i++) {
|
|
242
|
+
if (diffMode && changed && !(changed.get(path.resolve(file)) && changed.get(path.resolve(file)).has(i + 1))) continue;
|
|
243
|
+
for (const t of matchers) {
|
|
244
|
+
if (t.re.test(lines[i])) findings.push({ file: path.relative(process.cwd(), file), line: i + 1, ban: t.ban, prefer: t.prefer, why: t.why });
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
if (json) { console.log(JSON.stringify({ ok: findings.length === 0, scanned: files.length, terms: reg.terms.length, findings }, null, 2)); return findings.length ? 1 : 0; }
|
|
250
|
+
if (!findings.length) { if (!quiet) console.log(`\n ✓ clean — no banned terms in ${files.length} file${files.length === 1 ? '' : 's'}\n`); else console.log(' ✓ clean · exit 0'); return 0; }
|
|
251
|
+
if (!quiet) {
|
|
252
|
+
console.log('');
|
|
253
|
+
const w = Math.max(...findings.map((f) => `${f.file}:${f.line}`.length));
|
|
254
|
+
for (const f of findings) console.log(` ✗ ${`${f.file}:${f.line}`.padEnd(w)} "${f.ban}" → "${f.prefer}"${f.why ? ` ${f.why}` : ''}`);
|
|
255
|
+
}
|
|
256
|
+
console.log(`\n ${findings.length} banned-term use${findings.length === 1 ? '' : 's'} · exit 1\n`);
|
|
257
|
+
return 1;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
function list(argv) {
|
|
261
|
+
const json = argv.includes('--json');
|
|
262
|
+
const reg = loadRegistry();
|
|
263
|
+
if (json) { console.log(JSON.stringify(reg, null, 2)); return 0; }
|
|
264
|
+
if (!reg.strings.length) { console.error(' no registry yet — run: atris strings scan'); return 2; }
|
|
265
|
+
const top = Number((argv[argv.indexOf('--top') + 1]) || 30);
|
|
266
|
+
console.log(`\n ${reg.strings.length} strings (scanned ${reg.scannedAt || '?'}), top ${Math.min(top, reg.strings.length)} by use:\n`);
|
|
267
|
+
for (const s of reg.strings.slice(0, top)) console.log(` ${String(s.count).padStart(3)}× ${JSON.stringify(s.text)}`);
|
|
268
|
+
console.log('');
|
|
269
|
+
return 0;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
function stringsCommand(argv) {
|
|
273
|
+
const sub = argv[0];
|
|
274
|
+
const rest = argv.slice(1);
|
|
275
|
+
if (sub === 'scan') return scan(rest);
|
|
276
|
+
if (sub === 'variants' || sub === 'dupes') return variants(rest);
|
|
277
|
+
if (sub === 'term' || sub === 'terms') return term(rest);
|
|
278
|
+
if (sub === 'check' || sub === 'gate') return check(rest);
|
|
279
|
+
if (sub === 'list' || sub === 'ls') return list(rest);
|
|
280
|
+
console.log(`
|
|
281
|
+
atris strings — a content design system from your live codebase (no LLM)
|
|
282
|
+
|
|
283
|
+
atris strings scan [path] extract user-facing strings -> .atris/strings.json
|
|
284
|
+
atris strings variants the same string written N different ways (pick one)
|
|
285
|
+
atris strings term --ban <a> --prefer <b> [--why "..."] codify a preferred term
|
|
286
|
+
atris strings term --list show the preferred terms
|
|
287
|
+
atris strings check [--staged] gate: flag banned terms in changed lines (exit 1)
|
|
288
|
+
atris strings list [--top N] the registry, most-used first
|
|
289
|
+
add --json to scan/variants/check/list for machine output
|
|
290
|
+
|
|
291
|
+
Pairs with 'atris slop' (how copy reads) — strings governs what words you ship.
|
|
292
|
+
The registry lives in .atris/strings.json. Wire 'check --staged' into the pre-commit gate.
|
|
293
|
+
`);
|
|
294
|
+
return 0;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
module.exports = {
|
|
298
|
+
stringsCommand, scan, variants, term, check, list,
|
|
299
|
+
isUserFacing, extractStrings, normalize, variantClusters,
|
|
300
|
+
loadRegistry, saveRegistry, walk, CODE_EXTS, TEXT_EXTS,
|
|
301
|
+
};
|