@lovrozagar/grunt 0.9.4 → 0.10.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/scripts/setup.mjs CHANGED
@@ -5,38 +5,28 @@ import os from "node:os";
5
5
  import path from "node:path";
6
6
  import { fileURLToPath, pathToFileURL } from "node:url";
7
7
  import { isInteractive as ttyInteractive } from "./interactive.mjs";
8
+ import { selectedPacks } from "./feature-packs.mjs";
8
9
  import {
9
10
  CHROMIUM_BINS,
10
11
  installHints,
12
+ jevDoctorStatus,
11
13
  speakDoctorStatus,
12
14
  whichBin,
13
15
  workspaceDoctorStatus,
14
16
  } from "./doctor.mjs";
15
- import {
16
- loginLoopback,
17
- openUrl as openWorkspaceUrl,
18
- setAccount,
19
- tokenStorePath,
20
- } from "./google-workspace.mjs";
21
- import {
22
- downloadGgml,
23
- findWhisperBin,
24
- loadCfg as loadListenCfg,
25
- probeStt,
26
- } from "./listen.mjs";
27
- import { loadCfg as loadSpeakCfg, whoami as speakWhoami } from "./speak.mjs";
28
-
29
- export const TARGETS = ["speak", "listen", "google-workspace", "browser"];
30
- export const USAGE = `usage: setup [speak|listen|google-workspace|browser]
17
+
18
+ export const TARGETS = ["speak", "listen", "google-workspace", "browser", "jev"];
19
+ export const USAGE = `usage: setup [speak|listen|google-workspace|browser|jev]
31
20
  speak --elevenlabs-key KEY | --openai-key KEY [--provider elevenlabs|openai] [--skip-verify]
32
21
  listen [--openai-key KEY] [--device ID] [--skip-download]
33
22
  google-workspace [--creds PATH] [--account NAME] [--project ID]
34
23
  browser (engine install hints; no secrets)
24
+ jev --api-key KEY
35
25
  --redo redo a target that is already ok
36
26
 
37
27
  TTY menu shows ok/missing. Missing bins: print OS command, then re-check PATH. Never installs.
38
- Already ok: confirm redo (TTY) unless --redo. Ends with a four-line status (not a README).
39
- Secrets: ~/.grunt/ (not git). Env still wins over the file.
28
+ Already ok: confirm redo (TTY) unless --redo. Ends with one status line per target (not a README).
29
+ Secrets: ~/.grunt/ (not git). Env still wins over the file. Setup does not export keys.
40
30
  `;
41
31
 
42
32
  export const ELEVENLABS_KEY_URL = "https://elevenlabs.io/app/settings/api-keys";
@@ -207,7 +197,7 @@ export function workspaceConsoleUrls(projectId) {
207
197
  };
208
198
  }
209
199
 
210
- export function openUrl(
200
+ export async function openUrl(
211
201
  url,
212
202
  { platform = process.platform, spawnSyncFn, openUrlFn } = {},
213
203
  ) {
@@ -215,6 +205,7 @@ export function openUrl(
215
205
  openUrlFn(url);
216
206
  return;
217
207
  }
208
+ const { openUrl: openWorkspaceUrl } = await import("./google-workspace.mjs");
218
209
  openWorkspaceUrl(url, { platform, spawnSyncFn });
219
210
  }
220
211
 
@@ -243,46 +234,60 @@ export function browserEngineStatus(lp, cr, platform = process.platform) {
243
234
  return { ok: false, extra: have.concat(need).join(",") };
244
235
  }
245
236
 
246
- export function targetStatuses({
237
+ export function setupTargets(packs = []) {
238
+ const on = new Set(packs);
239
+ return TARGETS.filter((target) => target === "browser" || target === "jev" || on.has(target));
240
+ }
241
+
242
+ export async function targetStatuses({
247
243
  home = os.homedir(),
248
244
  env = process.env,
249
245
  whichFn = whichBin,
250
246
  pathEnv,
251
247
  platform = process.platform,
248
+ targets = TARGETS,
252
249
  } = {}) {
253
250
  const pEnv = pathEnv || env.PATH || process.env.PATH;
254
251
  const speak = speakDoctorStatus({ env, home });
252
+ const jev = jevDoctorStatus({ env, home });
255
253
  const ws = workspaceDoctorStatus({ home });
256
- const cfg = loadListenCfg({ env, home });
257
- const stt = probeStt({ cfg, whichFn, pathEnv: pEnv, platform, home, cwd: "" });
258
- const ffmpeg = whichFn("ffmpeg", pEnv, platform);
259
- const whisper = findWhisperBin(whichFn, pEnv, platform);
260
- const listenBits = [stt && stt.kind, ffmpeg && "ffmpeg", whisper && "whisper"].filter(Boolean);
261
254
  const lp = whichFn("lightpanda", pEnv, platform);
262
255
  const cr = lookupChromium(whichFn, pEnv, platform);
256
+ let listen = { ok: false, extra: "" };
257
+ if (targets.includes("listen")) {
258
+ const { loadCfg, probeStt, findWhisperBin } = await import("./listen.mjs");
259
+ const cfg = loadCfg({ env, home });
260
+ const stt = probeStt({ cfg, whichFn, pathEnv: pEnv, platform, home, cwd: "" });
261
+ const ffmpeg = whichFn("ffmpeg", pEnv, platform);
262
+ const whisper = findWhisperBin(whichFn, pEnv, platform);
263
+ const listenBits = [stt && stt.kind, ffmpeg && "ffmpeg", whisper && "whisper"].filter(Boolean);
264
+ listen = { ok: Boolean(stt), extra: listenBits.join(",") };
265
+ }
263
266
  return {
264
267
  speak: { ok: speak.ok, extra: speak.extra },
265
- listen: { ok: Boolean(stt), extra: listenBits.join(",") },
268
+ jev: { ok: jev.ok, extra: jev.extra },
269
+ listen,
266
270
  "google-workspace": { ok: ws.ok, extra: ws.extra },
267
271
  browser: browserEngineStatus(lp, cr, platform),
268
272
  };
269
273
  }
270
274
 
271
- export function formatStatus(statuses) {
272
- return TARGETS.map((t) => {
275
+ export function formatStatus(statuses, targets = TARGETS) {
276
+ return targets.map((t) => {
273
277
  const st = (statuses && statuses[t]) || { ok: false, extra: "" };
274
278
  const b = st.ok ? "ok" : "missing";
275
279
  return st.extra ? `${t} ${b} ${st.extra}` : `${t} ${b}`;
276
280
  }).join("\n");
277
281
  }
278
282
 
279
- export function menuOptions(statuses) {
283
+ export function menuOptions(statuses, targets = TARGETS) {
280
284
  const base = [
281
285
  { value: "speak", label: "speak (ElevenLabs / OpenAI TTS)" },
282
286
  { value: "listen", label: "listen (STT)" },
283
287
  { value: "google-workspace", label: "google-workspace" },
284
288
  { value: "browser", label: "browser (PATH engines)" },
285
- ];
289
+ { value: "jev", label: "jev (TypeSafe research decision)" },
290
+ ].filter((o) => targets.includes(o.value));
286
291
  return base.map((o) => {
287
292
  const st = (statuses && statuses[o.value]) || { ok: false, extra: "" };
288
293
  const tag = st.ok
@@ -296,11 +301,11 @@ export function menuOptions(statuses) {
296
301
  });
297
302
  }
298
303
 
299
- export function firstMissing(statuses) {
300
- for (const t of TARGETS) {
304
+ export function firstMissing(statuses, targets = TARGETS) {
305
+ for (const t of targets) {
301
306
  if (!statuses || !statuses[t] || !statuses[t].ok) return t;
302
307
  }
303
- return "speak";
308
+ return targets[0] || "browser";
304
309
  }
305
310
 
306
311
  export async function reprobeHint({
@@ -374,6 +379,7 @@ export async function setupSpeak({
374
379
  spawnSyncFn,
375
380
  interactive = false,
376
381
  } = {}) {
382
+ const { loadCfg: loadSpeakCfg, whoami: speakWhoami } = await import("./speak.mjs");
377
383
  let elevenKey = str(flags["elevenlabs-key"] || flags.elevenlabs);
378
384
  let openaiKey = str(flags["openai-key"] || flags.openai);
379
385
  let provider = str(flags.provider).toLowerCase();
@@ -399,12 +405,12 @@ export async function setupSpeak({
399
405
  if (interactive && io) {
400
406
  if (askEleven && !elevenKey) {
401
407
  logLine(io, `Open: ${ELEVENLABS_KEY_URL}`);
402
- openUrl(ELEVENLABS_KEY_URL, { platform, spawnSyncFn, openUrlFn });
408
+ await openUrl(ELEVENLABS_KEY_URL, { platform, spawnSyncFn, openUrlFn });
403
409
  elevenKey = str(await io.password({ message: "ElevenLabs API key" }));
404
410
  }
405
411
  if (askOpenAi && !openaiKey) {
406
412
  logLine(io, `Open: ${OPENAI_KEY_URL}`);
407
- openUrl(OPENAI_KEY_URL, { platform, spawnSyncFn, openUrlFn });
413
+ await openUrl(OPENAI_KEY_URL, { platform, spawnSyncFn, openUrlFn });
408
414
  openaiKey = str(await io.password({ message: "OpenAI API key" }));
409
415
  }
410
416
  }
@@ -453,6 +459,7 @@ export async function setupListen({
453
459
  pathEnv = process.env.PATH,
454
460
  interactive = false,
455
461
  } = {}) {
462
+ const { downloadGgml, findWhisperBin, loadCfg: loadListenCfg, probeStt } = await import("./listen.mjs");
456
463
  const ffmpegBin = await reprobeHint({
457
464
  id: "ffmpeg",
458
465
  found: whichFn("ffmpeg", pathEnv, platform),
@@ -491,7 +498,7 @@ export async function setupListen({
491
498
  if (!whisperBin && !cfgNow.apiKey && !openaiKey) {
492
499
  if (interactive && io) {
493
500
  logLine(io, `Local STT is unavailable. OpenAI Whisper fallback: ${OPENAI_KEY_URL}`);
494
- openUrl(OPENAI_KEY_URL, { platform, spawnSyncFn, openUrlFn });
501
+ await openUrl(OPENAI_KEY_URL, { platform, spawnSyncFn, openUrlFn });
495
502
  openaiKey = str(await io.password({ message: "OpenAI API key (fallback STT)" }));
496
503
  } else {
497
504
  throw new Error(
@@ -540,6 +547,7 @@ export async function setupGoogleWorkspace({
540
547
  interactive = false,
541
548
  loginFn,
542
549
  } = {}) {
550
+ const { loginLoopback, setAccount, tokenStorePath } = await import("./google-workspace.mjs");
543
551
  const prevHome = process.env.HOME;
544
552
  const prevProfile = process.env.USERPROFILE;
545
553
  process.env.HOME = home;
@@ -556,7 +564,7 @@ export async function setupGoogleWorkspace({
556
564
  const urls0 = workspaceConsoleUrls(projectId);
557
565
  logLine(io, "Each person uses their own GCP Desktop OAuth client. Do not share JSON or tokens.");
558
566
  logLine(io, `Open: ${urls0.projectCreate}`);
559
- openUrl(urls0.projectCreate, { platform, spawnSyncFn, openUrlFn });
567
+ await openUrl(urls0.projectCreate, { platform, spawnSyncFn, openUrlFn });
560
568
  if (typeof io.confirm === "function") {
561
569
  await io.confirm({ message: "GCP project created? Continue", initialValue: true });
562
570
  }
@@ -572,18 +580,18 @@ export async function setupGoogleWorkspace({
572
580
  const urls = workspaceConsoleUrls(projectId);
573
581
  logLine(io, "Consent: Internal on paid Workspace, else Testing + add your email as a test user.");
574
582
  logLine(io, `Open: ${urls.branding}`);
575
- openUrl(urls.branding, { platform, spawnSyncFn, openUrlFn });
583
+ await openUrl(urls.branding, { platform, spawnSyncFn, openUrlFn });
576
584
  if (typeof io.confirm === "function") {
577
585
  await io.confirm({ message: "Branding / audience done? Continue", initialValue: true });
578
586
  }
579
587
  logLine(io, `Open: ${urls.enableApis}`);
580
- openUrl(urls.enableApis, { platform, spawnSyncFn, openUrlFn });
588
+ await openUrl(urls.enableApis, { platform, spawnSyncFn, openUrlFn });
581
589
  if (typeof io.confirm === "function") {
582
590
  await io.confirm({ message: "APIs enabled? Continue", initialValue: true });
583
591
  }
584
592
  logLine(io, "Create OAuth client: application type Desktop app → Download JSON.");
585
593
  logLine(io, `Open: ${urls.clientCreate}`);
586
- openUrl(urls.clientCreate, { platform, spawnSyncFn, openUrlFn });
594
+ await openUrl(urls.clientCreate, { platform, spawnSyncFn, openUrlFn });
587
595
  if (typeof io.confirm === "function") {
588
596
  await io.confirm({ message: "JSON downloaded? Continue", initialValue: true });
589
597
  }
@@ -655,11 +663,35 @@ export async function setupBrowser({
655
663
  return { ok: st.ok, lightpanda: lp, chromium };
656
664
  }
657
665
 
666
+ export async function setupJev({
667
+ flags = {},
668
+ home = os.homedir(),
669
+ io,
670
+ interactive = false,
671
+ } = {}) {
672
+ let apiKey = str(flags["api-key"] || flags.key);
673
+ if (interactive && io && !apiKey && typeof io.password === "function") {
674
+ logLine(io, "Open: https://console.typesafe.ai/keys");
675
+ apiKey = str(await io.password({ message: "TypeSafe API key" }));
676
+ }
677
+ if (!apiKey) {
678
+ throw new Error(
679
+ interactive
680
+ ? "need a TypeSafe API key"
681
+ : "need --api-key (or TTY: node scripts/setup.mjs jev)",
682
+ );
683
+ }
684
+ const dest = writeSecretJson(path.join(home, ".grunt", "jev.json"), { apiKey });
685
+ logLine(io, `wrote ${dest}`);
686
+ return { ok: true, dest };
687
+ }
688
+
658
689
  async function runTarget(target, ctx) {
659
690
  if (target === "speak") return setupSpeak(ctx);
660
691
  if (target === "listen") return setupListen(ctx);
661
692
  if (target === "google-workspace") return setupGoogleWorkspace(ctx);
662
693
  if (target === "browser") return setupBrowser(ctx);
694
+ if (target === "jev") return setupJev(ctx);
663
695
  throw new Error(`unknown target ${target}`);
664
696
  }
665
697
 
@@ -667,7 +699,8 @@ export async function main(argv = process.argv.slice(2), opts = {}) {
667
699
  const { _, flags } = parseArgv(argv);
668
700
  const env = opts.env || process.env;
669
701
  const home = opts.home || env.HOME || env.USERPROFILE || os.homedir();
670
- const io = opts.io || (await defaultIo());
702
+ const cwd = opts.cwd || process.cwd();
703
+ const targets = setupTargets(selectedPacks(cwd));
671
704
  const interactive =
672
705
  opts.interactive != null
673
706
  ? Boolean(opts.interactive)
@@ -677,6 +710,7 @@ export async function main(argv = process.argv.slice(2), opts = {}) {
677
710
  stdin: opts.stdin || process.stdin,
678
711
  stdout: opts.stdout || process.stdout,
679
712
  });
713
+ const io = opts.io || (interactive ? await defaultIo() : null);
680
714
  const ctx = {
681
715
  flags,
682
716
  env,
@@ -699,14 +733,18 @@ export async function main(argv = process.argv.slice(2), opts = {}) {
699
733
  pathEnv: ctx.pathEnv,
700
734
  platform: ctx.platform,
701
735
  };
702
- let statuses = targetStatuses(statusOpts);
736
+ let statuses = await targetStatuses({ ...statusOpts, targets });
703
737
  let target = normalizeTarget(_[0]);
738
+ if (target && !targets.includes(target)) {
739
+ errLine(io, `${target} is off. Enable it with \`grunt init\` or \`grunt upgrade\`.`);
740
+ return 1;
741
+ }
704
742
  if (!target && interactive && io && typeof io.select === "function") {
705
743
  target = normalizeTarget(
706
744
  await io.select({
707
745
  message: "Setup",
708
- options: menuOptions(statuses),
709
- initialValue: firstMissing(statuses),
746
+ options: menuOptions(statuses, targets),
747
+ initialValue: firstMissing(statuses, targets),
710
748
  }),
711
749
  );
712
750
  }
@@ -725,13 +763,13 @@ export async function main(argv = process.argv.slice(2), opts = {}) {
725
763
  });
726
764
  if (!again) {
727
765
  logLine(io, `skip ${target} (already ok)`);
728
- logLine(io, formatStatus(statuses));
766
+ logLine(io, formatStatus(statuses, targets));
729
767
  return 0;
730
768
  }
731
769
  }
732
770
  await runTarget(target, ctx);
733
- statuses = targetStatuses(statusOpts);
734
- logLine(io, formatStatus(statuses));
771
+ statuses = await targetStatuses({ ...statusOpts, targets });
772
+ logLine(io, formatStatus(statuses, targets));
735
773
  return 0;
736
774
  } catch (e) {
737
775
  errLine(io, String(e && e.message ? e.message : e));
package/scripts/speak.mjs CHANGED
@@ -5,6 +5,7 @@ import fs from "node:fs";
5
5
  import os from "node:os";
6
6
  import path from "node:path";
7
7
  import { fileURLToPath, pathToFileURL } from "node:url";
8
+ import { loadOptionalConsumerEnv } from "./doctor.mjs";
8
9
 
9
10
  export const DEFAULT_ELEVEN_VOICE = "JBFqnCBsd6RMkjVDRZzb";
10
11
  export const DEFAULT_ELEVEN_MODEL = "eleven_multilingual_v2";
@@ -246,6 +247,7 @@ export async function say({
246
247
 
247
248
  export async function main(argv = process.argv.slice(2), opts = {}) {
248
249
  const { _, flags } = parseArgv(argv);
250
+ if (!opts.env) await loadOptionalConsumerEnv();
249
251
  const env = opts.env || process.env;
250
252
  const home = opts.home || os.homedir();
251
253
  const cwd = opts.cwd || process.cwd();