hacklab 0.22.3 → 26.921.2

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.
Files changed (259) hide show
  1. package/README.md +39 -415
  2. package/dist/index.js +317 -23
  3. package/package.json +22 -40
  4. package/LICENSE +0 -21
  5. package/dist/agent-handoff.d.ts +0 -51
  6. package/dist/agent-handoff.d.ts.map +0 -1
  7. package/dist/agent-handoff.js +0 -91
  8. package/dist/agent-handoff.js.map +0 -1
  9. package/dist/api-client.d.ts +0 -18
  10. package/dist/api-client.d.ts.map +0 -1
  11. package/dist/api-client.js +0 -40
  12. package/dist/api-client.js.map +0 -1
  13. package/dist/belt.d.ts +0 -25
  14. package/dist/belt.d.ts.map +0 -1
  15. package/dist/belt.js +0 -113
  16. package/dist/belt.js.map +0 -1
  17. package/dist/card.d.ts +0 -66
  18. package/dist/card.d.ts.map +0 -1
  19. package/dist/card.js +0 -151
  20. package/dist/card.js.map +0 -1
  21. package/dist/cli.d.ts +0 -2
  22. package/dist/cli.d.ts.map +0 -1
  23. package/dist/cli.js +0 -238
  24. package/dist/cli.js.map +0 -1
  25. package/dist/commands/book.d.ts +0 -14
  26. package/dist/commands/book.d.ts.map +0 -1
  27. package/dist/commands/book.js +0 -90
  28. package/dist/commands/book.js.map +0 -1
  29. package/dist/commands/chat.d.ts +0 -36
  30. package/dist/commands/chat.d.ts.map +0 -1
  31. package/dist/commands/chat.js +0 -642
  32. package/dist/commands/chat.js.map +0 -1
  33. package/dist/commands/config.d.ts +0 -2
  34. package/dist/commands/config.d.ts.map +0 -1
  35. package/dist/commands/config.js +0 -84
  36. package/dist/commands/config.js.map +0 -1
  37. package/dist/commands/daemon.d.ts +0 -19
  38. package/dist/commands/daemon.d.ts.map +0 -1
  39. package/dist/commands/daemon.js +0 -72
  40. package/dist/commands/daemon.js.map +0 -1
  41. package/dist/commands/drop.d.ts +0 -12
  42. package/dist/commands/drop.d.ts.map +0 -1
  43. package/dist/commands/drop.js +0 -103
  44. package/dist/commands/drop.js.map +0 -1
  45. package/dist/commands/drops.d.ts +0 -9
  46. package/dist/commands/drops.d.ts.map +0 -1
  47. package/dist/commands/drops.js +0 -96
  48. package/dist/commands/drops.js.map +0 -1
  49. package/dist/commands/essay.d.ts +0 -40
  50. package/dist/commands/essay.d.ts.map +0 -1
  51. package/dist/commands/essay.js +0 -651
  52. package/dist/commands/essay.js.map +0 -1
  53. package/dist/commands/hackathon.d.ts +0 -2
  54. package/dist/commands/hackathon.d.ts.map +0 -1
  55. package/dist/commands/hackathon.js +0 -893
  56. package/dist/commands/hackathon.js.map +0 -1
  57. package/dist/commands/hacker.d.ts +0 -2
  58. package/dist/commands/hacker.d.ts.map +0 -1
  59. package/dist/commands/hacker.js +0 -87
  60. package/dist/commands/hacker.js.map +0 -1
  61. package/dist/commands/jobs.d.ts +0 -18
  62. package/dist/commands/jobs.d.ts.map +0 -1
  63. package/dist/commands/jobs.js +0 -138
  64. package/dist/commands/jobs.js.map +0 -1
  65. package/dist/commands/keys.d.ts +0 -2
  66. package/dist/commands/keys.d.ts.map +0 -1
  67. package/dist/commands/keys.js +0 -138
  68. package/dist/commands/keys.js.map +0 -1
  69. package/dist/commands/login.d.ts +0 -83
  70. package/dist/commands/login.d.ts.map +0 -1
  71. package/dist/commands/login.js +0 -236
  72. package/dist/commands/login.js.map +0 -1
  73. package/dist/commands/logout.d.ts +0 -2
  74. package/dist/commands/logout.d.ts.map +0 -1
  75. package/dist/commands/logout.js +0 -22
  76. package/dist/commands/logout.js.map +0 -1
  77. package/dist/commands/org-jobs.d.ts +0 -50
  78. package/dist/commands/org-jobs.d.ts.map +0 -1
  79. package/dist/commands/org-jobs.js +0 -383
  80. package/dist/commands/org-jobs.js.map +0 -1
  81. package/dist/commands/org.d.ts +0 -67
  82. package/dist/commands/org.d.ts.map +0 -1
  83. package/dist/commands/org.js +0 -1244
  84. package/dist/commands/org.js.map +0 -1
  85. package/dist/commands/ping.d.ts +0 -2
  86. package/dist/commands/ping.d.ts.map +0 -1
  87. package/dist/commands/ping.js +0 -50
  88. package/dist/commands/ping.js.map +0 -1
  89. package/dist/commands/profile.d.ts +0 -72
  90. package/dist/commands/profile.d.ts.map +0 -1
  91. package/dist/commands/profile.js +0 -672
  92. package/dist/commands/profile.js.map +0 -1
  93. package/dist/commands/project.d.ts +0 -2
  94. package/dist/commands/project.d.ts.map +0 -1
  95. package/dist/commands/project.js +0 -583
  96. package/dist/commands/project.js.map +0 -1
  97. package/dist/commands/referral.d.ts +0 -6
  98. package/dist/commands/referral.d.ts.map +0 -1
  99. package/dist/commands/referral.js +0 -93
  100. package/dist/commands/referral.js.map +0 -1
  101. package/dist/commands/rtfm.d.ts +0 -9
  102. package/dist/commands/rtfm.d.ts.map +0 -1
  103. package/dist/commands/rtfm.js +0 -114
  104. package/dist/commands/rtfm.js.map +0 -1
  105. package/dist/commands/scan.d.ts +0 -13
  106. package/dist/commands/scan.d.ts.map +0 -1
  107. package/dist/commands/scan.js +0 -286
  108. package/dist/commands/scan.js.map +0 -1
  109. package/dist/commands/scout.d.ts +0 -18
  110. package/dist/commands/scout.d.ts.map +0 -1
  111. package/dist/commands/scout.js +0 -232
  112. package/dist/commands/scout.js.map +0 -1
  113. package/dist/commands/setup-rail.d.ts +0 -58
  114. package/dist/commands/setup-rail.d.ts.map +0 -1
  115. package/dist/commands/setup-rail.js +0 -135
  116. package/dist/commands/setup-rail.js.map +0 -1
  117. package/dist/commands/setup.d.ts +0 -29
  118. package/dist/commands/setup.d.ts.map +0 -1
  119. package/dist/commands/setup.js +0 -442
  120. package/dist/commands/setup.js.map +0 -1
  121. package/dist/commands/sync.d.ts +0 -9
  122. package/dist/commands/sync.d.ts.map +0 -1
  123. package/dist/commands/sync.js +0 -345
  124. package/dist/commands/sync.js.map +0 -1
  125. package/dist/commands/update.d.ts +0 -2
  126. package/dist/commands/update.d.ts.map +0 -1
  127. package/dist/commands/update.js +0 -92
  128. package/dist/commands/update.js.map +0 -1
  129. package/dist/commands/whoami.d.ts +0 -2
  130. package/dist/commands/whoami.d.ts.map +0 -1
  131. package/dist/commands/whoami.js +0 -41
  132. package/dist/commands/whoami.js.map +0 -1
  133. package/dist/config.d.ts +0 -78
  134. package/dist/config.d.ts.map +0 -1
  135. package/dist/config.js +0 -93
  136. package/dist/config.js.map +0 -1
  137. package/dist/daily-sync.d.ts +0 -150
  138. package/dist/daily-sync.d.ts.map +0 -1
  139. package/dist/daily-sync.js +0 -665
  140. package/dist/daily-sync.js.map +0 -1
  141. package/dist/dossier.d.ts +0 -73
  142. package/dist/dossier.d.ts.map +0 -1
  143. package/dist/dossier.js +0 -141
  144. package/dist/dossier.js.map +0 -1
  145. package/dist/field-normalize.d.ts +0 -4
  146. package/dist/field-normalize.d.ts.map +0 -1
  147. package/dist/field-normalize.js +0 -8
  148. package/dist/field-normalize.js.map +0 -1
  149. package/dist/index.d.ts +0 -3
  150. package/dist/index.d.ts.map +0 -1
  151. package/dist/index.js.map +0 -1
  152. package/dist/job-fields.d.ts +0 -47
  153. package/dist/job-fields.d.ts.map +0 -1
  154. package/dist/job-fields.js +0 -148
  155. package/dist/job-fields.js.map +0 -1
  156. package/dist/machine.d.ts +0 -15
  157. package/dist/machine.d.ts.map +0 -1
  158. package/dist/machine.js +0 -64
  159. package/dist/machine.js.map +0 -1
  160. package/dist/org-fields.d.ts +0 -57
  161. package/dist/org-fields.d.ts.map +0 -1
  162. package/dist/org-fields.js +0 -167
  163. package/dist/org-fields.js.map +0 -1
  164. package/dist/posthog.d.ts +0 -38
  165. package/dist/posthog.d.ts.map +0 -1
  166. package/dist/posthog.js +0 -224
  167. package/dist/posthog.js.map +0 -1
  168. package/dist/project-fields.d.ts +0 -40
  169. package/dist/project-fields.d.ts.map +0 -1
  170. package/dist/project-fields.js +0 -79
  171. package/dist/project-fields.js.map +0 -1
  172. package/dist/prompt-consent.d.ts +0 -73
  173. package/dist/prompt-consent.d.ts.map +0 -1
  174. package/dist/prompt-consent.js +0 -191
  175. package/dist/prompt-consent.js.map +0 -1
  176. package/dist/prompt-stats.d.ts +0 -226
  177. package/dist/prompt-stats.d.ts.map +0 -1
  178. package/dist/prompt-stats.js +0 -561
  179. package/dist/prompt-stats.js.map +0 -1
  180. package/dist/referral.d.ts +0 -14
  181. package/dist/referral.d.ts.map +0 -1
  182. package/dist/referral.js +0 -28
  183. package/dist/referral.js.map +0 -1
  184. package/dist/registry.d.ts +0 -15
  185. package/dist/registry.d.ts.map +0 -1
  186. package/dist/registry.js +0 -180
  187. package/dist/registry.js.map +0 -1
  188. package/dist/resolve-command.d.ts +0 -20
  189. package/dist/resolve-command.d.ts.map +0 -1
  190. package/dist/resolve-command.js +0 -21
  191. package/dist/resolve-command.js.map +0 -1
  192. package/dist/scanners/antigravity.d.ts +0 -41
  193. package/dist/scanners/antigravity.d.ts.map +0 -1
  194. package/dist/scanners/antigravity.js +0 -210
  195. package/dist/scanners/antigravity.js.map +0 -1
  196. package/dist/scanners/github-copilot-telemetry.d.ts +0 -23
  197. package/dist/scanners/github-copilot-telemetry.d.ts.map +0 -1
  198. package/dist/scanners/github-copilot-telemetry.js +0 -220
  199. package/dist/scanners/github-copilot-telemetry.js.map +0 -1
  200. package/dist/scanners/github-copilot.d.ts +0 -68
  201. package/dist/scanners/github-copilot.d.ts.map +0 -1
  202. package/dist/scanners/github-copilot.js +0 -262
  203. package/dist/scanners/github-copilot.js.map +0 -1
  204. package/dist/scanners/incremental.d.ts +0 -282
  205. package/dist/scanners/incremental.d.ts.map +0 -1
  206. package/dist/scanners/incremental.js +0 -788
  207. package/dist/scanners/incremental.js.map +0 -1
  208. package/dist/scanners/index.d.ts +0 -116
  209. package/dist/scanners/index.d.ts.map +0 -1
  210. package/dist/scanners/index.js +0 -726
  211. package/dist/scanners/index.js.map +0 -1
  212. package/dist/scanners/sqlite.d.ts +0 -18
  213. package/dist/scanners/sqlite.d.ts.map +0 -1
  214. package/dist/scanners/sqlite.js +0 -63
  215. package/dist/scanners/sqlite.js.map +0 -1
  216. package/dist/scanners/util.d.ts +0 -112
  217. package/dist/scanners/util.d.ts.map +0 -1
  218. package/dist/scanners/util.js +0 -167
  219. package/dist/scanners/util.js.map +0 -1
  220. package/dist/session.d.ts +0 -106
  221. package/dist/session.d.ts.map +0 -1
  222. package/dist/session.js +0 -191
  223. package/dist/session.js.map +0 -1
  224. package/dist/share-card-colors.d.ts +0 -19
  225. package/dist/share-card-colors.d.ts.map +0 -1
  226. package/dist/share-card-colors.js +0 -36
  227. package/dist/share-card-colors.js.map +0 -1
  228. package/dist/share-card.d.ts +0 -31
  229. package/dist/share-card.d.ts.map +0 -1
  230. package/dist/share-card.js +0 -380
  231. package/dist/share-card.js.map +0 -1
  232. package/dist/share.d.ts +0 -17
  233. package/dist/share.d.ts.map +0 -1
  234. package/dist/share.js +0 -75
  235. package/dist/share.js.map +0 -1
  236. package/dist/sync.d.ts +0 -100
  237. package/dist/sync.d.ts.map +0 -1
  238. package/dist/sync.js +0 -304
  239. package/dist/sync.js.map +0 -1
  240. package/dist/ui.d.ts +0 -33
  241. package/dist/ui.d.ts.map +0 -1
  242. package/dist/ui.js +0 -127
  243. package/dist/ui.js.map +0 -1
  244. package/dist/utils/npmGlobal.d.ts +0 -30
  245. package/dist/utils/npmGlobal.d.ts.map +0 -1
  246. package/dist/utils/npmGlobal.js +0 -69
  247. package/dist/utils/npmGlobal.js.map +0 -1
  248. package/dist/utils/openBrowser.d.ts +0 -14
  249. package/dist/utils/openBrowser.d.ts.map +0 -1
  250. package/dist/utils/openBrowser.js +0 -47
  251. package/dist/utils/openBrowser.js.map +0 -1
  252. package/dist/utils/updateCheck.d.ts +0 -16
  253. package/dist/utils/updateCheck.d.ts.map +0 -1
  254. package/dist/utils/updateCheck.js +0 -111
  255. package/dist/utils/updateCheck.js.map +0 -1
  256. package/dist/utils/waitForEnter.d.ts +0 -12
  257. package/dist/utils/waitForEnter.d.ts.map +0 -1
  258. package/dist/utils/waitForEnter.js +0 -43
  259. package/dist/utils/waitForEnter.js.map +0 -1
@@ -1,672 +0,0 @@
1
- import { readFile } from 'node:fs/promises';
2
- import * as clack from '@clack/prompts';
3
- import { parse as parseYaml } from 'yaml';
4
- import { emitJsonError, readApiError, requireSession } from '../api-client.js';
5
- import { agentProfilePath, renderDossier, } from '../dossier.js';
6
- import { ensureScheme, FALSE_WORDS, TRUE_WORDS } from '../field-normalize.js';
7
- import { captureEvent } from '../posthog.js';
8
- import { resolveCommand } from '../resolve-command.js';
9
- import { resolveAppUrl } from '../session.js';
10
- import { fetchApi } from '../sync.js';
11
- import { bold, dim, error, info, link, success } from '../ui.js';
12
- // `hacklab profile` — view and edit your own profile. Thin wrapper over
13
- // GET/PATCH /api/hackers/me: the server owns validation (lengths, URL rules);
14
- // this side only shapes input, so agents get one-shot writes (`set`, `apply
15
- // profile.yaml`) and humans get an org-style autosave editor (`edit`).
16
- const SUBCOMMANDS = ['view', 'set', 'edit', 'apply'];
17
- const ME_PATH = '/api/hackers/me?src=cli';
18
- // Mirrors the server's PROFILE_LINK_KEYS (apps/web/lib/profile-fields.ts) —
19
- // the real validation boundary; this side just shapes input. Same list, same
20
- // order, so the CLI, the settings form, and the public profile agree.
21
- export const PROFILE_FIELDS = [
22
- { key: 'displayName', name: 'name', kind: 'text', hint: 'display name' },
23
- { key: 'bio', name: 'bio', kind: 'text', hint: 'short bio' },
24
- {
25
- key: 'profileReadme',
26
- name: 'readme',
27
- kind: 'markdown',
28
- hint: 'markdown or --file profile.md',
29
- },
30
- { key: 'websiteUrl', name: 'website', kind: 'url', hint: 'your-site.com' },
31
- { key: 'blogUrl', name: 'blog', kind: 'url', hint: 'your-blog.com' },
32
- {
33
- key: 'xUrl',
34
- name: 'x',
35
- kind: 'handle',
36
- base: 'https://x.com/',
37
- strip: /^(?:https?:\/\/)?(?:www\.)?(?:x|twitter)\.com\//i,
38
- hint: 'handle or x.com url',
39
- },
40
- {
41
- key: 'youtubeUrl',
42
- name: 'youtube',
43
- kind: 'handle',
44
- base: 'https://youtube.com/',
45
- handlePath: '@',
46
- strip: /^(?:https?:\/\/)?(?:www\.|m\.)?youtube\.com\//i,
47
- hint: '@channel or youtube url',
48
- },
49
- {
50
- key: 'instagramUrl',
51
- name: 'instagram',
52
- kind: 'handle',
53
- base: 'https://instagram.com/',
54
- strip: /^(?:https?:\/\/)?(?:www\.)?instagram\.com\//i,
55
- hint: 'handle or instagram url',
56
- },
57
- {
58
- key: 'goodreadsUrl',
59
- name: 'goodreads',
60
- kind: 'handle',
61
- base: 'https://www.goodreads.com/',
62
- handlePath: 'user/show/',
63
- strip: /^(?:https?:\/\/)?(?:www\.)?goodreads\.com\//i,
64
- hint: 'user id or goodreads url',
65
- },
66
- {
67
- key: 'rssFeedUrl',
68
- name: 'rss',
69
- kind: 'url',
70
- hint: 'your-site.com/rss.xml',
71
- },
72
- {
73
- key: 'openToWork',
74
- name: 'open-to-work',
75
- kind: 'boolean',
76
- hint: 'yes / no',
77
- },
78
- ];
79
- const FIELD_NAMES = PROFILE_FIELDS.map((f) => f.name);
80
- /** Strip repeatedly until stable, so a pasted double prefix can't survive. */
81
- function stripAll(pattern, value) {
82
- let out = value;
83
- let prev;
84
- do {
85
- prev = out;
86
- out = out.replace(pattern, '');
87
- } while (out !== prev);
88
- return out;
89
- }
90
- /**
91
- * Shape one raw CLI string into the value the API expects. Empty clears the
92
- * field. URLs get https:// prepended when the scheme is missing; handle fields
93
- * additionally accept a bare handle (`@mattbratos`, `mattbratos`) or a pasted
94
- * profile URL and rebuild the canonical link. A pasted URL on the canonical
95
- * host keeps its path (`x.com/me/status/1`, `youtube.com/channel/UC…`) — only
96
- * a bare handle gets the handle path. Real validation stays server-side.
97
- */
98
- export function normalizeFieldValue(field, raw) {
99
- const trimmed = raw.trim();
100
- if (!trimmed)
101
- return { ok: true, value: null };
102
- if (field.kind === 'boolean') {
103
- const word = trimmed.toLowerCase();
104
- if (TRUE_WORDS.has(word))
105
- return { ok: true, value: true };
106
- if (FALSE_WORDS.has(word))
107
- return { ok: true, value: false };
108
- return { ok: false, error: `${field.name} must be yes or no` };
109
- }
110
- if (field.kind === 'text' || field.kind === 'markdown') {
111
- return { ok: true, value: trimmed };
112
- }
113
- if (field.kind === 'handle' && field.base && field.strip) {
114
- const onSite = field.strip.test(trimmed);
115
- const cleaned = stripAll(field.strip, trimmed);
116
- const handle = cleaned.replace(/^@/, '');
117
- if (!handle)
118
- return { ok: true, value: null };
119
- // A path on the canonical site is kept as-is under the canonical origin.
120
- if (onSite && handle.includes('/')) {
121
- return { ok: true, value: field.base + cleaned };
122
- }
123
- // Anything else URL-shaped passes through as a URL; a bare handle gets
124
- // the canonical base. Dots alone don't make it a URL — instagram handles
125
- // can contain them.
126
- if (!onSite && (/^https?:\/\//i.test(handle) || handle.includes('/'))) {
127
- return { ok: true, value: ensureScheme(handle) };
128
- }
129
- return { ok: true, value: field.base + (field.handlePath ?? '') + handle };
130
- }
131
- return { ok: true, value: ensureScheme(trimmed) };
132
- }
133
- /**
134
- * Map a parsed yaml/json document to API fields. Keys may be CLI names
135
- * (`website`, `open-to-work`) or API keys (`websiteUrl`, `openToWork`);
136
- * unknown keys fail loudly so a typo can't silently drop a field.
137
- */
138
- export function parseProfileDocument(doc) {
139
- if (typeof doc !== 'object' || doc === null || Array.isArray(doc)) {
140
- return { ok: false, error: 'expected a mapping of fields to values' };
141
- }
142
- const fields = {};
143
- for (const [rawKey, rawValue] of Object.entries(doc)) {
144
- const field = PROFILE_FIELDS.find((f) => f.name === rawKey || f.key === rawKey);
145
- if (!field) {
146
- return {
147
- ok: false,
148
- error: `unknown field "${rawKey}" (fields: ${FIELD_NAMES.join(', ')})`,
149
- };
150
- }
151
- if (rawValue === null) {
152
- fields[field.key] = null;
153
- continue;
154
- }
155
- if (typeof rawValue === 'boolean' && field.kind === 'boolean') {
156
- fields[field.key] = rawValue;
157
- continue;
158
- }
159
- if (typeof rawValue !== 'string' && typeof rawValue !== 'number') {
160
- return { ok: false, error: `${field.name}: expected a string value` };
161
- }
162
- const normalized = normalizeFieldValue(field, String(rawValue));
163
- if (!normalized.ok)
164
- return normalized;
165
- fields[field.key] = normalized.value;
166
- }
167
- if (Object.keys(fields).length === 0) {
168
- return { ok: false, error: 'no fields to apply' };
169
- }
170
- return { ok: true, fields };
171
- }
172
- /** Value column for human output. */
173
- export function displayValue(field, value) {
174
- if (field.kind === 'boolean')
175
- return value ? 'yes' : 'no';
176
- if (value == null || value === '')
177
- return '';
178
- if (field.kind === 'markdown')
179
- return `${String(value).length} chars`;
180
- return String(value);
181
- }
182
- // Login-shaped (DESIGN.md): only what's set, dim label + value, full
183
- // clickable URLs. No `(not set)` spreadsheet — an empty field is silence.
184
- export function renderProfile(profile, appUrl) {
185
- const lines = [];
186
- const title = profile.displayName
187
- ? `${bold(profile.displayName)} ${dim(`@${profile.handle}`)}`
188
- : bold(`@${profile.handle}`);
189
- lines.push(title);
190
- const rows = [];
191
- for (const f of PROFILE_FIELDS) {
192
- if (f.name === 'name')
193
- continue;
194
- const raw = profile[f.key];
195
- if (f.kind === 'boolean' ? !raw : raw == null || raw === '')
196
- continue;
197
- rows.push({
198
- label: f.name,
199
- value: displayValue(f, raw),
200
- isUrl: f.kind === 'url' || f.kind === 'handle',
201
- });
202
- }
203
- if (profile.githubUsername) {
204
- rows.push({
205
- label: 'github',
206
- value: `https://github.com/${profile.githubUsername}`,
207
- isUrl: true,
208
- });
209
- }
210
- if (rows.length > 0) {
211
- lines.push('');
212
- const width = Math.max(...rows.map((r) => r.label.length));
213
- for (const row of rows) {
214
- const value = row.isUrl ? link(row.value) : row.value;
215
- lines.push(`${dim(row.label.padEnd(width))} ${value}`);
216
- }
217
- }
218
- lines.push('');
219
- lines.push(link(`${appUrl}/${profile.handle}`));
220
- return lines;
221
- }
222
- function printJson(data) {
223
- console.log(JSON.stringify(data, null, 2));
224
- }
225
- // Help is plain console.log (DESIGN.md): no arrows, no echo of what they
226
- // typed, no example dump. A table of the real surface, nothing else.
227
- function table(rows) {
228
- const width = Math.max(...rows.map(([k]) => k.length));
229
- return rows.map(([k, v]) => `${dim(k.padEnd(width))} ${v}`);
230
- }
231
- function usage(exitCode = 1) {
232
- const lines = [
233
- 'profile [view|set|edit|apply]',
234
- '',
235
- ...table([
236
- ['view', 'show your profile'],
237
- ['set <field> <value>', 'set one field'],
238
- ['set <url>', 'x, youtube, instagram, goodreads — field from the host'],
239
- ['edit', 'interactive editor'],
240
- ['apply <file>', 'fields from a yaml/json file'],
241
- ]),
242
- '',
243
- ...table([['--json', 'machine-readable (view, set, apply)']]),
244
- ];
245
- console.log(lines.join('\n'));
246
- process.exit(exitCode);
247
- }
248
- // Help for `hacklab profile set` on its own. The field table is derived from
249
- // PROFILE_FIELDS so the help can't drift from what `set` actually accepts.
250
- function setUsage() {
251
- const lines = [
252
- 'profile set <field> <value>',
253
- 'profile set <url>',
254
- '',
255
- ...table(PROFILE_FIELDS.map((f) => [f.name, f.hint])),
256
- '',
257
- ...table([
258
- ['--clear', 'unset'],
259
- ['--file <path>', 'readme from a file'],
260
- ['--json', 'machine-readable'],
261
- ]),
262
- ];
263
- console.log(lines.join('\n'));
264
- process.exit(0);
265
- }
266
- /**
267
- * `hacklab profile set https://x.com/mattbratos` — no field named, so pick it
268
- * from the host. Only handle fields are inferable: website, blog, and rss all
269
- * live on your own domain, so a bare URL there stays explicit.
270
- */
271
- export function inferFieldFromUrl(token) {
272
- if (!/^(?:https?:\/\/|www\.)|\.[a-z]{2,}(?:\/|$)/i.test(token))
273
- return null;
274
- return (PROFILE_FIELDS.find((f) => f.kind === 'handle' && f.strip?.test(token)) ??
275
- null);
276
- }
277
- async function fetchProfile(session) {
278
- const res = await fetchApi(session, ME_PATH, {
279
- headers: { Authorization: `Bearer ${session.token}` },
280
- });
281
- if (!res.ok)
282
- throw new Error(await readApiError(res, session));
283
- const data = (await res.json().catch(() => null));
284
- if (!data?.profile)
285
- throw new Error('got a malformed response from hacklab');
286
- return data.profile;
287
- }
288
- async function patchProfile(session, fields) {
289
- const res = await fetchApi(session, ME_PATH, {
290
- method: 'PATCH',
291
- headers: {
292
- 'Content-Type': 'application/json',
293
- Authorization: `Bearer ${session.token}`,
294
- },
295
- body: JSON.stringify(fields),
296
- });
297
- if (!res.ok)
298
- throw new Error(await readApiError(res, session));
299
- const data = (await res.json().catch(() => null));
300
- if (!data?.profile)
301
- throw new Error('got a malformed response from hacklab');
302
- return data;
303
- }
304
- async function profileView(args) {
305
- const json = args.includes('--json');
306
- const session = await requireSession(json);
307
- if (json) {
308
- let profile;
309
- try {
310
- profile = await fetchProfile(session);
311
- }
312
- catch (err) {
313
- emitJsonError('error', err instanceof Error ? err.message : String(err));
314
- return;
315
- }
316
- printJson({ schemaVersion: 1, profile });
317
- return;
318
- }
319
- let handle = session.handle;
320
- if (!handle) {
321
- try {
322
- handle = (await fetchProfile(session)).handle;
323
- }
324
- catch (err) {
325
- error(err instanceof Error ? err.message : String(err));
326
- process.exit(1);
327
- }
328
- }
329
- let res;
330
- try {
331
- res = await fetchApi(session, agentProfilePath(handle), {
332
- headers: { Authorization: `Bearer ${session.token}` },
333
- });
334
- }
335
- catch (err) {
336
- error(err instanceof Error ? err.message : String(err));
337
- process.exit(1);
338
- }
339
- if (!res.ok) {
340
- error(await readApiError(res, session));
341
- process.exit(1);
342
- }
343
- const body = (await res.json().catch(() => null));
344
- if (!body?.hacker) {
345
- error('got a malformed response from hacklab');
346
- process.exit(1);
347
- }
348
- console.log(renderDossier(body.hacker, {
349
- self: true,
350
- appUrl: resolveAppUrl(session),
351
- }).join('\n'));
352
- }
353
- async function profileSet(args) {
354
- const json = args.includes('--json');
355
- // Help first: before any flag parsing or requireSession, so `--help` never
356
- // touches the network. A bare `help` only counts in the field slot, so
357
- // `--file help` still reads a file called help.
358
- if (args[0] === 'help' || args.some((a) => a === '--help' || a === '-h')) {
359
- setUsage();
360
- }
361
- const clear = args.includes('--clear');
362
- const fileFlagIndex = args.indexOf('--file');
363
- const filePath = fileFlagIndex >= 0 ? args[fileFlagIndex + 1] : undefined;
364
- if (fileFlagIndex >= 0 && (!filePath || filePath.startsWith('-'))) {
365
- if (json)
366
- emitJsonError('invalid_fields', '--file requires a path');
367
- error('--file requires a path');
368
- process.exit(1);
369
- }
370
- const ignoredIndexes = new Set();
371
- for (const [index, arg] of args.entries()) {
372
- if (arg === '--json' || arg === '--clear')
373
- ignoredIndexes.add(index);
374
- }
375
- if (fileFlagIndex >= 0) {
376
- ignoredIndexes.add(fileFlagIndex);
377
- ignoredIndexes.add(fileFlagIndex + 1);
378
- }
379
- const rest = args.filter((_, index) => !ignoredIndexes.has(index));
380
- const [fieldToken, ...valueParts] = rest;
381
- if (!fieldToken) {
382
- // A JSON consumer asked for machine output — a human help page would be
383
- // unparseable, so keep the error envelope.
384
- if (json) {
385
- emitJsonError('invalid_fields', 'usage: hacklab profile set <field> <value>');
386
- process.exit(1);
387
- }
388
- setUsage();
389
- }
390
- let resolved = resolveCommand(fieldToken, FIELD_NAMES);
391
- // Pasted a URL instead of a field name? Infer the field from its host and
392
- // treat the URL as the value.
393
- if (resolved.kind === 'unknown') {
394
- const inferred = inferFieldFromUrl(fieldToken);
395
- if (inferred) {
396
- if (valueParts.length > 0 || filePath || clear) {
397
- const message = `${fieldToken} is a value — pass the field too: profile set ${inferred.name} ${fieldToken}`;
398
- if (json)
399
- emitJsonError('invalid_fields', message);
400
- error(message);
401
- process.exit(1);
402
- }
403
- resolved = { kind: 'match', name: inferred.name };
404
- valueParts.push(fieldToken);
405
- }
406
- else if (/^(?:https?:\/\/|www\.)/i.test(fieldToken)) {
407
- const message = `can't tell which field ${fieldToken} is — profile set website|blog|rss ${fieldToken}`;
408
- if (json)
409
- emitJsonError('invalid_fields', message);
410
- error(message);
411
- process.exit(1);
412
- }
413
- }
414
- if (resolved.kind === 'ambiguous') {
415
- if (json)
416
- emitJsonError('invalid_fields', `ambiguous field: ${resolved.matches.join(', ')}`);
417
- error(`ambiguous field: ${fieldToken} (${resolved.matches.join(', ')})`);
418
- process.exit(1);
419
- }
420
- if (resolved.kind === 'unknown') {
421
- if (json)
422
- emitJsonError('invalid_fields', `unknown field "${fieldToken}" (fields: ${FIELD_NAMES.join(', ')})`);
423
- error(`unknown field: ${fieldToken} ${dim(`(${FIELD_NAMES.join(', ')})`)}`);
424
- process.exit(1);
425
- }
426
- const field = PROFILE_FIELDS.find((f) => f.name === resolved.name);
427
- if (!field)
428
- usage();
429
- if (clear && filePath) {
430
- if (json)
431
- emitJsonError('invalid_fields', 'use either --file or --clear');
432
- error('use either --file or --clear');
433
- process.exit(1);
434
- }
435
- if (filePath && valueParts.length > 0) {
436
- if (json)
437
- emitJsonError('invalid_fields', 'pass either a value or --file, not both');
438
- error('pass either a value or --file, not both');
439
- process.exit(1);
440
- }
441
- let rawValue = valueParts.join(' ');
442
- if (filePath) {
443
- try {
444
- rawValue = await readFile(filePath, 'utf8');
445
- }
446
- catch (err) {
447
- const code = err.code;
448
- const message = code === 'ENOENT'
449
- ? `file not found: ${filePath}`
450
- : `could not read ${filePath}`;
451
- if (json)
452
- emitJsonError('read_failed', message);
453
- error(message);
454
- process.exit(1);
455
- }
456
- }
457
- if (!clear && !rawValue.trim()) {
458
- if (json)
459
- emitJsonError('invalid_fields', 'pass a value, or --clear');
460
- error(`pass a value, or ${dim('--clear')} to unset`);
461
- process.exit(1);
462
- }
463
- const normalized = clear
464
- ? { ok: true, value: null }
465
- : normalizeFieldValue(field, rawValue);
466
- if (!normalized.ok) {
467
- if (json)
468
- emitJsonError('invalid_fields', normalized.error);
469
- error(normalized.error);
470
- process.exit(1);
471
- }
472
- const session = await requireSession(json);
473
- let result;
474
- try {
475
- result = await patchProfile(session, { [field.key]: normalized.value });
476
- }
477
- catch (err) {
478
- const message = err instanceof Error ? err.message : String(err);
479
- if (json)
480
- emitJsonError('error', message);
481
- error(message);
482
- process.exit(1);
483
- }
484
- await captureEvent(session.handle, 'cli_profile_updated', {
485
- via: 'set',
486
- fields: [field.key],
487
- });
488
- if (json) {
489
- printJson({ schemaVersion: 1, ...result });
490
- return;
491
- }
492
- const saved = displayValue(field, result.profile[field.key]);
493
- const shown = saved && (field.kind === 'url' || field.kind === 'handle')
494
- ? link(saved)
495
- : saved;
496
- success(`saved ${field.name}${shown ? `: ${shown}` : ' (cleared)'}`);
497
- console.log(link(`${resolveAppUrl(session)}/${result.profile.handle}`));
498
- }
499
- async function profileApply(args) {
500
- const json = args.includes('--json');
501
- const path = args.find((a) => !a.startsWith('-'));
502
- if (!path)
503
- usage();
504
- let content;
505
- try {
506
- content = await readFile(path, 'utf8');
507
- }
508
- catch (err) {
509
- const code = err.code;
510
- const message = code === 'ENOENT' ? `file not found: ${path}` : `could not read ${path}`;
511
- if (json)
512
- emitJsonError('read_failed', message);
513
- error(message);
514
- process.exit(1);
515
- }
516
- let doc;
517
- try {
518
- // YAML is a superset of JSON, so one parser covers both file shapes.
519
- doc = parseYaml(content);
520
- }
521
- catch (err) {
522
- const message = `could not parse ${path}: ${err instanceof Error ? err.message : String(err)}`;
523
- if (json)
524
- emitJsonError('parse_failed', message);
525
- error(message);
526
- process.exit(1);
527
- }
528
- const parsed = parseProfileDocument(doc);
529
- if (!parsed.ok) {
530
- if (json)
531
- emitJsonError('invalid_fields', parsed.error);
532
- error(parsed.error);
533
- process.exit(1);
534
- }
535
- const session = await requireSession(json);
536
- let result;
537
- try {
538
- result = await patchProfile(session, parsed.fields);
539
- }
540
- catch (err) {
541
- const message = err instanceof Error ? err.message : String(err);
542
- if (json)
543
- emitJsonError('error', message);
544
- error(message);
545
- process.exit(1);
546
- }
547
- await captureEvent(session.handle, 'cli_profile_updated', {
548
- via: 'apply',
549
- fields: result.updated,
550
- });
551
- if (json) {
552
- printJson({ schemaVersion: 1, ...result });
553
- return;
554
- }
555
- const names = result.updated
556
- .map((key) => PROFILE_FIELDS.find((f) => f.key === key)?.name ?? key)
557
- .join(', ');
558
- success(`saved ${result.updated.length} field${result.updated.length === 1 ? '' : 's'}: ${names}`);
559
- console.log(renderProfile(result.profile, resolveAppUrl(session)).join('\n'));
560
- }
561
- // Autosave editor over your own profile, mirroring `hacklab org`: pick a
562
- // field, set its value, it saves immediately — leaving loses nothing.
563
- async function profileEdit() {
564
- const session = await requireSession(false);
565
- clack.intro(bold('hacklab profile'));
566
- let profile;
567
- try {
568
- profile = await fetchProfile(session);
569
- }
570
- catch (err) {
571
- clack.cancel(err instanceof Error ? err.message : String(err));
572
- process.exit(1);
573
- }
574
- const DONE = '__done__';
575
- let savedCount = 0;
576
- const savedKeys = new Set();
577
- while (true) {
578
- const choice = await clack.select({
579
- message: `editing ${bold(`@${profile.handle}`)} ${dim('(saves as you go)')}`,
580
- options: [
581
- ...PROFILE_FIELDS.map((f) => ({
582
- value: f.name,
583
- label: f.name,
584
- hint: displayValue(f, profile[f.key]) || '(empty)',
585
- })),
586
- { value: DONE, label: '← done' },
587
- ],
588
- });
589
- if (clack.isCancel(choice) || choice === DONE)
590
- break;
591
- const field = PROFILE_FIELDS.find((f) => f.name === choice);
592
- if (!field)
593
- continue;
594
- let next;
595
- if (field.kind === 'boolean') {
596
- const value = await clack.confirm({
597
- message: field.name,
598
- initialValue: Boolean(profile[field.key]),
599
- });
600
- if (clack.isCancel(value))
601
- continue;
602
- next = value;
603
- }
604
- else {
605
- const current = profile[field.key];
606
- const value = await clack.text({
607
- message: `${field.name} ${dim(`(${field.hint}, blank to clear)`)}`,
608
- initialValue: current == null ? '' : String(current),
609
- });
610
- if (clack.isCancel(value))
611
- continue;
612
- const normalized = normalizeFieldValue(field, value);
613
- if (!normalized.ok) {
614
- error(normalized.error);
615
- continue;
616
- }
617
- next = normalized.value;
618
- }
619
- const spin = clack.spinner();
620
- spin.start(`saving ${field.name}`);
621
- try {
622
- const result = await patchProfile(session, { [field.key]: next });
623
- profile = result.profile;
624
- savedCount++;
625
- savedKeys.add(field.key);
626
- spin.stop(`saved ${field.name}`);
627
- }
628
- catch (err) {
629
- spin.stop(`could not save ${field.name}`);
630
- error(err instanceof Error ? err.message : String(err));
631
- // Stay in the loop so they can retry or edit something else.
632
- }
633
- }
634
- if (savedCount > 0) {
635
- await captureEvent(session.handle, 'cli_profile_updated', {
636
- via: 'edit',
637
- fields: [...savedKeys],
638
- });
639
- success(`saved ${savedCount} change${savedCount === 1 ? '' : 's'}`);
640
- info(`${resolveAppUrl(session)}/${profile.handle}`);
641
- }
642
- clack.outro(dim('done.'));
643
- }
644
- export async function profile(args) {
645
- const [subToken, ...rest] = args;
646
- if (subToken === '--help' || subToken === '-h' || subToken === 'help') {
647
- usage(0);
648
- }
649
- // Bare `hacklab profile` shows the profile — the safe default; editing is
650
- // an explicit `edit` away.
651
- if (!subToken || subToken.startsWith('-')) {
652
- return profileView(args);
653
- }
654
- const resolved = resolveCommand(subToken, SUBCOMMANDS);
655
- if (resolved.kind === 'ambiguous') {
656
- error(`ambiguous: profile ${subToken} (${resolved.matches.join(', ')})`);
657
- process.exit(1);
658
- }
659
- if (resolved.kind === 'unknown') {
660
- error(`unknown subcommand: profile ${subToken}`);
661
- usage();
662
- }
663
- if (resolved.name === 'view')
664
- return profileView(rest);
665
- if (resolved.name === 'set')
666
- return profileSet(rest);
667
- if (resolved.name === 'edit')
668
- return profileEdit();
669
- if (resolved.name === 'apply')
670
- return profileApply(rest);
671
- }
672
- //# sourceMappingURL=profile.js.map