hacklab 0.7.4 → 0.7.5

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.
@@ -0,0 +1,387 @@
1
+ import * as clack from '@clack/prompts';
2
+ import { captureEvent } from '../posthog.js';
3
+ import { findRepoRoot, inferProject, slugFromName, } from '../project-infer.js';
4
+ import { resolveCommand } from '../resolve-command.js';
5
+ import { loadSession, resolveAppUrl, unauthorizedHint, } from '../session.js';
6
+ import { fetchApi } from '../sync.js';
7
+ import { bold, dim, error, info, success } from '../ui.js';
8
+ // `hacklab project` — publish and manage the projects on your profile. The
9
+ // old `brag` flow asked you to transcribe your repo into a YAML manifest; this
10
+ // one reads the repo instead (git remote, README, package.json) and only asks
11
+ // for confirmation. `add` is idempotent per repo: the slug derives from the
12
+ // repo name, and a re-run refreshes the same project without touching its
13
+ // publish date or losing screenshots.
14
+ const SUBCOMMANDS = ['add', 'list', 'delete'];
15
+ const PROJECTS_PATH = '/api/projects';
16
+ function printJson(data) {
17
+ console.log(JSON.stringify(data, null, 2));
18
+ }
19
+ function emitJsonError(code, message) {
20
+ console.log(JSON.stringify({ schemaVersion: 1, error: { code, message } }));
21
+ process.exit(1);
22
+ }
23
+ function usage() {
24
+ error('usage: hacklab project [add|list|delete]');
25
+ info(` hacklab project ${dim('add [path] [--yes] [--json]')} publish the repo you're in (re-run to refresh)`);
26
+ info(` hacklab project ${dim('add --title/--description/--live/--tags/--slug')} override any inferred field`);
27
+ info(` hacklab project ${dim('list [--json]')} your projects`);
28
+ info(` hacklab project ${dim('delete <slug> [--yes] [--json]')} remove one`);
29
+ process.exit(1);
30
+ }
31
+ async function requireSession(json) {
32
+ const session = await loadSession();
33
+ if (!session) {
34
+ if (json)
35
+ emitJsonError('unauthorized', 'not logged in');
36
+ error('not logged in');
37
+ info(`run ${dim('hacklab login')} first`);
38
+ process.exit(1);
39
+ }
40
+ return session;
41
+ }
42
+ async function readEnvelopeError(res, session) {
43
+ if (res.status === 401)
44
+ return unauthorizedHint(session);
45
+ const data = (await res.json().catch(() => null));
46
+ if (typeof data?.error === 'string')
47
+ return data.error;
48
+ return data?.error?.message ?? `request failed (${res.status})`;
49
+ }
50
+ async function fetchProjects(session) {
51
+ const res = await fetchApi(session, PROJECTS_PATH, {
52
+ headers: { Authorization: `Bearer ${session.token}` },
53
+ });
54
+ if (!res.ok)
55
+ throw new Error(await readEnvelopeError(res, session));
56
+ const data = (await res.json().catch(() => null));
57
+ if (!data?.projects)
58
+ throw new Error('got a malformed response from hacklab');
59
+ return data;
60
+ }
61
+ function flagValue(args, ...names) {
62
+ for (const name of names) {
63
+ const i = args.indexOf(name);
64
+ if (i !== -1 && args[i + 1])
65
+ return args[i + 1];
66
+ const eq = args.find((a) => a.startsWith(`${name}=`));
67
+ if (eq)
68
+ return eq.slice(name.length + 1);
69
+ }
70
+ return undefined;
71
+ }
72
+ /** Split a `--tags a,b,c` value into clean tag names. */
73
+ export function parseTags(value) {
74
+ return value
75
+ .split(',')
76
+ .map((t) => t.trim().toLowerCase())
77
+ .filter(Boolean)
78
+ .slice(0, 20);
79
+ }
80
+ // og:image discovery: one GET of the live page, one regex. Both content-first
81
+ // and property-first attribute orders appear in the wild.
82
+ const OG_IMAGE_PATTERNS = [
83
+ /<meta[^>]+(?:property|name)=["']og:image["'][^>]*content=["']([^"']+)["']/i,
84
+ /<meta[^>]+content=["']([^"']+)["'][^>]*(?:property|name)=["']og:image["']/i,
85
+ ];
86
+ export function extractOgImage(html, baseUrl) {
87
+ for (const pattern of OG_IMAGE_PATTERNS) {
88
+ const match = html.match(pattern);
89
+ if (match?.[1]) {
90
+ try {
91
+ return new URL(match[1], baseUrl).toString();
92
+ }
93
+ catch {
94
+ return null;
95
+ }
96
+ }
97
+ }
98
+ return null;
99
+ }
100
+ const SCREENSHOT_MAX_BYTES = 4 * 1024 * 1024;
101
+ /**
102
+ * Best-effort screenshot from the live site's og:image. Any failure —
103
+ * unreachable site, no tag, oversized or non-png/jpeg image, upload error —
104
+ * returns null and costs nothing but the attempt.
105
+ */
106
+ async function captureOgScreenshot(session, liveUrl) {
107
+ try {
108
+ const page = await fetch(liveUrl, {
109
+ signal: AbortSignal.timeout(8000),
110
+ headers: { 'User-Agent': 'HacklabCLI (+https://hacklab.so)' },
111
+ });
112
+ if (!page.ok)
113
+ return null;
114
+ const imageUrl = extractOgImage(await page.text(), liveUrl);
115
+ if (!imageUrl)
116
+ return null;
117
+ const image = await fetch(imageUrl, {
118
+ signal: AbortSignal.timeout(8000),
119
+ headers: { 'User-Agent': 'HacklabCLI (+https://hacklab.so)' },
120
+ });
121
+ if (!image.ok)
122
+ return null;
123
+ const contentType = image.headers.get('content-type')?.split(';')[0];
124
+ if (contentType !== 'image/png' && contentType !== 'image/jpeg')
125
+ return null;
126
+ const bytes = Buffer.from(await image.arrayBuffer());
127
+ if (bytes.length === 0 || bytes.length > SCREENSHOT_MAX_BYTES)
128
+ return null;
129
+ const upload = await fetchApi(session, '/api/screenshots', {
130
+ method: 'POST',
131
+ headers: {
132
+ 'Content-Type': 'application/json',
133
+ Authorization: `Bearer ${session.token}`,
134
+ },
135
+ body: JSON.stringify({
136
+ image: bytes.toString('base64'),
137
+ filename: contentType === 'image/png' ? 'og.png' : 'og.jpg',
138
+ contentType,
139
+ }),
140
+ });
141
+ const data = (await upload.json().catch(() => null));
142
+ if (!upload.ok || !data?.url)
143
+ return null;
144
+ return { url: data.url, caption: '' };
145
+ }
146
+ catch {
147
+ return null;
148
+ }
149
+ }
150
+ function summarize(value, max = 72) {
151
+ if (!value)
152
+ return dim('(none)');
153
+ const oneLine = value.replace(/\s+/g, ' ').trim();
154
+ return oneLine.length > max ? `${oneLine.slice(0, max - 1)}…` : oneLine;
155
+ }
156
+ async function projectAdd(args) {
157
+ const json = args.includes('--json');
158
+ const yes = args.includes('--yes');
159
+ const overrides = {
160
+ title: flagValue(args, '--title'),
161
+ description: flagValue(args, '--description', '--desc'),
162
+ liveUrl: flagValue(args, '--live'),
163
+ repoUrl: flagValue(args, '--repo'),
164
+ slug: flagValue(args, '--slug'),
165
+ tags: flagValue(args, '--tags'),
166
+ };
167
+ const flagValues = new Set(Object.values(overrides).filter((v) => v !== undefined));
168
+ const pathArg = args.find((a) => !a.startsWith('-') && !flagValues.has(a));
169
+ const root = await findRepoRoot(pathArg ?? process.cwd());
170
+ if (!root) {
171
+ const message = `not a git repository: ${pathArg ?? process.cwd()}`;
172
+ if (json)
173
+ emitJsonError('not_a_repo', message);
174
+ error(message);
175
+ info('run from inside the repo you want to publish');
176
+ process.exit(1);
177
+ }
178
+ const inferred = await inferProject(root);
179
+ const draft = {
180
+ ...inferred,
181
+ title: overrides.title ?? inferred.title,
182
+ description: overrides.description ?? inferred.description,
183
+ liveUrl: overrides.liveUrl ?? inferred.liveUrl,
184
+ repoUrl: overrides.repoUrl ?? inferred.repoUrl,
185
+ slug: overrides.slug ? slugFromName(overrides.slug) : inferred.slug,
186
+ tags: overrides.tags ? parseTags(overrides.tags) : inferred.tags,
187
+ };
188
+ const session = await requireSession(json);
189
+ // The existing row (if any) decides create-vs-refresh, keeps the original
190
+ // publish date, and provides fallback screenshots.
191
+ let existing;
192
+ try {
193
+ existing = (await fetchProjects(session)).projects.find((p) => p.slug === draft.slug);
194
+ }
195
+ catch (err) {
196
+ const message = err instanceof Error ? err.message : String(err);
197
+ if (json)
198
+ emitJsonError('error', message);
199
+ error(message);
200
+ process.exit(1);
201
+ }
202
+ if (!json) {
203
+ console.log(` ${bold(existing ? 'refreshing' : 'publishing')} ${bold(draft.title)} ${dim(`(${draft.slug})`)}`);
204
+ console.log(` ${dim('description')} ${summarize(draft.description)}`);
205
+ console.log(` ${dim('repo')} ${summarize(draft.repoUrl)}`);
206
+ console.log(` ${dim('live')} ${summarize(draft.liveUrl)}`);
207
+ console.log(` ${dim('tags')} ${draft.tags.length ? draft.tags.join(', ') : dim('(none)')}`);
208
+ console.log(` ${dim('content')} ${draft.content ? `README (${draft.content.length} chars)` : dim('(none)')}`);
209
+ }
210
+ if (!json && !yes && process.stdout.isTTY) {
211
+ const go = await clack.confirm({ message: 'publish it?' });
212
+ if (clack.isCancel(go) || !go) {
213
+ clack.outro(dim('cancelled.'));
214
+ return;
215
+ }
216
+ }
217
+ // Fresh og:image capture when the site has one; otherwise whatever the
218
+ // project already shows keeps showing (POST overwrites screenshots).
219
+ let screenshots = existing?.screenshots ?? [];
220
+ if (draft.liveUrl) {
221
+ if (!json)
222
+ info(dim('looking for a screenshot (og:image)…'));
223
+ const shot = await captureOgScreenshot(session, draft.liveUrl);
224
+ if (shot)
225
+ screenshots = [shot];
226
+ }
227
+ const payload = {
228
+ title: draft.title,
229
+ slug: draft.slug,
230
+ description: draft.description ?? undefined,
231
+ tags: draft.tags,
232
+ repoUrl: draft.repoUrl ?? undefined,
233
+ liveUrl: draft.liveUrl ?? undefined,
234
+ screenshots,
235
+ content: draft.content ?? undefined,
236
+ publishedAt: existing?.publishedAt ?? new Date().toISOString(),
237
+ };
238
+ let res;
239
+ try {
240
+ res = await fetchApi(session, PROJECTS_PATH, {
241
+ method: 'POST',
242
+ headers: {
243
+ 'Content-Type': 'application/json',
244
+ Authorization: `Bearer ${session.token}`,
245
+ },
246
+ body: JSON.stringify(payload),
247
+ });
248
+ }
249
+ catch (err) {
250
+ const message = err instanceof Error ? err.message : String(err);
251
+ if (json)
252
+ emitJsonError('network', message);
253
+ error(message);
254
+ process.exit(1);
255
+ }
256
+ if (!res.ok) {
257
+ const message = await readEnvelopeError(res, session);
258
+ if (json)
259
+ emitJsonError('error', message);
260
+ error(message);
261
+ process.exit(1);
262
+ }
263
+ await captureEvent(session.handle, 'cli_project_added', {
264
+ slug: draft.slug,
265
+ refreshed: Boolean(existing),
266
+ has_live_url: Boolean(draft.liveUrl),
267
+ has_screenshot: screenshots.length > 0,
268
+ tag_count: draft.tags.length,
269
+ });
270
+ const path = `/${session.handle}/${draft.slug}`;
271
+ if (json) {
272
+ printJson({
273
+ schemaVersion: 1,
274
+ [existing ? 'refreshed' : 'published']: true,
275
+ slug: draft.slug,
276
+ path,
277
+ });
278
+ return;
279
+ }
280
+ success(`${existing ? 'refreshed' : 'published'} ${bold(draft.title)}`);
281
+ info(`${resolveAppUrl(session)}${path}`);
282
+ }
283
+ async function projectList(args) {
284
+ const json = args.includes('--json');
285
+ const session = await requireSession(json);
286
+ let list;
287
+ try {
288
+ list = await fetchProjects(session);
289
+ }
290
+ catch (err) {
291
+ const message = err instanceof Error ? err.message : String(err);
292
+ if (json)
293
+ emitJsonError('error', message);
294
+ error(message);
295
+ process.exit(1);
296
+ }
297
+ if (json) {
298
+ printJson({ schemaVersion: 1, ...list });
299
+ return;
300
+ }
301
+ if (list.projects.length === 0) {
302
+ info('no projects yet');
303
+ info(`run ${dim('hacklab project add')} inside a repo to publish one`);
304
+ return;
305
+ }
306
+ const width = Math.max(...list.projects.map((p) => p.slug.length));
307
+ for (const p of list.projects) {
308
+ console.log(` ${bold(p.slug.padEnd(width))} ${summarize(p.title, 48)} ${dim(`(${p.source})`)}`);
309
+ }
310
+ console.log('');
311
+ info(dim(`${resolveAppUrl(session)}/${list.handle}`));
312
+ }
313
+ async function projectDelete(args) {
314
+ const json = args.includes('--json');
315
+ const yes = args.includes('--yes');
316
+ const slug = args.find((a) => !a.startsWith('-'));
317
+ if (!slug)
318
+ usage();
319
+ const session = await requireSession(json);
320
+ if (!json && !yes && process.stdout.isTTY) {
321
+ const go = await clack.confirm({
322
+ message: `delete ${bold(slug)} from your profile?`,
323
+ });
324
+ if (clack.isCancel(go) || !go) {
325
+ clack.outro(dim('cancelled.'));
326
+ return;
327
+ }
328
+ }
329
+ let res;
330
+ try {
331
+ res = await fetchApi(session, `${PROJECTS_PATH}/${encodeURIComponent(slug)}`, {
332
+ method: 'DELETE',
333
+ headers: { Authorization: `Bearer ${session.token}` },
334
+ });
335
+ }
336
+ catch (err) {
337
+ const message = err instanceof Error ? err.message : String(err);
338
+ if (json)
339
+ emitJsonError('network', message);
340
+ error(message);
341
+ process.exit(1);
342
+ }
343
+ if (!res.ok) {
344
+ const message = await readEnvelopeError(res, session);
345
+ if (json)
346
+ emitJsonError('error', message);
347
+ error(message);
348
+ process.exit(1);
349
+ }
350
+ const data = (await res.json().catch(() => null));
351
+ await captureEvent(session.handle, 'cli_project_deleted', {
352
+ slug,
353
+ source: data?.deleted?.source,
354
+ });
355
+ if (json) {
356
+ printJson({ schemaVersion: 1, deleted: data?.deleted ?? { slug } });
357
+ return;
358
+ }
359
+ success(`deleted ${bold(slug)}`);
360
+ if (data?.deleted?.source === 'github') {
361
+ info(`it's synced from a pinned GitHub repo — unpin it or the next ${dim('hacklab sync')} brings it back`);
362
+ }
363
+ }
364
+ export async function project(args) {
365
+ const [subToken, ...rest] = args;
366
+ // Bare `hacklab project` inside a repo means "publish this" — the verb the
367
+ // command exists for. `list` is one word away.
368
+ if (!subToken || subToken.startsWith('-')) {
369
+ return projectAdd(args);
370
+ }
371
+ const resolved = resolveCommand(subToken, SUBCOMMANDS);
372
+ if (resolved.kind === 'ambiguous') {
373
+ error(`ambiguous: project ${subToken} (${resolved.matches.join(', ')})`);
374
+ process.exit(1);
375
+ }
376
+ if (resolved.kind === 'unknown') {
377
+ error(`unknown subcommand: project ${subToken}`);
378
+ usage();
379
+ }
380
+ if (resolved.name === 'add')
381
+ return projectAdd(rest);
382
+ if (resolved.name === 'list')
383
+ return projectList(rest);
384
+ if (resolved.name === 'delete')
385
+ return projectDelete(rest);
386
+ }
387
+ //# sourceMappingURL=project.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"project.js","sourceRoot":"","sources":["../../src/commands/project.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,50 @@
1
+ export type RepoRemote = {
2
+ /** Canonical https URL, no trailing `.git`. */
3
+ url: string;
4
+ /** `owner/name` host path when it parses, e.g. `acme/hacklab`. */
5
+ owner: string | null;
6
+ name: string;
7
+ };
8
+ export type InferredProject = {
9
+ slug: string;
10
+ title: string;
11
+ description: string | null;
12
+ tags: string[];
13
+ repoUrl: string | null;
14
+ liveUrl: string | null;
15
+ /** Full README markdown, the project page's long-form content. */
16
+ content: string | null;
17
+ };
18
+ /**
19
+ * Normalize any common git remote spelling to a canonical https URL.
20
+ * `git@github.com:acme/hacklab.git`, `ssh://git@github.com/acme/hacklab.git`,
21
+ * and `https://github.com/acme/hacklab.git` all become
22
+ * `https://github.com/acme/hacklab`.
23
+ */
24
+ export declare function normalizeRepoUrl(remote: string): RepoRemote | null;
25
+ /** Repo-name-derived slug matching the server's PROJECT_SLUG_PATTERN. */
26
+ export declare function slugFromName(name: string): string;
27
+ /** The first ATX heading's text, e.g. `# hacklab` -> `hacklab`. */
28
+ export declare function titleFromReadme(markdown: string): string | null;
29
+ /**
30
+ * The first prose paragraph of a README: skips headings, badge/image-only
31
+ * lines, block quotes, code fences, and HTML, then joins the first run of
32
+ * plain text lines. This is the description fallback when package.json has
33
+ * none.
34
+ */
35
+ export declare function firstParagraph(markdown: string): string | null;
36
+ /** package.json keywords first, then detected stack tags, deduped, capped. */
37
+ export declare function detectTags(pkg: {
38
+ keywords?: unknown;
39
+ dependencies?: Record<string, string>;
40
+ devDependencies?: Record<string, string>;
41
+ }): string[];
42
+ /** The repo root for cwd, or null when not inside a git work tree. */
43
+ export declare function findRepoRoot(cwd: string): Promise<string | null>;
44
+ /**
45
+ * Build the best project we can from the repo at `root`. Precedence per
46
+ * field: README title > repo name; package.json description > README first
47
+ * paragraph; homepage is the only liveUrl source (anything else is guessing).
48
+ */
49
+ export declare function inferProject(root: string): Promise<InferredProject>;
50
+ //# sourceMappingURL=project-infer.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"project-infer.d.ts","sourceRoot":"","sources":["../src/project-infer.ts"],"names":[],"mappings":"AAcA,MAAM,MAAM,UAAU,GAAG;IACvB,+CAA+C;IAC/C,GAAG,EAAE,MAAM,CAAA;IACX,kEAAkE;IAClE,KAAK,EAAE,MAAM,GAAG,IAAI,CAAA;IACpB,IAAI,EAAE,MAAM,CAAA;CACb,CAAA;AAED,MAAM,MAAM,eAAe,GAAG;IAC5B,IAAI,EAAE,MAAM,CAAA;IACZ,KAAK,EAAE,MAAM,CAAA;IACb,WAAW,EAAE,MAAM,GAAG,IAAI,CAAA;IAC1B,IAAI,EAAE,MAAM,EAAE,CAAA;IACd,OAAO,EAAE,MAAM,GAAG,IAAI,CAAA;IACtB,OAAO,EAAE,MAAM,GAAG,IAAI,CAAA;IACtB,kEAAkE;IAClE,OAAO,EAAE,MAAM,GAAG,IAAI,CAAA;CACvB,CAAA;AAED;;;;;GAKG;AACH,wBAAgB,gBAAgB,CAAC,MAAM,EAAE,MAAM,GAAG,UAAU,GAAG,IAAI,CAwBlE;AAED,yEAAyE;AACzE,wBAAgB,YAAY,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CASjD;AAWD,mEAAmE;AACnE,wBAAgB,eAAe,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAS/D;AAED;;;;;GAKG;AACH,wBAAgB,cAAc,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAiC9D;AA2BD,8EAA8E;AAC9E,wBAAgB,UAAU,CAAC,GAAG,EAAE;IAC9B,QAAQ,CAAC,EAAE,OAAO,CAAA;IAClB,YAAY,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;IACrC,eAAe,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;CACzC,GAAG,MAAM,EAAE,CAqBX;AAWD,sEAAsE;AACtE,wBAAsB,YAAY,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAEtE;AAmBD;;;;GAIG;AACH,wBAAsB,YAAY,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,eAAe,CAAC,CAoCzE"}
@@ -0,0 +1,211 @@
1
+ import { execFile } from 'node:child_process';
2
+ import { readFile } from 'node:fs/promises';
3
+ import { join } from 'node:path';
4
+ import { promisify } from 'node:util';
5
+ // Infer a publishable project from the repo you're standing in — the machine
6
+ // already knows the title, description, links, and tags, so no manifest file
7
+ // is ever required. Everything here is best-effort: a missing README or
8
+ // package.json just means fewer prefilled fields, never a failure. The only
9
+ // hard requirement is a git repo (identity + idempotency come from the
10
+ // remote), enforced by the command, not here.
11
+ const execFileAsync = promisify(execFile);
12
+ /**
13
+ * Normalize any common git remote spelling to a canonical https URL.
14
+ * `git@github.com:acme/hacklab.git`, `ssh://git@github.com/acme/hacklab.git`,
15
+ * and `https://github.com/acme/hacklab.git` all become
16
+ * `https://github.com/acme/hacklab`.
17
+ */
18
+ export function normalizeRepoUrl(remote) {
19
+ const trimmed = remote.trim();
20
+ if (!trimmed)
21
+ return null;
22
+ const scp = trimmed.match(/^(?:[\w.-]+@)?([\w.-]+):(?!\/\/)(.+)$/);
23
+ const url = trimmed.match(/^(?:https?|ssh|git):\/\/(?:[\w.-]+@)?([\w.-]+)(?::\d+)?\/(.+)$/);
24
+ const host = url?.[1] ?? scp?.[1];
25
+ let path = url?.[2] ?? scp?.[2];
26
+ if (!host || !path)
27
+ return null;
28
+ path = path.replace(/\.git$/i, '').replace(/\/+$/, '');
29
+ if (!path)
30
+ return null;
31
+ const segments = path.split('/');
32
+ const name = segments.at(-1);
33
+ if (!name)
34
+ return null;
35
+ return {
36
+ url: `https://${host}/${path}`,
37
+ owner: segments.at(-2) ?? null,
38
+ name,
39
+ };
40
+ }
41
+ /** Repo-name-derived slug matching the server's PROJECT_SLUG_PATTERN. */
42
+ export function slugFromName(name) {
43
+ const slug = name
44
+ .trim()
45
+ .toLowerCase()
46
+ .replace(/[^a-z0-9]+/g, '-')
47
+ .replace(/^-+|-+$/g, '')
48
+ .slice(0, 100)
49
+ .replace(/-+$/, '');
50
+ return slug || 'project';
51
+ }
52
+ /** Strip inline markdown (links, images, emphasis, code ticks) to plain text. */
53
+ function stripInlineMarkdown(line) {
54
+ return line
55
+ .replace(/!\[[^\]]*\]\([^)]*\)/g, '')
56
+ .replace(/\[([^\]]*)\]\([^)]*\)/g, '$1')
57
+ .replace(/[*_`]/g, '')
58
+ .trim();
59
+ }
60
+ /** The first ATX heading's text, e.g. `# hacklab` -> `hacklab`. */
61
+ export function titleFromReadme(markdown) {
62
+ for (const line of markdown.split('\n')) {
63
+ const match = line.match(/^#{1,2}\s+(.+?)\s*#*\s*$/);
64
+ if (match?.[1]) {
65
+ const text = stripInlineMarkdown(match[1]);
66
+ if (text)
67
+ return text;
68
+ }
69
+ }
70
+ return null;
71
+ }
72
+ /**
73
+ * The first prose paragraph of a README: skips headings, badge/image-only
74
+ * lines, block quotes, code fences, and HTML, then joins the first run of
75
+ * plain text lines. This is the description fallback when package.json has
76
+ * none.
77
+ */
78
+ export function firstParagraph(markdown) {
79
+ const collected = [];
80
+ let inFence = false;
81
+ for (const raw of markdown.split('\n')) {
82
+ const line = raw.trim();
83
+ if (/^(```|~~~)/.test(line)) {
84
+ inFence = !inFence;
85
+ continue;
86
+ }
87
+ if (inFence)
88
+ continue;
89
+ const isProse = line.length > 0 &&
90
+ !/^#/.test(line) &&
91
+ !/^>/.test(line) &&
92
+ !/^[-*+]\s/.test(line) &&
93
+ !/^\d+\.\s/.test(line) &&
94
+ !/^</.test(line) &&
95
+ !/^\|/.test(line) &&
96
+ !/^(---|===)/.test(line) &&
97
+ stripInlineMarkdown(line).length > 0;
98
+ if (isProse) {
99
+ collected.push(stripInlineMarkdown(line));
100
+ continue;
101
+ }
102
+ if (collected.length > 0)
103
+ break;
104
+ }
105
+ if (collected.length === 0)
106
+ return null;
107
+ return collected.join(' ').replace(/\s+/g, ' ').slice(0, 2000).trim();
108
+ }
109
+ // Dependency -> tag. Deliberately coarse: tags are for browsing, not a
110
+ // lockfile inventory, so only broadly recognizable stack names make the cut.
111
+ const DEP_TAGS = [
112
+ [/^next$/, 'nextjs'],
113
+ [/^react(-dom)?$/, 'react'],
114
+ [/^vue$/, 'vue'],
115
+ [/^svelte$/, 'svelte'],
116
+ [/^@angular\//, 'angular'],
117
+ [/^solid-js$/, 'solidjs'],
118
+ [/^astro$/, 'astro'],
119
+ [/^expo$|^react-native$/, 'react-native'],
120
+ [/^electron$/, 'electron'],
121
+ [/^tailwindcss$/, 'tailwind'],
122
+ [/^typescript$/, 'typescript'],
123
+ [/^drizzle-orm$/, 'drizzle'],
124
+ [/^@prisma\/client$|^prisma$/, 'prisma'],
125
+ [/^hono$/, 'hono'],
126
+ [/^express$/, 'express'],
127
+ [/^fastify$/, 'fastify'],
128
+ [/^@trpc\//, 'trpc'],
129
+ [/^ai$|^openai$|^@anthropic-ai\//, 'ai'],
130
+ ];
131
+ const MAX_TAGS = 10;
132
+ /** package.json keywords first, then detected stack tags, deduped, capped. */
133
+ export function detectTags(pkg) {
134
+ const tags = [];
135
+ const push = (tag) => {
136
+ const t = tag.trim().toLowerCase().slice(0, 50);
137
+ if (t && !tags.includes(t))
138
+ tags.push(t);
139
+ };
140
+ if (Array.isArray(pkg.keywords)) {
141
+ for (const k of pkg.keywords) {
142
+ if (typeof k === 'string')
143
+ push(k);
144
+ }
145
+ }
146
+ const deps = { ...pkg.devDependencies, ...pkg.dependencies };
147
+ for (const dep of Object.keys(deps)) {
148
+ for (const [pattern, tag] of DEP_TAGS) {
149
+ if (pattern.test(dep))
150
+ push(tag);
151
+ }
152
+ }
153
+ return tags.slice(0, MAX_TAGS);
154
+ }
155
+ async function git(cwd, ...args) {
156
+ try {
157
+ const { stdout } = await execFileAsync('git', ['-C', cwd, ...args]);
158
+ return stdout.trim() || null;
159
+ }
160
+ catch {
161
+ return null;
162
+ }
163
+ }
164
+ /** The repo root for cwd, or null when not inside a git work tree. */
165
+ export async function findRepoRoot(cwd) {
166
+ return git(cwd, 'rev-parse', '--show-toplevel');
167
+ }
168
+ async function readIfExists(path) {
169
+ try {
170
+ return await readFile(path, 'utf8');
171
+ }
172
+ catch {
173
+ return null;
174
+ }
175
+ }
176
+ /**
177
+ * Build the best project we can from the repo at `root`. Precedence per
178
+ * field: README title > repo name; package.json description > README first
179
+ * paragraph; homepage is the only liveUrl source (anything else is guessing).
180
+ */
181
+ export async function inferProject(root) {
182
+ const remoteUrl = await git(root, 'remote', 'get-url', 'origin');
183
+ const remote = remoteUrl ? normalizeRepoUrl(remoteUrl) : null;
184
+ let pkg = {};
185
+ const pkgRaw = await readIfExists(join(root, 'package.json'));
186
+ if (pkgRaw) {
187
+ try {
188
+ pkg = JSON.parse(pkgRaw);
189
+ }
190
+ catch {
191
+ // A broken package.json costs its fields, nothing more.
192
+ }
193
+ }
194
+ const readme = (await readIfExists(join(root, 'README.md'))) ??
195
+ (await readIfExists(join(root, 'readme.md')));
196
+ const dirName = root.split('/').filter(Boolean).pop() ?? 'project';
197
+ const name = remote?.name ?? dirName;
198
+ const homepage = typeof pkg.homepage === 'string' && /^https?:\/\//.test(pkg.homepage)
199
+ ? pkg.homepage
200
+ : null;
201
+ return {
202
+ slug: slugFromName(name),
203
+ title: (readme ? titleFromReadme(readme) : null) ?? name,
204
+ description: pkg.description?.trim() || (readme ? firstParagraph(readme) : null),
205
+ tags: detectTags(pkg),
206
+ repoUrl: remote?.url ?? null,
207
+ liveUrl: homepage,
208
+ content: readme,
209
+ };
210
+ }
211
+ //# sourceMappingURL=project-infer.js.map