Skip to content

Commit d799fc6

Browse files
committed
cli turned out to be stupidly cool
1 parent 2fce119 commit d799fc6

7 files changed

Lines changed: 295 additions & 81 deletions

File tree

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "degoog-cli",
3-
"version": "0.3.0",
3+
"version": "0.4.0",
44
"description": "CLI toolset for degoog",
55
"type": "module",
66
"scripts": {

src/commands/create.ts

Lines changed: 54 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import * as p from "@clack/prompts";
2+
import { join } from "node:path";
23
import { ExtType, type GeneratorCtx } from "../types/index.ts";
34
import { promptExtType } from "../prompts/ext-type.ts";
45
import { generateEngine } from "../generators/engine.ts";
@@ -13,6 +14,13 @@ import { generatePluginMid } from "../generators/plugin-mid.ts";
1314
import { generatePluginRoute } from "../generators/plugin-route.ts";
1415
import { loadConfig } from "../config/store.ts";
1516
import { argv } from "../utils/argv.ts";
17+
import {
18+
extTypeToCategory,
19+
findStoreRoot,
20+
registerExtensionInStore,
21+
scaffoldStore,
22+
} from "../utils/store.ts";
23+
import { t } from "../utils/theme.ts";
1624

1725
const SLUG_RE = /^[a-z][a-z0-9-]*$/;
1826

@@ -31,6 +39,43 @@ const GENERATORS: Record<ExtType, (ctx: GeneratorCtx) => Promise<string>> = {
3139
[ExtType.PluginRoutes]: generatePluginRoute,
3240
};
3341

42+
const resolveOutDir = async (
43+
extType: ExtType,
44+
config: Awaited<ReturnType<typeof loadConfig>>,
45+
): Promise<string | null> => {
46+
if (argv.out) return argv.out;
47+
48+
const category = extTypeToCategory(extType);
49+
let store = await findStoreRoot(process.cwd());
50+
51+
if (store) {
52+
p.log.info(t.muted(`store detected — creating in ${category}/`));
53+
return join(store.dir, category);
54+
}
55+
56+
const setup = await p.confirm({
57+
message: "No store detected. Set up a store in the current directory?",
58+
initialValue: true,
59+
});
60+
if (p.isCancel(setup)) return null;
61+
62+
if (setup) {
63+
await scaffoldStore(process.cwd(), config);
64+
store = await findStoreRoot(process.cwd());
65+
if (store) {
66+
p.log.info(t.muted(`store created — creating in ${category}/`));
67+
return join(store.dir, category);
68+
}
69+
}
70+
71+
const input = await p.text({
72+
message: "Output directory",
73+
placeholder: ".",
74+
});
75+
if (p.isCancel(input)) return null;
76+
return input || ".";
77+
};
78+
3479
export const createCmd = async () => {
3580
let name: string
3681

@@ -60,23 +105,18 @@ export const createCmd = async () => {
60105
extType = picked
61106
}
62107

63-
let outDir: string
64-
65-
if (argv.out) {
66-
outDir = argv.out
67-
} else {
68-
const input = await p.text({
69-
message: "Output directory",
70-
initialValue: ".",
71-
})
72-
if (p.isCancel(input)) return
73-
outDir = input || "."
74-
}
75-
76108
const config = await loadConfig()
109+
const outDir = await resolveOutDir(extType, config)
110+
if (!outDir) return
111+
77112
const ctx: GeneratorCtx = { name, outDir, config }
78113

79-
await GENERATORS[extType](ctx)
114+
const createdPath = await GENERATORS[extType](ctx)
115+
116+
const store = await findStoreRoot(createdPath)
117+
if (store) {
118+
await registerExtensionInStore(store.dir, name, extType)
119+
}
80120

81121
p.note(
82122
`That's it, you now have a sexy template for your extension. Have fun making it your own!`,

src/commands/doctor/checks.ts

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { readFile, writeFile, readdir } from "node:fs/promises"
22
import { join, basename } from "node:path"
33
import { exists } from "./detect.ts"
4+
import { findStoreRoot } from "../../utils/store.ts"
45
import type { CheckResult, ExtensionKind, RunSummary } from "./types.ts"
56

67
const readJson = async <T>(path: string): Promise<T | null> => {
@@ -279,9 +280,12 @@ export const runChecks = async (
279280
if (themeRes.failed) failed = true
280281
}
281282

282-
const authorRes = await checkAuthorJson(dir, doFix)
283-
results.push(authorRes.result)
284-
if (authorRes.failed) failed = true
283+
const inStore = await findStoreRoot(dir)
284+
if (!inStore) {
285+
const authorRes = await checkAuthorJson(dir, doFix)
286+
results.push(authorRes.result)
287+
if (authorRes.failed) failed = true
288+
}
285289

286290
if (kind === "plugin") {
287291
const routeChecks = await checkRouteConventions(dir, doFix)

src/commands/doctor/detect.ts

Lines changed: 5 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,9 @@
1-
import { access, readFile } from "node:fs/promises"
21
import { join, basename, dirname } from "node:path"
3-
import type { ExtensionKind, StoreManifest, Target } from "./types.ts"
4-
import { EXTENSION_CATEGORIES } from "./types.ts"
2+
import { exists } from "../../utils/files.ts"
3+
import { readStoreManifest } from "../../utils/store.ts"
4+
import type { ExtensionKind, Target } from "./types.ts"
55

6-
export const exists = async (path: string): Promise<boolean> => {
7-
try {
8-
await access(path)
9-
return true
10-
} catch {
11-
return false
12-
}
13-
}
6+
export { exists }
147

158
const KIND_FROM_FOLDER: Record<string, ExtensionKind> = {
169
themes: "theme",
@@ -37,25 +30,10 @@ export const detectExtKind = async (dir: string): Promise<ExtensionKind> => {
3730
return detectKindFromParent(dir)
3831
}
3932

40-
const readStoreManifest = async (
41-
dir: string,
42-
): Promise<{ path: string; manifest: StoreManifest } | null> => {
43-
const path = join(dir, "package.json")
44-
if (!(await exists(path))) return null
45-
try {
46-
const manifest = JSON.parse(await readFile(path, "utf-8")) as StoreManifest
47-
const hasAny = EXTENSION_CATEGORIES.some((c) => Array.isArray(manifest[c]))
48-
if (!hasAny) return null
49-
return { path, manifest }
50-
} catch {
51-
return null
52-
}
53-
}
54-
5533
export const detectTarget = async (dir: string): Promise<Target | null> => {
5634
const store = await readStoreManifest(dir)
5735
if (store) {
58-
return { kind: "store", dir, manifestPath: store.path, manifest: store.manifest }
36+
return { kind: "store", dir, manifestPath: store.manifestPath, manifest: store.manifest }
5937
}
6038
if (await isExtensionDir(dir)) {
6139
const extKind = await detectExtKind(dir)

src/commands/search.ts

Lines changed: 90 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,14 @@ type SearchResp = {
1616
relatedSearches: string[];
1717
};
1818

19+
const PAGE_SIZE = 10;
20+
21+
const parseSearchQuery = (): string | undefined => {
22+
const argStart = process.argv[2] === "search" ? 3 : 2;
23+
const args = process.argv.slice(argStart).filter((a) => !a.startsWith("-"));
24+
return args.length ? args.join(" ") : undefined;
25+
};
26+
1927
const openUrl = (url: string) => {
2028
const cmd = process.platform === "darwin" ? "open" : "xdg-open";
2129
try {
@@ -46,19 +54,28 @@ const wrap = (text: string, width: number): string[] => {
4654
const truncate = (s: string, max: number) =>
4755
s.length > max ? s.slice(0, max - 1) + "…" : s;
4856

49-
const renderResults = (data: SearchResp) => {
57+
const totalPages = (count: number) => Math.max(1, Math.ceil(count / PAGE_SIZE));
58+
59+
const renderResults = (
60+
data: SearchResp,
61+
page: number,
62+
showRelated: boolean,
63+
) => {
5064
const cols = process.stdout.columns ?? 100;
5165
const maxWidth = Math.min(cols - 8, 90);
5266
const pad = " ";
67+
const pages = totalPages(data.results.length);
68+
const start = (page - 1) * PAGE_SIZE;
69+
const pageResults = data.results.slice(start, start + PAGE_SIZE);
5370

5471
console.log();
5572
console.log(
56-
` ${t.muted(`${data.results.length} results · ${data.totalTime}ms`)}`,
73+
` ${t.muted(`page ${page}/${pages} · ${data.results.length} results · ${data.totalTime}ms`)}`,
5774
);
5875
console.log();
5976

60-
data.results.forEach((r, i) => {
61-
const num = t.dim(` ${String(i + 1).padStart(2)} `);
77+
pageResults.forEach((r, i) => {
78+
const num = t.dim(` ${String(start + i + 1).padStart(2)} `);
6279
console.log(`${num}${t.bold(t.brand(truncate(r.title, maxWidth)))}`);
6380
console.log(`${pad}${t.success(truncate(r.url, maxWidth))}`);
6481

@@ -74,7 +91,7 @@ const renderResults = (data: SearchResp) => {
7491
console.log();
7592
});
7693

77-
if (data.relatedSearches?.length) {
94+
if (showRelated && data.relatedSearches?.length) {
7895
const related = data.relatedSearches
7996
.slice(0, 5)
8097
.map((s) => t.primary(s))
@@ -105,49 +122,58 @@ const doSearch = async (
105122
return result.data;
106123
};
107124

108-
export const searchCmd = async () => {
109-
const config = await loadConfig();
125+
const promptQuery = async (): Promise<string | null> => {
126+
const query = await p.text({
127+
message: t.brand("search"),
128+
placeholder: "what are you looking for?",
129+
});
130+
if (p.isCancel(query) || !query) return null;
131+
return query;
132+
};
110133

111-
if (!config.instanceUrl) {
112-
p.log.warn(t.warning("no instance configured - run Login / Setup first"));
113-
return;
114-
}
134+
const browseResults = async (data: SearchResp): Promise<"again" | "back"> => {
135+
let page = 1;
136+
const pages = totalPages(data.results.length);
115137

116138
while (true) {
117-
const query = await p.text({
118-
message: t.brand("search"),
119-
placeholder: "what are you looking for?",
120-
});
121-
if (p.isCancel(query) || !query) return;
122-
123-
const data = await doSearch(query, config);
124-
if (!data) return;
125-
126-
if (!data.results.length) {
127-
p.log.warn(t.muted("no results found"));
128-
continue;
129-
}
130-
131-
renderResults(data);
132-
133-
const openOpts = data.results.slice(0, 5).map((r, i) => ({
134-
value: `open:${i}`,
135-
label: t.text(`open #${i + 1}`),
136-
hint: truncate(r.title, 50),
137-
}));
139+
renderResults(data, page, page === 1);
140+
141+
const openOpts = data.results
142+
.slice((page - 1) * PAGE_SIZE, page * PAGE_SIZE)
143+
.map((r, i) => ({
144+
value: `open:${(page - 1) * PAGE_SIZE + i}`,
145+
label: t.text(truncate(r.title, 60)),
146+
}));
147+
148+
const navOpts = [
149+
...(page > 1
150+
? [{ value: "prev", label: t.muted("previous page") }]
151+
: []),
152+
...(page < pages
153+
? [{ value: "next", label: t.muted("next page") }]
154+
: []),
155+
];
138156

139157
const action = await p.select({
140158
message: t.muted("open a result, search again, or go back"),
141159
options: [
142160
...openOpts,
161+
...navOpts,
143162
{ value: "again", label: t.muted("search again") },
144163
{ value: "back", label: t.muted("back to menu") },
145164
],
146165
});
147166

148-
if (p.isCancel(action) || action === "back") return;
149-
150-
if (action === "again") continue;
167+
if (p.isCancel(action) || action === "back") return "back";
168+
if (action === "again") return "again";
169+
if (action === "next") {
170+
page = Math.min(page + 1, pages);
171+
continue;
172+
}
173+
if (action === "prev") {
174+
page = Math.max(page - 1, 1);
175+
continue;
176+
}
151177

152178
if (typeof action === "string" && action.startsWith("open:")) {
153179
const idx = parseInt(action.slice(5), 10);
@@ -156,3 +182,32 @@ export const searchCmd = async () => {
156182
}
157183
}
158184
};
185+
186+
export const searchCmd = async () => {
187+
const config = await loadConfig();
188+
189+
if (!config.instanceUrl) {
190+
p.log.warn(t.warning("no instance configured - run Login / Setup first"));
191+
return;
192+
}
193+
194+
let pendingQuery = parseSearchQuery();
195+
196+
while (true) {
197+
const query = pendingQuery ?? (await promptQuery());
198+
pendingQuery = undefined;
199+
200+
if (!query) return;
201+
202+
const data = await doSearch(query, config);
203+
if (!data) return;
204+
205+
if (!data.results.length) {
206+
p.log.warn(t.muted("no results found"));
207+
continue;
208+
}
209+
210+
const action = await browseResults(data);
211+
if (action === "back") return;
212+
}
213+
};

src/utils/files.ts

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { mkdir, writeFile } from "node:fs/promises"
1+
import { mkdir, writeFile, access } from "node:fs/promises"
22
import { join, resolve, dirname } from "node:path"
33
import { logger } from "./logger.ts"
44
import type { Config } from "../types/index.ts"
@@ -8,6 +8,15 @@ export const mkdirp = async (dir: string) => {
88
await mkdir(dir, { recursive: true })
99
}
1010

11+
export const exists = async (path: string): Promise<boolean> => {
12+
try {
13+
await access(path)
14+
return true
15+
} catch {
16+
return false
17+
}
18+
}
19+
1120
export const writeOut = async (filePath: string, content: string | HTMLBundle) => {
1221
// @ts-expect-error - Bun supports writing HTMLBundles
1322
await writeFile(filePath, content, "utf8")

0 commit comments

Comments
 (0)