Skip to content

Commit e44b61c

Browse files
committed
improve cli
1 parent 326e888 commit e44b61c

27 files changed

Lines changed: 1194 additions & 97 deletions

src/commands/doctor.ts

Lines changed: 223 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,223 @@
1+
import { readFile, writeFile, access } from "node:fs/promises"
2+
import { join, basename, resolve } from "node:path"
3+
import * as p from "@clack/prompts"
4+
import { t } from "../utils/theme.ts"
5+
import { logger } from "../utils/logger.ts"
6+
import { ExtType } from "../types/index.ts"
7+
8+
const PASS = t.success("PASS")
9+
const FAIL = t.danger("FAIL")
10+
const WARN = t.warning("WARN")
11+
const FIX = t.success("FIX ")
12+
13+
type CheckResult = { label: string; status: "pass" | "fail" | "warn" | "fix"; detail?: string }
14+
15+
const exists = async (path: string) => {
16+
try { await access(path); return true }
17+
catch { return false }
18+
}
19+
20+
const readJson = async <T>(path: string): Promise<T | null> => {
21+
try { return JSON.parse(await readFile(path, "utf-8")) as T }
22+
catch { return null }
23+
}
24+
25+
const writeJson = async (path: string, data: unknown) =>
26+
writeFile(path, JSON.stringify(data, null, 2) + "\n", "utf-8")
27+
28+
const detectType = async (dir: string): Promise<ExtType | null> => {
29+
if (await exists(join(dir, "theme.json"))) return ExtType.Theme
30+
if (!await exists(join(dir, "index.ts"))) return null
31+
try {
32+
const src = await readFile(join(dir, "index.ts"), "utf-8")
33+
if (src.includes("export") && src.includes("engine")) return ExtType.Engine
34+
if (src.includes("export") && src.includes("autocomplete")) return ExtType.Autocomplete
35+
if (src.includes("export") && src.includes("bang")) return ExtType.PluginBang
36+
if (src.includes("export") && src.includes("slot")) return ExtType.PluginSlot
37+
if (src.includes("export") && src.includes("tab")) return ExtType.PluginTab
38+
return null
39+
} catch {
40+
return null
41+
}
42+
}
43+
44+
const checkManifestField = async (
45+
manifest: Record<string, unknown>,
46+
field: string,
47+
manifestPath: string,
48+
doFix: boolean,
49+
defaultVal: unknown,
50+
requireNonEmpty = false,
51+
): Promise<CheckResult> => {
52+
const missing = manifest[field] == null
53+
const empty = requireNonEmpty && String(manifest[field] ?? "").trim() === ""
54+
if (!missing && !empty) return { label: `manifest has "${field}"`, status: "pass" }
55+
if (!doFix) return { label: `manifest has "${field}"`, status: "fail", detail: missing ? "missing" : "empty" }
56+
manifest[field] = defaultVal
57+
await writeJson(manifestPath, manifest)
58+
return { label: `manifest has "${field}"`, status: "fix", detail: `set to ${JSON.stringify(defaultVal)}` }
59+
}
60+
61+
const checkThemePaths = async (
62+
manifest: Record<string, unknown>,
63+
dir: string,
64+
): Promise<CheckResult[]> => {
65+
const results: CheckResult[] = []
66+
const check = async (label: string, filePath: string) => {
67+
const full = join(dir, filePath)
68+
const ok = await exists(full)
69+
results.push({ label, status: ok ? "pass" : "warn", detail: ok ? undefined : `missing: ${filePath}` })
70+
}
71+
72+
if (typeof manifest.css === "string") await check(`css file resolves`, manifest.css)
73+
74+
const html = manifest.html as Record<string, string> | undefined
75+
if (html && typeof html === "object") {
76+
for (const [key, path] of Object.entries(html)) {
77+
if (typeof path === "string") await check(`html.${key} resolves`, path)
78+
}
79+
}
80+
81+
const templates = manifest.templates as Record<string, string> | undefined
82+
if (templates && typeof templates === "object") {
83+
for (const [key, path] of Object.entries(templates)) {
84+
if (typeof path === "string") await check(`templates.${key} resolves`, path)
85+
}
86+
}
87+
88+
return results
89+
}
90+
91+
const runChecks = async (dir: string, doFix: boolean): Promise<{ results: CheckResult[]; failed: boolean }> => {
92+
const results: CheckResult[] = []
93+
let failed = false
94+
95+
const extType = await detectType(dir)
96+
if (!extType) {
97+
results.push({ label: "extension type detected", status: "fail", detail: "no theme.json or recognisable index.ts found" })
98+
return { results, failed: true }
99+
}
100+
results.push({ label: `extension type: ${extType}`, status: "pass" })
101+
102+
const manifestName = extType === ExtType.Theme ? "theme.json" : "author.json"
103+
const manifestPath = join(dir, extType === ExtType.Theme ? "theme.json" : "index.ts")
104+
105+
if (extType === ExtType.Theme) {
106+
const themePath = join(dir, "theme.json")
107+
let manifest = await readJson<Record<string, unknown>>(themePath)
108+
109+
if (!manifest) {
110+
if (doFix) {
111+
manifest = { name: basename(dir), description: "", version: "1.0.0" }
112+
await writeJson(themePath, manifest)
113+
results.push({ label: "theme.json exists", status: "fix", detail: "created minimal manifest" })
114+
} else {
115+
results.push({ label: "theme.json exists", status: "fail" })
116+
failed = true
117+
}
118+
manifest = manifest ?? {}
119+
} else {
120+
results.push({ label: "theme.json exists", status: "pass" })
121+
}
122+
123+
for (const [field, def, req] of [["name", basename(dir), true], ["description", "", false], ["version", "1.0.0", true]] as const) {
124+
const r = await checkManifestField(manifest, field, themePath, doFix, def, req)
125+
results.push(r)
126+
if (r.status === "fail") failed = true
127+
}
128+
129+
const pathChecks = await checkThemePaths(manifest, dir)
130+
results.push(...pathChecks)
131+
if (pathChecks.some(c => c.status === "warn")) failed = true
132+
}
133+
134+
const authorPath = join(dir, "author.json")
135+
const authorExists = await exists(authorPath)
136+
if (!authorExists) {
137+
if (doFix) {
138+
await writeJson(authorPath, { name: "", url: "" })
139+
results.push({ label: "author.json exists", status: "fix", detail: "created with empty fields" })
140+
} else {
141+
results.push({ label: "author.json exists", status: "fail" })
142+
failed = true
143+
}
144+
} else {
145+
results.push({ label: "author.json exists", status: "pass" })
146+
}
147+
148+
return { results, failed }
149+
}
150+
151+
const statusIcon = (s: CheckResult["status"]) => {
152+
if (s === "pass") return PASS
153+
if (s === "fail") return FAIL
154+
if (s === "warn") return WARN
155+
return FIX
156+
}
157+
158+
export const doctorCmd = async () => {
159+
const argStart = process.argv[2] === "doctor" ? 3 : 2
160+
const args = process.argv.slice(argStart)
161+
const doFix = args.includes("--fix")
162+
const pathArg = args.find(a => !a.startsWith("-"))
163+
164+
p.intro(t.brand("degoog doctor"))
165+
166+
let targetDir: string
167+
168+
if (pathArg) {
169+
targetDir = resolve(process.cwd(), pathArg)
170+
} else {
171+
const input = await p.text({
172+
message: t.muted("path to the extension folder"),
173+
placeholder: process.cwd(),
174+
validate: (v) => {
175+
if (!v.trim()) return undefined
176+
return undefined
177+
},
178+
})
179+
if (p.isCancel(input)) {
180+
p.cancel(t.muted("cancelled"))
181+
process.exit(0)
182+
}
183+
targetDir = resolve(process.cwd(), (input as string).trim() || process.cwd())
184+
}
185+
186+
const applyFix = doFix || (await p.confirm({
187+
message: t.muted("apply safe fixes automatically?"),
188+
initialValue: false,
189+
})) === true
190+
191+
if (p.isCancel(applyFix)) {
192+
p.cancel(t.muted("cancelled"))
193+
process.exit(0)
194+
}
195+
196+
const dirExists = await exists(targetDir)
197+
if (!dirExists) {
198+
logger.error(`directory not found: ${targetDir}`)
199+
process.exit(1)
200+
}
201+
202+
p.log.info(t.muted(`checking ${targetDir}`))
203+
204+
const { results, failed } = await runChecks(targetDir, applyFix)
205+
206+
for (const r of results) {
207+
const icon = statusIcon(r.status)
208+
const detail = r.detail ? t.muted(` - ${r.detail}`) : ""
209+
console.log(` ${icon} ${r.label}${detail}`)
210+
}
211+
212+
console.log("")
213+
214+
if (failed && !applyFix) {
215+
p.outro(t.danger("issues found - re-run and choose to apply fixes, or pass --fix"))
216+
process.exit(1)
217+
} else if (failed) {
218+
p.outro(t.warning("some issues could not be auto-fixed (see WARN above)"))
219+
process.exit(1)
220+
} else {
221+
p.outro(t.success("all checks passed"))
222+
}
223+
}

src/generators/autocomplete.ts

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -23,11 +23,6 @@ export const getSuggestions = async (
2323
context: {
2424
fetch: typeof fetch
2525
lang: string
26-
createCache: <T>(ttlMs: number) => {
27-
get: (key: string) => T | undefined
28-
set: (key: string, value: T) => void
29-
clear: () => void
30-
}
3126
}
3227
): Promise<Suggestion[]> => {
3328
// TODO: implement autocomplete logic

src/generators/engine.ts

Lines changed: 20 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,15 @@ export const executeSearch = async (
2525
buildAcceptLanguage: () => string
2626
dateFrom?: string
2727
dateTo?: string
28+
sentinel?: (
29+
response: { ok: boolean; status: number },
30+
engineName?: string
31+
) => void
32+
engineError?: (
33+
status: string,
34+
message: string,
35+
opts?: { httpStatus?: number; engine?: string }
36+
) => Error
2837
}
2938
) => {
3039
const results: Array<{
@@ -35,10 +44,17 @@ export const executeSearch = async (
3544
thumbnail?: string
3645
}> = []
3746
38-
// TODO: implement search logic
39-
// use context.fetch instead of global fetch
40-
41-
return results
47+
try {
48+
const doFetch = context?.fetch ?? fetch
49+
const response = await doFetch(\`https://api.example.com/search?q=\${encodeURIComponent(query)}\`)
50+
context?.sentinel?.(response, name)
51+
const data = await response.json()
52+
// TODO: map data into results
53+
return results
54+
} catch (e: any) {
55+
if (e?.name === "SentinelBreach") throw e
56+
return []
57+
}
4258
}
4359
`
4460

src/generators/plugin-bang.ts

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -35,10 +35,11 @@ const indexTpl = (name: string) => `export default {
3535
readFile: (filename: string) => Promise<string>
3636
signProxyUrl: (url: string) => string
3737
fetch: typeof fetch
38-
createCache: <T>(ttlMs: number) => {
39-
get: (key: string) => T | undefined
40-
set: (key: string, value: T) => void
41-
clear: () => void
38+
useCache: <T>(namespace: string, defaultTtlMs: number) => {
39+
get: (key: string) => Promise<T | null>
40+
set: (key: string, value: T, ttlMs?: number) => Promise<void>
41+
delete: (key: string) => Promise<void>
42+
clear: () => Promise<void>
4243
}
4344
}) {
4445
return {

src/generators/plugin-slot.ts

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -34,10 +34,11 @@ export const slot = {
3434
readFile: (filename: string) => Promise<string>
3535
signProxyUrl: (url: string) => string
3636
fetch: typeof fetch
37-
createCache: <T>(ttlMs: number) => {
38-
get: (key: string) => T | undefined
39-
set: (key: string, value: T) => void
40-
clear: () => void
37+
useCache: <T>(namespace: string, defaultTtlMs: number) => {
38+
get: (key: string) => Promise<T | null>
39+
set: (key: string, value: T, ttlMs?: number) => Promise<void>
40+
delete: (key: string) => Promise<void>
41+
clear: () => Promise<void>
4142
}
4243
}) {
4344
return {

src/generators/plugin-tab.ts

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -28,10 +28,11 @@ const indexTpl = (name: string) => `export const tab = {
2828
dir: string
2929
fetch: typeof fetch
3030
signProxyUrl: (url: string) => string
31-
createCache: <T>(ttlMs: number) => {
32-
get: (key: string) => T | undefined
33-
set: (key: string, value: T) => void
34-
clear: () => void
31+
useCache: <T>(namespace: string, defaultTtlMs: number) => {
32+
get: (key: string) => Promise<T | null>
33+
set: (key: string, value: T, ttlMs?: number) => Promise<void>
34+
delete: (key: string) => Promise<void>
35+
clear: () => Promise<void>
3536
}
3637
}) {
3738
const results: Array<{

0 commit comments

Comments
 (0)