@lovrozagar/grunt 0.5.2 → 0.6.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/.agents/agents/grunt.md +2 -24
- package/.agents/agents/orchestrator.md +5 -50
- package/.agents/skills/ask/SKILL.md +5 -12
- package/.agents/skills/auto/SKILL.md +7 -12
- package/.agents/skills/browser/SKILL.md +4 -8
- package/.agents/skills/clasp/SKILL.md +11 -0
- package/.agents/skills/commit/SKILL.md +3 -1
- package/.agents/skills/commit-push/SKILL.md +2 -0
- package/.agents/skills/commit-push-deploy/SKILL.md +2 -0
- package/.agents/skills/commit-push-release/SKILL.md +2 -0
- package/.agents/skills/explain/SKILL.md +7 -13
- package/.agents/skills/google-workspace/SKILL.md +13 -0
- package/.agents/skills/handoff/SKILL.md +7 -11
- package/.agents/skills/implement-plan/SKILL.md +5 -109
- package/.agents/skills/listen/SKILL.md +15 -0
- package/.agents/skills/pickup/SKILL.md +6 -12
- package/.agents/skills/speak/SKILL.md +13 -0
- package/.agents/skills/tmp/SKILL.md +6 -10
- package/.agents/skills/write-plan/SKILL.md +5 -77
- package/.claude/agents/grunt.md +2 -26
- package/.claude/agents/orchestrator.md +12 -50
- package/.claude/skills/ask/SKILL.md +5 -12
- package/.claude/skills/auto/SKILL.md +7 -12
- package/.claude/skills/browser/SKILL.md +4 -8
- package/.claude/skills/clasp/SKILL.md +11 -0
- package/.claude/skills/commit/SKILL.md +3 -1
- package/.claude/skills/commit-push/SKILL.md +2 -0
- package/.claude/skills/commit-push-deploy/SKILL.md +2 -0
- package/.claude/skills/commit-push-release/SKILL.md +2 -0
- package/.claude/skills/explain/SKILL.md +7 -13
- package/.claude/skills/google-workspace/SKILL.md +13 -0
- package/.claude/skills/handoff/SKILL.md +7 -11
- package/.claude/skills/implement-plan/SKILL.md +5 -109
- package/.claude/skills/listen/SKILL.md +15 -0
- package/.claude/skills/pickup/SKILL.md +6 -12
- package/.claude/skills/speak/SKILL.md +13 -0
- package/.claude/skills/tmp/SKILL.md +6 -10
- package/.claude/skills/write-plan/SKILL.md +5 -77
- package/.codex/agents/grunt.toml +2 -26
- package/.codex/agents/orchestrator.toml +4 -50
- package/.grok/agents/grunt.md +2 -24
- package/.grok/agents/orchestrator.md +4 -48
- package/.grok/global-settings.toml +1 -3
- package/.grok/hooks/orchestrate-parent.js +190 -358
- package/.grok/skills/ask/SKILL.md +5 -12
- package/.grok/skills/auto/SKILL.md +7 -12
- package/.grok/skills/browser/SKILL.md +4 -8
- package/.grok/skills/clasp/SKILL.md +11 -0
- package/.grok/skills/commit/SKILL.md +3 -1
- package/.grok/skills/commit-push/SKILL.md +2 -0
- package/.grok/skills/commit-push-deploy/SKILL.md +2 -0
- package/.grok/skills/commit-push-release/SKILL.md +2 -0
- package/.grok/skills/explain/SKILL.md +7 -13
- package/.grok/skills/google-workspace/SKILL.md +13 -0
- package/.grok/skills/handoff/SKILL.md +7 -11
- package/.grok/skills/implement-plan/SKILL.md +5 -109
- package/.grok/skills/listen/SKILL.md +15 -0
- package/.grok/skills/pickup/SKILL.md +6 -12
- package/.grok/skills/speak/SKILL.md +13 -0
- package/.grok/skills/tmp/SKILL.md +6 -10
- package/.grok/skills/write-plan/SKILL.md +5 -77
- package/.rulesync/hooks.jsonc +1 -1
- package/.rulesync/mcp-policy.jsonc +0 -2
- package/.rulesync/reference/INDEX.md +30 -20
- package/.rulesync/reference/browser.md +13 -6
- package/.rulesync/reference/cascade.md +6 -86
- package/.rulesync/reference/clasp.md +21 -0
- package/.rulesync/reference/google-workspace.md +79 -0
- package/.rulesync/reference/hooks.md +7 -11
- package/.rulesync/reference/implementation-format.md +113 -0
- package/.rulesync/reference/law.md +5 -3
- package/.rulesync/reference/listen.md +80 -0
- package/.rulesync/reference/map.md +40 -36
- package/.rulesync/reference/output.md +3 -51
- package/.rulesync/reference/plan-format.md +4 -2
- package/.rulesync/reference/refs-map.md +12 -5
- package/.rulesync/reference/rtk.md +3 -2
- package/.rulesync/reference/scope.md +37 -0
- package/.rulesync/reference/setup.md +27 -0
- package/.rulesync/reference/skills-map.md +13 -12
- package/.rulesync/reference/speak.md +59 -0
- package/.rulesync/rules/CLAUDE.md +3 -49
- package/.rulesync/rules/overview.md +9 -47
- package/.rulesync/skills/ask/SKILL.md +5 -12
- package/.rulesync/skills/auto/SKILL.md +5 -12
- package/.rulesync/skills/browser/SKILL.md +4 -8
- package/.rulesync/skills/clasp/SKILL.md +9 -0
- package/.rulesync/skills/commit/SKILL.md +3 -1
- package/.rulesync/skills/commit-push/SKILL.md +2 -0
- package/.rulesync/skills/commit-push-deploy/SKILL.md +2 -0
- package/.rulesync/skills/commit-push-release/SKILL.md +2 -0
- package/.rulesync/skills/explain/SKILL.md +7 -14
- package/.rulesync/skills/google-workspace/SKILL.md +11 -0
- package/.rulesync/skills/handoff/SKILL.md +7 -11
- package/.rulesync/skills/implement-plan/SKILL.md +5 -119
- package/.rulesync/skills/listen/SKILL.md +13 -0
- package/.rulesync/skills/pickup/SKILL.md +6 -12
- package/.rulesync/skills/speak/SKILL.md +13 -0
- package/.rulesync/skills/tmp/SKILL.md +6 -10
- package/.rulesync/skills/write-plan/SKILL.md +5 -84
- package/.rulesync/subagents/grunt.md +2 -24
- package/.rulesync/subagents/orchestrator.md +14 -59
- package/AGENTS.md +9 -47
- package/CHANGELOG.md +67 -0
- package/CLAUDE.md +1 -49
- package/README.md +89 -80
- package/cli/grunt.mjs +21 -3
- package/cli/grunt.test.ts +47 -3
- package/cli/init.mjs +134 -234
- package/cli/init.test.ts +241 -58
- package/cli/prompt.mjs +9 -36
- package/cli/prompt.test.ts +14 -0
- package/package.json +10 -2
- package/scripts/browser.mjs +128 -7
- package/scripts/doctor.mjs +104 -10
- package/scripts/emit-gemini.mjs +2 -2
- package/scripts/emit-mcp-policy.mjs +1 -1
- package/scripts/gate-fat-tools.mjs +223 -173
- package/scripts/google-workspace.mjs +713 -0
- package/scripts/grunt-job.mjs +453 -69
- package/scripts/guarded-md.mjs +230 -0
- package/scripts/guarded-roots.mjs +1 -1
- package/scripts/interactive.mjs +15 -0
- package/scripts/listen.mjs +829 -0
- package/scripts/parse-need.mjs +21 -2
- package/scripts/persist-implementation.mjs +278 -0
- package/scripts/prompt.mjs +32 -0
- package/scripts/scrub-spawn-prompt.mjs +3 -50
- package/scripts/setup.mjs +746 -0
- package/scripts/speak.mjs +291 -0
- package/.agents/agents/implementer.md +0 -32
- package/.agents/agents/thinker.md +0 -29
- package/.agents/skills/cascade/SKILL.md +0 -28
- package/.agents/skills/parent/SKILL.md +0 -6
- package/.agents/skills/solo/SKILL.md +0 -25
- package/.claude/agents/implementer.md +0 -37
- package/.claude/agents/thinker.md +0 -36
- package/.claude/skills/cascade/SKILL.md +0 -28
- package/.claude/skills/parent/SKILL.md +0 -6
- package/.claude/skills/solo/SKILL.md +0 -25
- package/.codex/agents/implementer.toml +0 -26
- package/.codex/agents/thinker.toml +0 -26
- package/.grok/agents/implementer.md +0 -27
- package/.grok/agents/thinker.md +0 -27
- package/.grok/parent.md +0 -1
- package/.grok/roles/implementer.toml +0 -5
- package/.grok/roles/thinker.toml +0 -5
- package/.grok/skills/cascade/SKILL.md +0 -28
- package/.grok/skills/parent/SKILL.md +0 -6
- package/.grok/skills/solo/SKILL.md +0 -25
- package/.rulesync/grunt.config.jsonc +0 -7
- package/.rulesync/skills/cascade/SKILL.md +0 -28
- package/.rulesync/skills/parent/SKILL.md +0 -6
- package/.rulesync/skills/solo/SKILL.md +0 -25
- package/.rulesync/subagents/implementer.md +0 -49
- package/.rulesync/subagents/thinker.md +0 -49
- package/scripts/grunt-config.mjs +0 -50
|
@@ -0,0 +1,746 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/** Handheld machine setup for optional grunt integrations. Secrets stay in ~/.grunt/. */
|
|
3
|
+
import fs from "node:fs";
|
|
4
|
+
import os from "node:os";
|
|
5
|
+
import path from "node:path";
|
|
6
|
+
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
7
|
+
import { isInteractive as ttyInteractive } from "./interactive.mjs";
|
|
8
|
+
import {
|
|
9
|
+
CHROMIUM_BINS,
|
|
10
|
+
installHints,
|
|
11
|
+
speakDoctorStatus,
|
|
12
|
+
whichBin,
|
|
13
|
+
workspaceDoctorStatus,
|
|
14
|
+
} 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]
|
|
31
|
+
speak --elevenlabs-key KEY | --openai-key KEY [--provider elevenlabs|openai] [--skip-verify]
|
|
32
|
+
listen [--openai-key KEY] [--device ID] [--skip-download]
|
|
33
|
+
google-workspace [--creds PATH] [--account NAME] [--project ID]
|
|
34
|
+
browser (engine install hints; no secrets)
|
|
35
|
+
--redo redo a target that is already ok
|
|
36
|
+
|
|
37
|
+
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.
|
|
40
|
+
`;
|
|
41
|
+
|
|
42
|
+
export const ELEVENLABS_KEY_URL = "https://elevenlabs.io/app/settings/api-keys";
|
|
43
|
+
export const OPENAI_KEY_URL = "https://platform.openai.com/api-keys";
|
|
44
|
+
|
|
45
|
+
export function parseArgv(argv) {
|
|
46
|
+
const out = { _: [], flags: {} };
|
|
47
|
+
const args = Array.isArray(argv) ? argv : [];
|
|
48
|
+
for (let i = 0; i < args.length; i++) {
|
|
49
|
+
const a = args[i];
|
|
50
|
+
if (a === "--") {
|
|
51
|
+
out._.push(...args.slice(i + 1));
|
|
52
|
+
break;
|
|
53
|
+
}
|
|
54
|
+
if (typeof a === "string" && a.startsWith("--")) {
|
|
55
|
+
const eq = a.indexOf("=");
|
|
56
|
+
if (eq > 2) {
|
|
57
|
+
out.flags[a.slice(2, eq)] = a.slice(eq + 1);
|
|
58
|
+
continue;
|
|
59
|
+
}
|
|
60
|
+
const key = a.slice(2);
|
|
61
|
+
const nxt = args[i + 1];
|
|
62
|
+
if (nxt && !String(nxt).startsWith("--")) {
|
|
63
|
+
out.flags[key] = nxt;
|
|
64
|
+
i += 1;
|
|
65
|
+
} else out.flags[key] = "1";
|
|
66
|
+
} else out._.push(a);
|
|
67
|
+
}
|
|
68
|
+
return out;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export function normalizeTarget(raw) {
|
|
72
|
+
const s = String(raw || "").trim().toLowerCase();
|
|
73
|
+
if (!s) return "";
|
|
74
|
+
if (s === "gw" || s === "google" || s === "workspace" || s === "google-workspace") {
|
|
75
|
+
return "google-workspace";
|
|
76
|
+
}
|
|
77
|
+
if (TARGETS.includes(s)) return s;
|
|
78
|
+
return "";
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function str(v, fallback = "") {
|
|
82
|
+
const s = String(v ?? "").trim();
|
|
83
|
+
return s || fallback;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export function gruntHome(home = os.homedir()) {
|
|
87
|
+
const d = path.join(home, ".grunt");
|
|
88
|
+
fs.mkdirSync(d, { recursive: true, mode: 0o700 });
|
|
89
|
+
try {
|
|
90
|
+
fs.chmodSync(d, 0o700);
|
|
91
|
+
} catch {
|
|
92
|
+
/* win */
|
|
93
|
+
}
|
|
94
|
+
return d;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export function writeSecretJson(p, obj) {
|
|
98
|
+
fs.mkdirSync(path.dirname(p), { recursive: true, mode: 0o700 });
|
|
99
|
+
fs.writeFileSync(p, JSON.stringify(obj, null, 2) + "\n", { mode: 0o600 });
|
|
100
|
+
try {
|
|
101
|
+
fs.chmodSync(p, 0o600);
|
|
102
|
+
} catch {
|
|
103
|
+
/* win */
|
|
104
|
+
}
|
|
105
|
+
return p;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function readJson(p) {
|
|
109
|
+
try {
|
|
110
|
+
return JSON.parse(fs.readFileSync(p, "utf8"));
|
|
111
|
+
} catch {
|
|
112
|
+
return {};
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export function speakJsonPath(home) {
|
|
117
|
+
return path.join(home, ".grunt", "speak.json");
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export function mergeSpeakJson(home, patch) {
|
|
121
|
+
gruntHome(home);
|
|
122
|
+
const p = speakJsonPath(home);
|
|
123
|
+
const cur = readJson(p);
|
|
124
|
+
const next = { ...cur, ...patch };
|
|
125
|
+
if (patch && patch.elevenlabs) {
|
|
126
|
+
next.elevenlabs = { ...(cur.elevenlabs || {}), ...patch.elevenlabs };
|
|
127
|
+
}
|
|
128
|
+
if (patch && patch.openai) {
|
|
129
|
+
next.openai = { ...(cur.openai || {}), ...patch.openai };
|
|
130
|
+
}
|
|
131
|
+
if (patch && patch.listen) {
|
|
132
|
+
next.listen = { ...(cur.listen || {}), ...patch.listen };
|
|
133
|
+
}
|
|
134
|
+
writeSecretJson(p, next);
|
|
135
|
+
return p;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
export function parseDesktopOAuth(raw) {
|
|
139
|
+
const j = typeof raw === "string" ? JSON.parse(raw) : raw;
|
|
140
|
+
const inst = j && (j.installed || j.web);
|
|
141
|
+
if (!inst || !inst.client_id) {
|
|
142
|
+
throw new Error("OAuth JSON needs installed.client_id (Desktop app download)");
|
|
143
|
+
}
|
|
144
|
+
if (!j.installed) {
|
|
145
|
+
throw new Error("OAuth JSON must be a Desktop client (installed), not a Web client");
|
|
146
|
+
}
|
|
147
|
+
return j;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
export function loadOAuthInput(s) {
|
|
151
|
+
const t = String(s || "").trim();
|
|
152
|
+
if (!t) throw new Error("OAuth JSON path or JSON body required");
|
|
153
|
+
if (t.startsWith("{")) return parseDesktopOAuth(t);
|
|
154
|
+
if (!fs.existsSync(t) || !fs.statSync(t).isFile()) {
|
|
155
|
+
throw new Error(`OAuth JSON not found: ${t}`);
|
|
156
|
+
}
|
|
157
|
+
return parseDesktopOAuth(fs.readFileSync(t, "utf8"));
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
export function installOAuthJson({ src, dest }) {
|
|
161
|
+
const j = loadOAuthInput(src);
|
|
162
|
+
writeSecretJson(dest, j);
|
|
163
|
+
return dest;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
export function findDownloadedOAuthJson(home) {
|
|
167
|
+
const dirs = [path.join(home, "Downloads"), path.join(home, "downloads")];
|
|
168
|
+
const hits = [];
|
|
169
|
+
for (const dir of dirs) {
|
|
170
|
+
let ents;
|
|
171
|
+
try {
|
|
172
|
+
ents = fs.readdirSync(dir);
|
|
173
|
+
} catch {
|
|
174
|
+
continue;
|
|
175
|
+
}
|
|
176
|
+
for (const name of ents) {
|
|
177
|
+
if (!/client_secret.*\.json$/i.test(name) && name.toLowerCase() !== "google-oauth.json") {
|
|
178
|
+
continue;
|
|
179
|
+
}
|
|
180
|
+
const p = path.join(dir, name);
|
|
181
|
+
try {
|
|
182
|
+
const st = fs.statSync(p);
|
|
183
|
+
if (st.isFile()) hits.push({ p, m: st.mtimeMs });
|
|
184
|
+
} catch {
|
|
185
|
+
/* skip */
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
hits.sort((a, b) => b.m - a.m);
|
|
190
|
+
return hits[0] ? hits[0].p : "";
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
export function workspaceConsoleUrls(projectId) {
|
|
194
|
+
const p = str(projectId);
|
|
195
|
+
const enc = encodeURIComponent(p);
|
|
196
|
+
return {
|
|
197
|
+
projectCreate: "https://console.cloud.google.com/projectcreate",
|
|
198
|
+
branding: p
|
|
199
|
+
? `https://console.cloud.google.com/auth/overview/create?project=${enc}`
|
|
200
|
+
: "https://console.cloud.google.com/auth/overview/create",
|
|
201
|
+
enableApis: p
|
|
202
|
+
? `https://console.cloud.google.com/flows/enableapi?apiid=calendar-json.googleapis.com,gmail.googleapis.com,sheets.googleapis.com,docs.googleapis.com,drive.googleapis.com&project=${enc}`
|
|
203
|
+
: "https://console.cloud.google.com/flows/enableapi?apiid=calendar-json.googleapis.com,gmail.googleapis.com,sheets.googleapis.com,docs.googleapis.com,drive.googleapis.com",
|
|
204
|
+
clientCreate: p
|
|
205
|
+
? `https://console.cloud.google.com/auth/clients/create?project=${enc}`
|
|
206
|
+
: "https://console.cloud.google.com/auth/clients/create",
|
|
207
|
+
};
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
export function openUrl(
|
|
211
|
+
url,
|
|
212
|
+
{ platform = process.platform, spawnSyncFn, openUrlFn } = {},
|
|
213
|
+
) {
|
|
214
|
+
if (typeof openUrlFn === "function") {
|
|
215
|
+
openUrlFn(url);
|
|
216
|
+
return;
|
|
217
|
+
}
|
|
218
|
+
openWorkspaceUrl(url, { platform, spawnSyncFn });
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
function flagOn(flags, key) {
|
|
222
|
+
const v = flags && flags[key];
|
|
223
|
+
return v === "1" || v === true || v === "true";
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
export function lookupChromium(whichFn = whichBin, pathEnv = process.env.PATH, platform = process.platform) {
|
|
227
|
+
for (const n of CHROMIUM_BINS) {
|
|
228
|
+
const hit = whichFn(n, pathEnv, platform);
|
|
229
|
+
if (hit) return hit;
|
|
230
|
+
}
|
|
231
|
+
return "";
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
export function browserEngineStatus(lp, cr, platform = process.platform) {
|
|
235
|
+
const have = [lp && "lightpanda", cr && "chromium"].filter(Boolean);
|
|
236
|
+
if (lp && cr) return { ok: true, extra: have.join(",") };
|
|
237
|
+
if (platform === "win32" && cr) {
|
|
238
|
+
return { ok: true, extra: have.concat(["need lightpanda in WSL"]).join(",") };
|
|
239
|
+
}
|
|
240
|
+
const need = [];
|
|
241
|
+
if (!lp) need.push("need lightpanda");
|
|
242
|
+
if (!cr) need.push("need chromium");
|
|
243
|
+
return { ok: false, extra: have.concat(need).join(",") };
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
export function targetStatuses({
|
|
247
|
+
home = os.homedir(),
|
|
248
|
+
env = process.env,
|
|
249
|
+
whichFn = whichBin,
|
|
250
|
+
pathEnv,
|
|
251
|
+
platform = process.platform,
|
|
252
|
+
} = {}) {
|
|
253
|
+
const pEnv = pathEnv || env.PATH || process.env.PATH;
|
|
254
|
+
const speak = speakDoctorStatus({ env, home });
|
|
255
|
+
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
|
+
const lp = whichFn("lightpanda", pEnv, platform);
|
|
262
|
+
const cr = lookupChromium(whichFn, pEnv, platform);
|
|
263
|
+
return {
|
|
264
|
+
speak: { ok: speak.ok, extra: speak.extra },
|
|
265
|
+
listen: { ok: Boolean(stt), extra: listenBits.join(",") },
|
|
266
|
+
"google-workspace": { ok: ws.ok, extra: ws.extra },
|
|
267
|
+
browser: browserEngineStatus(lp, cr, platform),
|
|
268
|
+
};
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
export function formatStatus(statuses) {
|
|
272
|
+
return TARGETS.map((t) => {
|
|
273
|
+
const st = (statuses && statuses[t]) || { ok: false, extra: "" };
|
|
274
|
+
const b = st.ok ? "ok" : "missing";
|
|
275
|
+
return st.extra ? `${t} ${b} ${st.extra}` : `${t} ${b}`;
|
|
276
|
+
}).join("\n");
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
export function menuOptions(statuses) {
|
|
280
|
+
const base = [
|
|
281
|
+
{ value: "speak", label: "speak (ElevenLabs / OpenAI TTS)" },
|
|
282
|
+
{ value: "listen", label: "listen (STT)" },
|
|
283
|
+
{ value: "google-workspace", label: "google-workspace" },
|
|
284
|
+
{ value: "browser", label: "browser (PATH engines)" },
|
|
285
|
+
];
|
|
286
|
+
return base.map((o) => {
|
|
287
|
+
const st = (statuses && statuses[o.value]) || { ok: false, extra: "" };
|
|
288
|
+
const tag = st.ok
|
|
289
|
+
? st.extra
|
|
290
|
+
? `ok ${st.extra}`
|
|
291
|
+
: "ok"
|
|
292
|
+
: st.extra
|
|
293
|
+
? `missing ${st.extra}`
|
|
294
|
+
: "missing";
|
|
295
|
+
return { value: o.value, label: `${o.label} ${tag}` };
|
|
296
|
+
});
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
export function firstMissing(statuses) {
|
|
300
|
+
for (const t of TARGETS) {
|
|
301
|
+
if (!statuses || !statuses[t] || !statuses[t].ok) return t;
|
|
302
|
+
}
|
|
303
|
+
return "speak";
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
export async function reprobeHint({
|
|
307
|
+
id,
|
|
308
|
+
found,
|
|
309
|
+
probe,
|
|
310
|
+
platform = process.platform,
|
|
311
|
+
io,
|
|
312
|
+
interactive = false,
|
|
313
|
+
} = {}) {
|
|
314
|
+
let hit = found;
|
|
315
|
+
if (hit) {
|
|
316
|
+
logLine(io, `${id} ok ${hit}`);
|
|
317
|
+
return hit;
|
|
318
|
+
}
|
|
319
|
+
logLine(io, `${id} missing. print-only; not installed:`);
|
|
320
|
+
for (const h of installHints(id, platform)) logLine(io, h);
|
|
321
|
+
if (interactive && io && typeof io.confirm === "function") {
|
|
322
|
+
const again = await io.confirm({
|
|
323
|
+
message: `Installed ${id}? Re-check PATH`,
|
|
324
|
+
initialValue: false,
|
|
325
|
+
});
|
|
326
|
+
if (again) {
|
|
327
|
+
hit = typeof probe === "function" ? probe() : "";
|
|
328
|
+
if (hit) logLine(io, `${id} ok ${hit}`);
|
|
329
|
+
else logLine(io, `${id} still missing`);
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
return hit;
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
function logLine(io, s) {
|
|
336
|
+
if (io && typeof io.log === "function") io.log(s);
|
|
337
|
+
else process.stdout.write(`${s}\n`);
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
function errLine(io, s) {
|
|
341
|
+
if (io && typeof io.error === "function") io.error(s);
|
|
342
|
+
else process.stderr.write(`${s}\n`);
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
async function defaultIo() {
|
|
346
|
+
const { confirm, password, select, text } = await import("./prompt.mjs");
|
|
347
|
+
return {
|
|
348
|
+
confirm,
|
|
349
|
+
password,
|
|
350
|
+
select,
|
|
351
|
+
text,
|
|
352
|
+
log: (s) => process.stdout.write(`${s}\n`),
|
|
353
|
+
error: (s) => process.stderr.write(`${s}\n`),
|
|
354
|
+
};
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
export function oauthWritePath(home, account) {
|
|
358
|
+
const id = str(account, "default");
|
|
359
|
+
if (id !== "default") {
|
|
360
|
+
return path.join(home, ".grunt", "workspace", id, "google-oauth.json");
|
|
361
|
+
}
|
|
362
|
+
const nested = path.join(home, ".grunt", "workspace", "default", "google-oauth.json");
|
|
363
|
+
if (fs.existsSync(nested)) return nested;
|
|
364
|
+
return path.join(home, ".grunt", "google-oauth.json");
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
export async function setupSpeak({
|
|
368
|
+
flags = {},
|
|
369
|
+
home = os.homedir(),
|
|
370
|
+
io,
|
|
371
|
+
fetchFn,
|
|
372
|
+
openUrlFn,
|
|
373
|
+
platform = process.platform,
|
|
374
|
+
spawnSyncFn,
|
|
375
|
+
interactive = false,
|
|
376
|
+
} = {}) {
|
|
377
|
+
let elevenKey = str(flags["elevenlabs-key"] || flags.elevenlabs);
|
|
378
|
+
let openaiKey = str(flags["openai-key"] || flags.openai);
|
|
379
|
+
let provider = str(flags.provider).toLowerCase();
|
|
380
|
+
const skipVerify = flagOn(flags, "skip-verify");
|
|
381
|
+
|
|
382
|
+
if (interactive && io && !elevenKey && !openaiKey) {
|
|
383
|
+
provider = str(
|
|
384
|
+
await io.select({
|
|
385
|
+
message: "Speak provider",
|
|
386
|
+
options: [
|
|
387
|
+
{ value: "elevenlabs", label: "ElevenLabs" },
|
|
388
|
+
{ value: "openai", label: "OpenAI" },
|
|
389
|
+
{ value: "both", label: "both" },
|
|
390
|
+
],
|
|
391
|
+
initialValue: provider === "openai" ? "openai" : "elevenlabs",
|
|
392
|
+
}),
|
|
393
|
+
);
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
const askEleven = provider === "elevenlabs" || provider === "both" || (!provider && !openaiKey);
|
|
397
|
+
const askOpenAi = provider === "openai" || provider === "both";
|
|
398
|
+
|
|
399
|
+
if (interactive && io) {
|
|
400
|
+
if (askEleven && !elevenKey) {
|
|
401
|
+
logLine(io, `Open: ${ELEVENLABS_KEY_URL}`);
|
|
402
|
+
openUrl(ELEVENLABS_KEY_URL, { platform, spawnSyncFn, openUrlFn });
|
|
403
|
+
elevenKey = str(await io.password({ message: "ElevenLabs API key" }));
|
|
404
|
+
}
|
|
405
|
+
if (askOpenAi && !openaiKey) {
|
|
406
|
+
logLine(io, `Open: ${OPENAI_KEY_URL}`);
|
|
407
|
+
openUrl(OPENAI_KEY_URL, { platform, spawnSyncFn, openUrlFn });
|
|
408
|
+
openaiKey = str(await io.password({ message: "OpenAI API key" }));
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
if (provider === "openai") elevenKey = str(flags["elevenlabs-key"] || flags.elevenlabs);
|
|
413
|
+
if (provider === "elevenlabs") openaiKey = str(flags["openai-key"] || flags.openai);
|
|
414
|
+
|
|
415
|
+
const patch = {};
|
|
416
|
+
if (provider && provider !== "both") patch.provider = provider;
|
|
417
|
+
if (elevenKey) patch.elevenlabs = { apiKey: elevenKey };
|
|
418
|
+
if (openaiKey) patch.openai = { apiKey: openaiKey };
|
|
419
|
+
|
|
420
|
+
if (!patch.elevenlabs && !patch.openai) {
|
|
421
|
+
throw new Error(
|
|
422
|
+
interactive
|
|
423
|
+
? "need an ElevenLabs or OpenAI key"
|
|
424
|
+
: "need --elevenlabs-key or --openai-key (or TTY: node scripts/setup.mjs speak)",
|
|
425
|
+
);
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
const dest = mergeSpeakJson(home, patch);
|
|
429
|
+
logLine(io, `wrote ${dest}`);
|
|
430
|
+
|
|
431
|
+
if (skipVerify) return { ok: true, dest, verified: false };
|
|
432
|
+
|
|
433
|
+
const cfg = loadSpeakCfg({ env: {}, home });
|
|
434
|
+
const who = await speakWhoami({
|
|
435
|
+
provider: provider === "both" || !provider ? undefined : provider,
|
|
436
|
+
cfg,
|
|
437
|
+
fetchFn,
|
|
438
|
+
});
|
|
439
|
+
logLine(io, who);
|
|
440
|
+
return { ok: true, dest, verified: true, who };
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
export async function setupListen({
|
|
444
|
+
flags = {},
|
|
445
|
+
env = process.env,
|
|
446
|
+
home = os.homedir(),
|
|
447
|
+
io,
|
|
448
|
+
fetchFn,
|
|
449
|
+
openUrlFn,
|
|
450
|
+
platform = process.platform,
|
|
451
|
+
spawnSyncFn,
|
|
452
|
+
whichFn = whichBin,
|
|
453
|
+
pathEnv = process.env.PATH,
|
|
454
|
+
interactive = false,
|
|
455
|
+
} = {}) {
|
|
456
|
+
const ffmpegBin = await reprobeHint({
|
|
457
|
+
id: "ffmpeg",
|
|
458
|
+
found: whichFn("ffmpeg", pathEnv, platform),
|
|
459
|
+
probe: () => whichFn("ffmpeg", pathEnv, platform),
|
|
460
|
+
platform,
|
|
461
|
+
io,
|
|
462
|
+
interactive,
|
|
463
|
+
});
|
|
464
|
+
const whisperBin = await reprobeHint({
|
|
465
|
+
id: "whisper-cli",
|
|
466
|
+
found: findWhisperBin(whichFn, pathEnv, platform),
|
|
467
|
+
probe: () => findWhisperBin(whichFn, pathEnv, platform),
|
|
468
|
+
platform,
|
|
469
|
+
io,
|
|
470
|
+
interactive,
|
|
471
|
+
});
|
|
472
|
+
|
|
473
|
+
const skipDownload = flagOn(flags, "skip-download");
|
|
474
|
+
if (whisperBin && !skipDownload) {
|
|
475
|
+
let go = true;
|
|
476
|
+
if (interactive && io && typeof io.confirm === "function") {
|
|
477
|
+
go = await io.confirm({
|
|
478
|
+
message: "Download ggml-base.en.bin (~142MB) into ~/.grunt/whisper/?",
|
|
479
|
+
initialValue: true,
|
|
480
|
+
});
|
|
481
|
+
}
|
|
482
|
+
if (go) {
|
|
483
|
+
const model = await downloadGgml({ home, fetchFn, log: { error: (s) => logLine(io, s) } });
|
|
484
|
+
logLine(io, `model ${model}`);
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
const openai = str(flags["openai-key"] || flags.openai);
|
|
489
|
+
let openaiKey = openai;
|
|
490
|
+
const cfgNow = loadListenCfg({ env, home });
|
|
491
|
+
if (!whisperBin && !cfgNow.apiKey && !openaiKey) {
|
|
492
|
+
if (interactive && io) {
|
|
493
|
+
logLine(io, `Local STT is unavailable. OpenAI Whisper fallback: ${OPENAI_KEY_URL}`);
|
|
494
|
+
openUrl(OPENAI_KEY_URL, { platform, spawnSyncFn, openUrlFn });
|
|
495
|
+
openaiKey = str(await io.password({ message: "OpenAI API key (fallback STT)" }));
|
|
496
|
+
} else {
|
|
497
|
+
throw new Error(
|
|
498
|
+
"need whisper-cli or --openai-key (or TTY: node scripts/setup.mjs listen)",
|
|
499
|
+
);
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
const patch = {};
|
|
504
|
+
if (openaiKey) patch.openai = { apiKey: openaiKey };
|
|
505
|
+
const device = str(flags.device);
|
|
506
|
+
if (device) patch.listen = { device };
|
|
507
|
+
if (interactive && io && ffmpegBin && !device && typeof io.text === "function") {
|
|
508
|
+
const d = str(
|
|
509
|
+
await io.text({
|
|
510
|
+
message: "Mic device id (blank = default; Windows needs an id from listen devices)",
|
|
511
|
+
defaultValue: "",
|
|
512
|
+
}),
|
|
513
|
+
);
|
|
514
|
+
if (d) patch.listen = { ...(patch.listen || {}), device: d };
|
|
515
|
+
}
|
|
516
|
+
if (Object.keys(patch).length) {
|
|
517
|
+
const dest = mergeSpeakJson(home, patch);
|
|
518
|
+
logLine(io, `wrote ${dest}`);
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
const cfg = loadListenCfg({ env: {}, home });
|
|
522
|
+
const stt = probeStt({ cfg, whichFn, pathEnv, platform, home, cwd: "" });
|
|
523
|
+
if (stt && stt.kind === "local") {
|
|
524
|
+
logLine(io, `stt=local bin=${stt.bin} model=${stt.model}`);
|
|
525
|
+
} else if (stt && stt.kind === "openai") {
|
|
526
|
+
logLine(io, `stt=openai model=${stt.model}`);
|
|
527
|
+
} else {
|
|
528
|
+
logLine(io, "stt=none");
|
|
529
|
+
}
|
|
530
|
+
return { ok: true, stt };
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
export async function setupGoogleWorkspace({
|
|
534
|
+
flags = {},
|
|
535
|
+
home = os.homedir(),
|
|
536
|
+
io,
|
|
537
|
+
openUrlFn,
|
|
538
|
+
platform = process.platform,
|
|
539
|
+
spawnSyncFn,
|
|
540
|
+
interactive = false,
|
|
541
|
+
loginFn,
|
|
542
|
+
} = {}) {
|
|
543
|
+
const prevHome = process.env.HOME;
|
|
544
|
+
const prevProfile = process.env.USERPROFILE;
|
|
545
|
+
process.env.HOME = home;
|
|
546
|
+
process.env.USERPROFILE = home;
|
|
547
|
+
try {
|
|
548
|
+
const account = str(flags.account, "default");
|
|
549
|
+
setAccount(account);
|
|
550
|
+
gruntHome(home);
|
|
551
|
+
const dest = oauthWritePath(home, account);
|
|
552
|
+
const credsFlag = str(flags.creds || flags.json);
|
|
553
|
+
let projectId = str(flags.project);
|
|
554
|
+
|
|
555
|
+
if (!credsFlag && interactive && io) {
|
|
556
|
+
const urls0 = workspaceConsoleUrls(projectId);
|
|
557
|
+
logLine(io, "Each person uses their own GCP Desktop OAuth client. Do not share JSON or tokens.");
|
|
558
|
+
logLine(io, `Open: ${urls0.projectCreate}`);
|
|
559
|
+
openUrl(urls0.projectCreate, { platform, spawnSyncFn, openUrlFn });
|
|
560
|
+
if (typeof io.confirm === "function") {
|
|
561
|
+
await io.confirm({ message: "GCP project created? Continue", initialValue: true });
|
|
562
|
+
}
|
|
563
|
+
if (typeof io.text === "function") {
|
|
564
|
+
projectId = str(
|
|
565
|
+
await io.text({
|
|
566
|
+
message: "Project ID (from the console URL ?project=)",
|
|
567
|
+
initialValue: projectId,
|
|
568
|
+
}),
|
|
569
|
+
projectId,
|
|
570
|
+
);
|
|
571
|
+
}
|
|
572
|
+
const urls = workspaceConsoleUrls(projectId);
|
|
573
|
+
logLine(io, "Consent: Internal on paid Workspace, else Testing + add your email as a test user.");
|
|
574
|
+
logLine(io, `Open: ${urls.branding}`);
|
|
575
|
+
openUrl(urls.branding, { platform, spawnSyncFn, openUrlFn });
|
|
576
|
+
if (typeof io.confirm === "function") {
|
|
577
|
+
await io.confirm({ message: "Branding / audience done? Continue", initialValue: true });
|
|
578
|
+
}
|
|
579
|
+
logLine(io, `Open: ${urls.enableApis}`);
|
|
580
|
+
openUrl(urls.enableApis, { platform, spawnSyncFn, openUrlFn });
|
|
581
|
+
if (typeof io.confirm === "function") {
|
|
582
|
+
await io.confirm({ message: "APIs enabled? Continue", initialValue: true });
|
|
583
|
+
}
|
|
584
|
+
logLine(io, "Create OAuth client: application type Desktop app → Download JSON.");
|
|
585
|
+
logLine(io, `Open: ${urls.clientCreate}`);
|
|
586
|
+
openUrl(urls.clientCreate, { platform, spawnSyncFn, openUrlFn });
|
|
587
|
+
if (typeof io.confirm === "function") {
|
|
588
|
+
await io.confirm({ message: "JSON downloaded? Continue", initialValue: true });
|
|
589
|
+
}
|
|
590
|
+
const found = findDownloadedOAuthJson(home);
|
|
591
|
+
const src = str(
|
|
592
|
+
typeof io.text === "function"
|
|
593
|
+
? await io.text({
|
|
594
|
+
message: "Path to Desktop OAuth JSON (or paste the JSON)",
|
|
595
|
+
initialValue: found,
|
|
596
|
+
defaultValue: found,
|
|
597
|
+
})
|
|
598
|
+
: found,
|
|
599
|
+
);
|
|
600
|
+
if (!src) throw new Error("need path to Desktop OAuth JSON");
|
|
601
|
+
installOAuthJson({ src, dest });
|
|
602
|
+
} else if (credsFlag) {
|
|
603
|
+
installOAuthJson({ src: credsFlag, dest });
|
|
604
|
+
} else if (!fs.existsSync(dest)) {
|
|
605
|
+
throw new Error(
|
|
606
|
+
`need --creds PATH (or TTY: node scripts/setup.mjs google-workspace)`,
|
|
607
|
+
);
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
logLine(io, `oauth ${dest}`);
|
|
611
|
+
const login = loginFn || loginLoopback;
|
|
612
|
+
await login(dest);
|
|
613
|
+
logLine(io, `tokens ${tokenStorePath()}`);
|
|
614
|
+
return { ok: true, dest, tokens: tokenStorePath() };
|
|
615
|
+
} finally {
|
|
616
|
+
if (prevHome === undefined) delete process.env.HOME;
|
|
617
|
+
else process.env.HOME = prevHome;
|
|
618
|
+
if (prevProfile === undefined) delete process.env.USERPROFILE;
|
|
619
|
+
else process.env.USERPROFILE = prevProfile;
|
|
620
|
+
}
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
export async function setupBrowser({
|
|
624
|
+
io,
|
|
625
|
+
platform = process.platform,
|
|
626
|
+
pathEnv = process.env.PATH,
|
|
627
|
+
whichFn = whichBin,
|
|
628
|
+
interactive = false,
|
|
629
|
+
} = {}) {
|
|
630
|
+
const lp = await reprobeHint({
|
|
631
|
+
id: "lightpanda",
|
|
632
|
+
found: whichFn("lightpanda", pathEnv, platform),
|
|
633
|
+
probe: () => whichFn("lightpanda", pathEnv, platform),
|
|
634
|
+
platform,
|
|
635
|
+
io,
|
|
636
|
+
interactive,
|
|
637
|
+
});
|
|
638
|
+
const chromium = await reprobeHint({
|
|
639
|
+
id: "chromium",
|
|
640
|
+
found: lookupChromium(whichFn, pathEnv, platform),
|
|
641
|
+
probe: () => lookupChromium(whichFn, pathEnv, platform),
|
|
642
|
+
platform,
|
|
643
|
+
io,
|
|
644
|
+
interactive,
|
|
645
|
+
});
|
|
646
|
+
const st = browserEngineStatus(lp, chromium, platform);
|
|
647
|
+
logLine(io, "session: .tmp/grunt/browser/");
|
|
648
|
+
if (lp && chromium) {
|
|
649
|
+
logLine(io, "browser ok lightpanda (default) + chromium (fallback)");
|
|
650
|
+
} else if (!lp && chromium && platform !== "win32") {
|
|
651
|
+
logLine(io, "Chromium is the fallback only. Install Lightpanda as well (default engine).");
|
|
652
|
+
} else if (lp && !chromium) {
|
|
653
|
+
logLine(io, "Lightpanda ok. Install a chromium-family browser as well (shot/pdf/fallback).");
|
|
654
|
+
}
|
|
655
|
+
return { ok: st.ok, lightpanda: lp, chromium };
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
async function runTarget(target, ctx) {
|
|
659
|
+
if (target === "speak") return setupSpeak(ctx);
|
|
660
|
+
if (target === "listen") return setupListen(ctx);
|
|
661
|
+
if (target === "google-workspace") return setupGoogleWorkspace(ctx);
|
|
662
|
+
if (target === "browser") return setupBrowser(ctx);
|
|
663
|
+
throw new Error(`unknown target ${target}`);
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
export async function main(argv = process.argv.slice(2), opts = {}) {
|
|
667
|
+
const { _, flags } = parseArgv(argv);
|
|
668
|
+
const env = opts.env || process.env;
|
|
669
|
+
const home = opts.home || env.HOME || env.USERPROFILE || os.homedir();
|
|
670
|
+
const io = opts.io || (await defaultIo());
|
|
671
|
+
const interactive =
|
|
672
|
+
opts.interactive != null
|
|
673
|
+
? Boolean(opts.interactive)
|
|
674
|
+
: ttyInteractive({
|
|
675
|
+
argv,
|
|
676
|
+
env,
|
|
677
|
+
stdin: opts.stdin || process.stdin,
|
|
678
|
+
stdout: opts.stdout || process.stdout,
|
|
679
|
+
});
|
|
680
|
+
const ctx = {
|
|
681
|
+
flags,
|
|
682
|
+
env,
|
|
683
|
+
home,
|
|
684
|
+
io,
|
|
685
|
+
fetchFn: opts.fetchFn,
|
|
686
|
+
openUrlFn: opts.openUrlFn,
|
|
687
|
+
platform: opts.platform || process.platform,
|
|
688
|
+
spawnSyncFn: opts.spawnSyncFn,
|
|
689
|
+
whichFn: opts.whichFn,
|
|
690
|
+
pathEnv: opts.pathEnv || env.PATH || process.env.PATH,
|
|
691
|
+
interactive,
|
|
692
|
+
loginFn: opts.loginFn,
|
|
693
|
+
};
|
|
694
|
+
|
|
695
|
+
const statusOpts = {
|
|
696
|
+
home,
|
|
697
|
+
env,
|
|
698
|
+
whichFn: ctx.whichFn,
|
|
699
|
+
pathEnv: ctx.pathEnv,
|
|
700
|
+
platform: ctx.platform,
|
|
701
|
+
};
|
|
702
|
+
let statuses = targetStatuses(statusOpts);
|
|
703
|
+
let target = normalizeTarget(_[0]);
|
|
704
|
+
if (!target && interactive && io && typeof io.select === "function") {
|
|
705
|
+
target = normalizeTarget(
|
|
706
|
+
await io.select({
|
|
707
|
+
message: "Setup",
|
|
708
|
+
options: menuOptions(statuses),
|
|
709
|
+
initialValue: firstMissing(statuses),
|
|
710
|
+
}),
|
|
711
|
+
);
|
|
712
|
+
}
|
|
713
|
+
if (!target) {
|
|
714
|
+
errLine(io, USAGE.trimEnd());
|
|
715
|
+
return 1;
|
|
716
|
+
}
|
|
717
|
+
try {
|
|
718
|
+
const st = statuses[target] || { ok: false, extra: "" };
|
|
719
|
+
const force = flagOn(flags, "redo");
|
|
720
|
+
if (st.ok && interactive && !force && io && typeof io.confirm === "function") {
|
|
721
|
+
const extra = st.extra ? ` (${st.extra})` : "";
|
|
722
|
+
const again = await io.confirm({
|
|
723
|
+
message: `${target} already ok${extra}. Redo?`,
|
|
724
|
+
initialValue: false,
|
|
725
|
+
});
|
|
726
|
+
if (!again) {
|
|
727
|
+
logLine(io, `skip ${target} (already ok)`);
|
|
728
|
+
logLine(io, formatStatus(statuses));
|
|
729
|
+
return 0;
|
|
730
|
+
}
|
|
731
|
+
}
|
|
732
|
+
await runTarget(target, ctx);
|
|
733
|
+
statuses = targetStatuses(statusOpts);
|
|
734
|
+
logLine(io, formatStatus(statuses));
|
|
735
|
+
return 0;
|
|
736
|
+
} catch (e) {
|
|
737
|
+
errLine(io, String(e && e.message ? e.message : e));
|
|
738
|
+
return 1;
|
|
739
|
+
}
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
const thisFile = fileURLToPath(import.meta.url);
|
|
743
|
+
const invoked = process.argv[1] ? path.resolve(process.argv[1]) : "";
|
|
744
|
+
if (invoked === thisFile || import.meta.url === pathToFileURL(invoked).href) {
|
|
745
|
+
main().then((code) => process.exit(code));
|
|
746
|
+
}
|