letfixit 0.1.0 → 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/.env.example +3 -3
- package/README.md +5 -2
- package/package.json +12 -2
- package/scripts/setup-model.js +48 -9
- package/server/index.js +75 -24
- package/server/model_config.js +1 -0
package/.env.example
CHANGED
|
@@ -17,9 +17,9 @@ DASHBOARD_PORT=8081
|
|
|
17
17
|
FLUTTER_VM_URL=
|
|
18
18
|
|
|
19
19
|
# ── Ultron n0-beta model download (encrypted chunks) ───────────────────────────
|
|
20
|
-
#
|
|
21
|
-
#
|
|
22
|
-
#
|
|
20
|
+
# Normally you DON'T set this — `npm run setup` prompts you to paste the model
|
|
21
|
+
# decryption key interactively (input hidden). Set it here ONLY for
|
|
22
|
+
# non-interactive / CI installs. NEVER commit a real key.
|
|
23
23
|
PARITYFIX_MODEL_KEY=
|
|
24
24
|
# Optional: path to a chunk manifest (defaults to config/model.manifest.json).
|
|
25
25
|
PARITYFIX_MODEL_MANIFEST=
|
package/README.md
CHANGED
|
@@ -10,10 +10,13 @@ A lightweight developer tool that connects to a running Flutter or Android app,
|
|
|
10
10
|
|
|
11
11
|
**From npm (published):**
|
|
12
12
|
```bash
|
|
13
|
-
npm install -g letfixit #
|
|
14
|
-
|
|
13
|
+
npm install -g letfixit # postinstall runs setup and PROMPTS for the model key
|
|
14
|
+
# → paste the Ultron n0-beta key you were given (input hidden)
|
|
15
15
|
letfixit # branded startup → "Ultron n0-beta initializing…" → analyze menu
|
|
16
16
|
```
|
|
17
|
+
The decryption key is **never bundled** — setup asks for it interactively, so only
|
|
18
|
+
users you give the key to can decrypt the model. For CI/non-interactive installs,
|
|
19
|
+
set `PARITYFIX_MODEL_KEY` in the environment instead of typing it.
|
|
17
20
|
|
|
18
21
|
**From source (clone):**
|
|
19
22
|
```bash
|
package/package.json
CHANGED
|
@@ -1,8 +1,18 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "letfixit",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.2",
|
|
4
4
|
"description": "LetFixIt — AI-based realtime performance analysis & suggestions for Flutter, Android & iOS, powered by the local Ultron n0-beta model.",
|
|
5
|
-
"keywords": [
|
|
5
|
+
"keywords": [
|
|
6
|
+
"flutter",
|
|
7
|
+
"android",
|
|
8
|
+
"ios",
|
|
9
|
+
"performance",
|
|
10
|
+
"profiler",
|
|
11
|
+
"devtools",
|
|
12
|
+
"observability",
|
|
13
|
+
"llm",
|
|
14
|
+
"local-ai"
|
|
15
|
+
],
|
|
6
16
|
"license": "MIT",
|
|
7
17
|
"author": "JahidDev24",
|
|
8
18
|
"main": "server/index.js",
|
package/scripts/setup-model.js
CHANGED
|
@@ -12,6 +12,7 @@ const path = require('path');
|
|
|
12
12
|
const crypto = require('crypto');
|
|
13
13
|
const https = require('https');
|
|
14
14
|
const http = require('http');
|
|
15
|
+
const readline = require('readline');
|
|
15
16
|
const { fileURLToPath } = require('url');
|
|
16
17
|
require('./load-env')(); // pull PARITYFIX_MODEL_KEY etc. from .env (root has no dotenv)
|
|
17
18
|
const mc = require('../server/model_config');
|
|
@@ -132,6 +133,29 @@ async function fetchChunk(ch, encPath, baseLabel) {
|
|
|
132
133
|
throw lastErr || new Error(`chunk ${ch.index} download failed`);
|
|
133
134
|
}
|
|
134
135
|
|
|
136
|
+
// ── Key resolution (interactive) ──────────────────────────────────────────────
|
|
137
|
+
// The decryption key is NEVER bundled or committed. It's resolved at setup time
|
|
138
|
+
// from, in order: an explicit env var (CI/automation), else an interactive
|
|
139
|
+
// prompt where the user pastes the key they were given. Non-interactive with no
|
|
140
|
+
// env key → null (skip, non-fatal). The key is only needed once — after a
|
|
141
|
+
// successful setup the decrypted model is on disk and later runs skip this.
|
|
142
|
+
async function resolveKey() {
|
|
143
|
+
if (process.env.PARITYFIX_MODEL_KEY) return process.env.PARITYFIX_MODEL_KEY.trim();
|
|
144
|
+
if (!process.stdin.isTTY) return null; // can't prompt (postinstall in CI, piped stdin)
|
|
145
|
+
const k = await promptHidden(` 🔑 Enter the ${mc.MODEL_NAME} decryption key: `);
|
|
146
|
+
return k || null;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// Read a line from stdin without echoing it (so the key isn't shown on screen).
|
|
150
|
+
function promptHidden(query) {
|
|
151
|
+
return new Promise((resolve) => {
|
|
152
|
+
process.stdout.write(c(C.mag, query));
|
|
153
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
154
|
+
rl._writeToOutput = () => {}; // mute echo of typed characters
|
|
155
|
+
rl.question('', (answer) => { rl.close(); process.stdout.write('\n'); resolve(answer.trim()); });
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
|
|
135
159
|
function sha256File(p) {
|
|
136
160
|
return new Promise((resolve, reject) => {
|
|
137
161
|
const h = crypto.createHash('sha256');
|
|
@@ -186,12 +210,17 @@ async function setup() {
|
|
|
186
210
|
}
|
|
187
211
|
|
|
188
212
|
const manifest = mc.loadManifest();
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
console.log(c(C.
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
213
|
+
if (!manifest) {
|
|
214
|
+
console.log(c(C.yel, ' ⚠️ No chunk manifest found — skipping automatic download.'));
|
|
215
|
+
console.log(` Copy ${c(C.cyan, 'config/model.manifest.example.json')} → ${c(C.cyan, 'config/model.manifest.json')} and fill in the chunk URLs, then run ${c(C.cyan, 'npm run setup')}.\n`);
|
|
216
|
+
return; // non-fatal
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
const key = await resolveKey();
|
|
220
|
+
if (!key) {
|
|
221
|
+
console.log(c(C.yel, ' ⚠️ No decryption key provided — skipping model download.'));
|
|
222
|
+
console.log(` Run ${c(C.cyan, 'npm run setup')} in a terminal and paste the ${mc.MODEL_NAME} key when prompted`);
|
|
223
|
+
console.log(` (or set ${c(C.cyan, 'PARITYFIX_MODEL_KEY')} for non-interactive/CI installs).\n`);
|
|
195
224
|
return; // non-fatal
|
|
196
225
|
}
|
|
197
226
|
|
|
@@ -213,7 +242,12 @@ async function setup() {
|
|
|
213
242
|
if (!haveValid) await fetchChunk(ch, encPath, label);
|
|
214
243
|
|
|
215
244
|
process.stdout.write(` ${c(C.dim, `↳ decrypting chunk ${ch.index + 1}…`)}\r`);
|
|
216
|
-
|
|
245
|
+
try {
|
|
246
|
+
await decryptChunk(encPath, decPath, derivedKey, ch.iv, ch.authTag);
|
|
247
|
+
} catch (e) {
|
|
248
|
+
// GCM auth failure = the key doesn't match the ciphertext → wrong key.
|
|
249
|
+
throw new Error(`WRONG_KEY: decryption failed on chunk ${ch.index} — the ${mc.MODEL_NAME} key is incorrect.`);
|
|
250
|
+
}
|
|
217
251
|
process.stdout.write(' '.repeat(40) + '\r');
|
|
218
252
|
decParts.push(decPath);
|
|
219
253
|
}
|
|
@@ -260,8 +294,13 @@ function wrap(text, width, indent) {
|
|
|
260
294
|
|
|
261
295
|
if (require.main === module) {
|
|
262
296
|
setup().catch((err) => {
|
|
263
|
-
|
|
264
|
-
|
|
297
|
+
if (String(err.message).startsWith('WRONG_KEY:')) {
|
|
298
|
+
console.log('\n' + c(C.red, ` ❌ ${err.message.replace('WRONG_KEY: ', '')}`));
|
|
299
|
+
console.log(c(C.dim, ` Double-check the key you were given and run ${'`npm run setup`'} to try again.\n`));
|
|
300
|
+
} else {
|
|
301
|
+
console.log('\n' + c(C.red, ` ❌ Model setup failed: ${err.message}`));
|
|
302
|
+
console.log(c(C.dim, ' You can retry with `npm run setup`, or use ./setup.sh for the HuggingFace source.\n'));
|
|
303
|
+
}
|
|
265
304
|
process.exit(isPostinstall ? 0 : 1); // never break `npm install`
|
|
266
305
|
});
|
|
267
306
|
}
|
package/server/index.js
CHANGED
|
@@ -29,7 +29,7 @@ const ENABLE_LOCAL_AI = process.env.ENABLE_LOCAL_AI !== 'false';
|
|
|
29
29
|
const MODEL_PATH = model.MODEL_PATH;
|
|
30
30
|
|
|
31
31
|
// ── Terminal styling + branded startup ─────────────────────────────────────────
|
|
32
|
-
const C = { reset: '\x1b[0m', bold: '\x1b[1m', dim: '\x1b[2m', mag: '\x1b[95m', grn: '\x1b[92m', yel: '\x1b[93m', cyan: '\x1b[96m' };
|
|
32
|
+
const C = { reset: '\x1b[0m', bold: '\x1b[1m', dim: '\x1b[2m', mag: '\x1b[95m', grn: '\x1b[92m', yel: '\x1b[93m', red: '\x1b[91m', cyan: '\x1b[96m' };
|
|
33
33
|
const col = (x, s) => `${x}${s}${C.reset}`;
|
|
34
34
|
const stripAnsi = (s) => s.replace(/\x1b\[[0-9;]*m/g, '');
|
|
35
35
|
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
@@ -42,10 +42,11 @@ function bannerBox(lines) {
|
|
|
42
42
|
}
|
|
43
43
|
|
|
44
44
|
function printWelcome() {
|
|
45
|
+
const v = require('../package.json').version;
|
|
45
46
|
console.log('');
|
|
46
47
|
bannerBox([
|
|
47
48
|
'',
|
|
48
|
-
col(C.bold + C.mag, '✦ P A R I T Y F I X') + col(C.dim,
|
|
49
|
+
col(C.bold + C.mag, '✦ P A R I T Y F I X') + col(C.dim, ` v${v}`),
|
|
49
50
|
col(C.reset, 'AI-based realtime performance analysis & suggestions'),
|
|
50
51
|
col(C.dim, 'Flutter · Android · iOS powered by Ultron n0-beta'),
|
|
51
52
|
'',
|
|
@@ -53,32 +54,40 @@ function printWelcome() {
|
|
|
53
54
|
console.log('');
|
|
54
55
|
}
|
|
55
56
|
|
|
56
|
-
//
|
|
57
|
-
//
|
|
57
|
+
// Boot the local model. On first run (model not installed) this RUNS the setup
|
|
58
|
+
// — download + key prompt + decrypt + merge — then starts the model. Returns
|
|
59
|
+
// true if AI is available (local model ready, or a cloud provider configured).
|
|
58
60
|
async function initUltron() {
|
|
59
61
|
if (!ENABLE_LOCAL_AI) {
|
|
60
62
|
console.log(' ' + col(C.dim, 'Ultron n0-beta is disabled (ENABLE_LOCAL_AI=false) — using cloud fallback if configured.\n'));
|
|
61
|
-
return;
|
|
63
|
+
return aiStatus().anyAvailable;
|
|
62
64
|
}
|
|
65
|
+
|
|
66
|
+
// First-time setup: install the model (prompts for the decryption key).
|
|
63
67
|
if (!model.isReady()) {
|
|
64
|
-
console.log(' ' + col(C.
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
+
console.log(' ' + col(C.mag, '✦ First-time setup') + col(C.dim, ` — installing ${model.MODEL_NAME}. This runs once.\n`));
|
|
69
|
+
try {
|
|
70
|
+
await require('../scripts/setup-model').setup();
|
|
71
|
+
} catch (e) {
|
|
72
|
+
const msg = String(e.message || e);
|
|
73
|
+
if (msg.startsWith('WRONG_KEY:')) console.log('\n ' + col(C.red, '❌ ' + msg.replace('WRONG_KEY: ', '')));
|
|
74
|
+
else console.log('\n ' + col(C.red, '❌ Setup failed: ' + msg));
|
|
75
|
+
}
|
|
76
|
+
console.log('');
|
|
68
77
|
}
|
|
69
78
|
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
console.log(' ' + col(C.
|
|
78
|
-
|
|
79
|
-
console.log(' ' + col(C.yel, '… still loading') + col(C.dim, ' — weights load in the background (first start can take ~20s).'));
|
|
79
|
+
if (model.isReady()) {
|
|
80
|
+
process.stdout.write(' ' + col(C.mag, '✦ Ultron n0-beta') + col(C.dim, ' initializing'));
|
|
81
|
+
startLlamafile();
|
|
82
|
+
let ok = false;
|
|
83
|
+
for (let i = 0; i < 8 && !ok; i++) { process.stdout.write(col(C.dim, '.')); ok = await isLlamaHealthy(); if (!ok) await sleep(500); }
|
|
84
|
+
process.stdout.write('\n');
|
|
85
|
+
if (ok) console.log(' ' + col(C.grn, '✓ ready') + col(C.dim, ` — ${model.MODEL_NAME} running locally on :${LOCAL_AI_PORT} (${model.sizeGB()} GB, private)`));
|
|
86
|
+
else console.log(' ' + col(C.yel, '… still loading') + col(C.dim, ' — weights load in the background (first start can take ~20s).'));
|
|
87
|
+
console.log(' ' + col(C.dim, model.DISCLAIMER.split(' — ')[0] + ' — treat suggestions as hints.\n'));
|
|
80
88
|
}
|
|
81
|
-
|
|
89
|
+
|
|
90
|
+
return aiStatus().anyAvailable; // local ready OR cloud key present
|
|
82
91
|
}
|
|
83
92
|
|
|
84
93
|
// ── Interactive startup ───────────────────────────────────────────────────────
|
|
@@ -569,9 +578,16 @@ server.on('error', (err) => {
|
|
|
569
578
|
// ── Startup ───────────────────────────────────────────────────────────────────
|
|
570
579
|
async function main() {
|
|
571
580
|
printWelcome();
|
|
572
|
-
//
|
|
573
|
-
|
|
574
|
-
|
|
581
|
+
// First-run: install + start the model. Returns true only if AI is available.
|
|
582
|
+
const aiReady = await initUltron();
|
|
583
|
+
|
|
584
|
+
// AI is required to analyze — don't show the menu without it.
|
|
585
|
+
if (!aiReady) {
|
|
586
|
+
console.log(' ' + col(C.red, '⚠ AI is required to run analysis, and it isn\'t set up yet.'));
|
|
587
|
+
console.log(' ' + col(C.dim, `Re-run ${col(C.cyan, 'letfixit')} and paste your ${model.MODEL_NAME} key when prompted,`));
|
|
588
|
+
console.log(' ' + col(C.dim, `or add a cloud key (GROQ/GEMINI/CLAUDE) to .env, then start again.\n`));
|
|
589
|
+
process.exit(0);
|
|
590
|
+
}
|
|
575
591
|
|
|
576
592
|
const setup = await interactiveSetup();
|
|
577
593
|
|
|
@@ -616,4 +632,39 @@ function shutdown() {
|
|
|
616
632
|
process.on('SIGINT', shutdown);
|
|
617
633
|
process.on('SIGTERM', shutdown);
|
|
618
634
|
|
|
619
|
-
|
|
635
|
+
// ── CLI dispatch ────────────────────────────────────────────────────────────
|
|
636
|
+
function printHelp() {
|
|
637
|
+
const v = require('../package.json').version;
|
|
638
|
+
console.log(`
|
|
639
|
+
${col(C.bold + C.mag, 'LetFixIt')} v${v} ${col(C.dim, '— AI-based realtime performance analysis for Flutter, Android & iOS')}
|
|
640
|
+
|
|
641
|
+
${col(C.bold, 'Usage')}
|
|
642
|
+
${col(C.cyan, 'letfixit')} Launch the analyzer (first run installs ${model.MODEL_NAME})
|
|
643
|
+
${col(C.cyan, 'letfixit setup')} Download & set up the model (prompts for the decryption key)
|
|
644
|
+
${col(C.cyan, 'letfixit setup --force')} Re-download / reinstall the model
|
|
645
|
+
${col(C.cyan, 'letfixit version')} Print the installed version (or --version / -v)
|
|
646
|
+
${col(C.cyan, 'letfixit --help')} Show this help
|
|
647
|
+
|
|
648
|
+
${col(C.bold, 'Environment')}
|
|
649
|
+
${col(C.dim, 'PARITYFIX_MODEL_KEY')} decryption key for non-interactive / CI installs
|
|
650
|
+
${col(C.dim, 'DASHBOARD_PORT')} dashboard port (default 8081)
|
|
651
|
+
${col(C.dim, 'LOCAL_AI_PORT')} local model port (default 8080)
|
|
652
|
+
${col(C.dim, 'ENABLE_LOCAL_AI')} set to "false" to use cloud fallback only
|
|
653
|
+
`);
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
const cmd = process.argv[2];
|
|
657
|
+
if (cmd === '--version' || cmd === '-v') {
|
|
658
|
+
console.log(require('../package.json').version); // bare number (scripts)
|
|
659
|
+
} else if (cmd === 'version') {
|
|
660
|
+
console.log(`letfixit v${require('../package.json').version}`); // friendly line
|
|
661
|
+
} else if (cmd === '--help' || cmd === '-h' || cmd === 'help') {
|
|
662
|
+
printHelp();
|
|
663
|
+
} else if (cmd === 'setup') {
|
|
664
|
+
// Explicit setup: run the installer directly and exit (like `npm run setup`).
|
|
665
|
+
require('../scripts/setup-model').setup()
|
|
666
|
+
.then(() => process.exit(0))
|
|
667
|
+
.catch((e) => { console.error(' ❌ setup failed:', e.message); process.exit(1); });
|
|
668
|
+
} else {
|
|
669
|
+
main(); // default: launch the analyzer
|
|
670
|
+
}
|