letfixit 0.1.0 → 0.1.1

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 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
- # Decryption passphrase for the model chunks. NEVER commit this set it locally
21
- # and store it as a GitHub Actions secret for CI releases. Without it, `npm run
22
- # setup` skips the automatic download and prints instructions.
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 # or: npx letfixit
14
- export PARITYFIX_MODEL_KEY=… # the decryption passphrase (enables the local model)
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,6 +1,6 @@
1
1
  {
2
2
  "name": "letfixit",
3
- "version": "0.1.0",
3
+ "version": "0.1.1",
4
4
  "description": "LetFixIt — AI-based realtime performance analysis & suggestions for Flutter, Android & iOS, powered by the local Ultron n0-beta model.",
5
5
  "keywords": ["flutter", "android", "ios", "performance", "profiler", "devtools", "observability", "llm", "local-ai"],
6
6
  "license": "MIT",
@@ -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
- const key = process.env.PARITYFIX_MODEL_KEY;
190
- if (!manifest || !key) {
191
- console.log(c(C.yel, ' ⚠️ Model chunks not configured skipping automatic download.\n'));
192
- if (!manifest) console.log(` • No chunk manifest found. Copy ${c(C.cyan, 'config/model.manifest.example.json')} → ${c(C.cyan, 'config/model.manifest.json')} and fill in the chunk URLs.`);
193
- if (!key) console.log(` • Set ${c(C.cyan, 'PARITYFIX_MODEL_KEY')} (the decryption passphrase) in your environment.`);
194
- console.log(`\n Then run ${c(C.cyan, 'npm run setup')} again. (You can also use the legacy ${c(C.cyan, './setup.sh')} to pull from HuggingFace.)\n`);
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
- await decryptChunk(encPath, decPath, derivedKey, ch.iv, ch.authTag);
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
- console.log('\n' + c(C.red, ` ❌ Model setup failed: ${err.message}`));
264
- console.log(c(C.dim, ' You can retry with `npm run setup`, or use ./setup.sh for the HuggingFace source.\n'));
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
  }
@@ -29,6 +29,7 @@ function isReady() {
29
29
  catch { return false; }
30
30
  }
31
31
 
32
+
32
33
  function sizeGB() {
33
34
  try { return parseFloat((fs.statSync(MODEL_PATH).size / (1024 ** 3)).toFixed(2)); }
34
35
  catch { return 0; }