hacklab 0.0.2 → 0.3.0

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 (166) hide show
  1. package/README.md +44 -3
  2. package/bin/ddd +19 -0
  3. package/dist/commands/brag.d.ts +3 -6
  4. package/dist/commands/brag.d.ts.map +1 -1
  5. package/dist/commands/brag.js +68 -299
  6. package/dist/commands/brag.js.map +1 -1
  7. package/dist/commands/claim.d.ts +2 -0
  8. package/dist/commands/claim.d.ts.map +1 -0
  9. package/dist/commands/claim.js +534 -0
  10. package/dist/commands/claim.js.map +1 -0
  11. package/dist/commands/config.d.ts +2 -0
  12. package/dist/commands/config.d.ts.map +1 -0
  13. package/dist/commands/config.js +48 -0
  14. package/dist/commands/config.js.map +1 -0
  15. package/dist/commands/dojo-init.d.ts +2 -0
  16. package/dist/commands/dojo-init.d.ts.map +1 -0
  17. package/dist/commands/dojo-init.js +75 -0
  18. package/dist/commands/dojo-init.js.map +1 -0
  19. package/dist/commands/drop.d.ts +2 -0
  20. package/dist/commands/drop.d.ts.map +1 -0
  21. package/dist/commands/drop.js +29 -0
  22. package/dist/commands/drop.js.map +1 -0
  23. package/dist/commands/exam.d.ts +5 -0
  24. package/dist/commands/exam.d.ts.map +1 -0
  25. package/dist/commands/exam.js +224 -0
  26. package/dist/commands/exam.js.map +1 -0
  27. package/dist/commands/login.d.ts +1 -15
  28. package/dist/commands/login.d.ts.map +1 -1
  29. package/dist/commands/login.js +69 -133
  30. package/dist/commands/login.js.map +1 -1
  31. package/dist/commands/scan-profile.d.ts +33 -0
  32. package/dist/commands/scan-profile.d.ts.map +1 -0
  33. package/dist/commands/scan-profile.js +279 -0
  34. package/dist/commands/scan-profile.js.map +1 -0
  35. package/dist/commands/signup.d.ts +11 -0
  36. package/dist/commands/signup.d.ts.map +1 -0
  37. package/dist/commands/signup.js +242 -0
  38. package/dist/commands/signup.js.map +1 -0
  39. package/dist/commands/sync.d.ts +2 -0
  40. package/dist/commands/sync.d.ts.map +1 -0
  41. package/dist/commands/sync.js +55 -0
  42. package/dist/commands/sync.js.map +1 -0
  43. package/dist/commands/whoami.d.ts +1 -3
  44. package/dist/commands/whoami.d.ts.map +1 -1
  45. package/dist/commands/whoami.js +10 -37
  46. package/dist/commands/whoami.js.map +1 -1
  47. package/dist/config.d.ts +7 -0
  48. package/dist/config.d.ts.map +1 -0
  49. package/dist/config.js +18 -0
  50. package/dist/config.js.map +1 -0
  51. package/dist/dd.d.ts +3 -0
  52. package/dist/dd.d.ts.map +1 -0
  53. package/dist/dd.js +28 -0
  54. package/dist/dd.js.map +1 -0
  55. package/dist/index.js +117 -112
  56. package/dist/index.js.map +1 -1
  57. package/dist/project-brag.d.ts +58 -0
  58. package/dist/project-brag.d.ts.map +1 -0
  59. package/dist/project-brag.js +270 -0
  60. package/dist/project-brag.js.map +1 -0
  61. package/dist/scanners/index.d.ts +24 -0
  62. package/dist/scanners/index.d.ts.map +1 -0
  63. package/dist/scanners/index.js +543 -0
  64. package/dist/scanners/index.js.map +1 -0
  65. package/dist/scanners/util.d.ts +83 -0
  66. package/dist/scanners/util.d.ts.map +1 -0
  67. package/dist/scanners/util.js +129 -0
  68. package/dist/scanners/util.js.map +1 -0
  69. package/dist/session.d.ts +17 -13
  70. package/dist/session.d.ts.map +1 -1
  71. package/dist/session.js +46 -155
  72. package/dist/session.js.map +1 -1
  73. package/dist/share-card.d.ts +29 -0
  74. package/dist/share-card.d.ts.map +1 -0
  75. package/dist/share-card.js +401 -0
  76. package/dist/share-card.js.map +1 -0
  77. package/dist/sync.d.ts +64 -0
  78. package/dist/sync.d.ts.map +1 -0
  79. package/dist/sync.js +805 -0
  80. package/dist/sync.js.map +1 -0
  81. package/dist/ui.d.ts +10 -0
  82. package/dist/ui.d.ts.map +1 -0
  83. package/dist/ui.js +21 -0
  84. package/dist/ui.js.map +1 -0
  85. package/dist/utils/openBrowser.js.map +1 -1
  86. package/package.json +36 -29
  87. package/dist/api/client.d.ts +0 -150
  88. package/dist/api/client.d.ts.map +0 -1
  89. package/dist/api/client.js +0 -246
  90. package/dist/api/client.js.map +0 -1
  91. package/dist/commands/admin-waitlist.d.ts +0 -12
  92. package/dist/commands/admin-waitlist.d.ts.map +0 -1
  93. package/dist/commands/admin-waitlist.js +0 -96
  94. package/dist/commands/admin-waitlist.js.map +0 -1
  95. package/dist/commands/brag.utils.d.ts +0 -12
  96. package/dist/commands/brag.utils.d.ts.map +0 -1
  97. package/dist/commands/brag.utils.js +0 -43
  98. package/dist/commands/brag.utils.js.map +0 -1
  99. package/dist/commands/essay.d.ts +0 -18
  100. package/dist/commands/essay.d.ts.map +0 -1
  101. package/dist/commands/essay.js +0 -117
  102. package/dist/commands/essay.js.map +0 -1
  103. package/dist/commands/explore.d.ts +0 -9
  104. package/dist/commands/explore.d.ts.map +0 -1
  105. package/dist/commands/explore.js +0 -32
  106. package/dist/commands/explore.js.map +0 -1
  107. package/dist/commands/init.d.ts +0 -16
  108. package/dist/commands/init.d.ts.map +0 -1
  109. package/dist/commands/init.js +0 -237
  110. package/dist/commands/init.js.map +0 -1
  111. package/dist/commands/join.d.ts +0 -10
  112. package/dist/commands/join.d.ts.map +0 -1
  113. package/dist/commands/join.js +0 -314
  114. package/dist/commands/join.js.map +0 -1
  115. package/dist/commands/link.d.ts +0 -17
  116. package/dist/commands/link.d.ts.map +0 -1
  117. package/dist/commands/link.js +0 -36
  118. package/dist/commands/link.js.map +0 -1
  119. package/dist/commands/login.utils.d.ts +0 -3
  120. package/dist/commands/login.utils.d.ts.map +0 -1
  121. package/dist/commands/login.utils.js +0 -13
  122. package/dist/commands/login.utils.js.map +0 -1
  123. package/dist/commands/new.d.ts +0 -15
  124. package/dist/commands/new.d.ts.map +0 -1
  125. package/dist/commands/new.js +0 -172
  126. package/dist/commands/new.js.map +0 -1
  127. package/dist/commands/new.utils.d.ts +0 -7
  128. package/dist/commands/new.utils.d.ts.map +0 -1
  129. package/dist/commands/new.utils.js +0 -50
  130. package/dist/commands/new.utils.js.map +0 -1
  131. package/dist/commands/onboard.d.ts +0 -10
  132. package/dist/commands/onboard.d.ts.map +0 -1
  133. package/dist/commands/onboard.js +0 -93
  134. package/dist/commands/onboard.js.map +0 -1
  135. package/dist/commands/publish.d.ts +0 -8
  136. package/dist/commands/publish.d.ts.map +0 -1
  137. package/dist/commands/publish.js +0 -47
  138. package/dist/commands/publish.js.map +0 -1
  139. package/dist/commands/tui.d.ts +0 -12
  140. package/dist/commands/tui.d.ts.map +0 -1
  141. package/dist/commands/tui.js +0 -112
  142. package/dist/commands/tui.js.map +0 -1
  143. package/dist/help-format.d.ts +0 -4
  144. package/dist/help-format.d.ts.map +0 -1
  145. package/dist/help-format.js +0 -30
  146. package/dist/help-format.js.map +0 -1
  147. package/dist/lab/config.d.ts +0 -32
  148. package/dist/lab/config.d.ts.map +0 -1
  149. package/dist/lab/config.js +0 -148
  150. package/dist/lab/config.js.map +0 -1
  151. package/dist/lab/contentStatus.d.ts +0 -6
  152. package/dist/lab/contentStatus.d.ts.map +0 -1
  153. package/dist/lab/contentStatus.js +0 -19
  154. package/dist/lab/contentStatus.js.map +0 -1
  155. package/dist/ui/banner.d.ts +0 -2
  156. package/dist/ui/banner.d.ts.map +0 -1
  157. package/dist/ui/banner.js +0 -22
  158. package/dist/ui/banner.js.map +0 -1
  159. package/dist/utils/findRepoRoot.d.ts +0 -2
  160. package/dist/utils/findRepoRoot.d.ts.map +0 -1
  161. package/dist/utils/findRepoRoot.js +0 -17
  162. package/dist/utils/findRepoRoot.js.map +0 -1
  163. package/dist/utils/pathExists.d.ts +0 -2
  164. package/dist/utils/pathExists.d.ts.map +0 -1
  165. package/dist/utils/pathExists.js +0 -11
  166. package/dist/utils/pathExists.js.map +0 -1
@@ -0,0 +1,75 @@
1
+ import { execSync } from 'node:child_process';
2
+ import { existsSync } from 'node:fs';
3
+ import { mkdir, writeFile } from 'node:fs/promises';
4
+ import { resolve } from 'node:path';
5
+ import * as clack from '@clack/prompts';
6
+ import { loadSession } from '../session.js';
7
+ import { dim, error, info } from '../ui.js';
8
+ import { login } from './login.js';
9
+ export async function dojoInit(pathArg) {
10
+ clack.intro('hacklab dojo init');
11
+ // Check auth
12
+ let session = await loadSession();
13
+ if (!session) {
14
+ info('you need to log in first');
15
+ await login();
16
+ session = await loadSession();
17
+ if (!session) {
18
+ error('login failed');
19
+ process.exit(1);
20
+ }
21
+ }
22
+ // Resolve path
23
+ let dojoPath;
24
+ if (pathArg) {
25
+ dojoPath = resolve(pathArg.replace(/^~/, process.env.HOME ?? '~'));
26
+ }
27
+ else {
28
+ const input = await clack.text({
29
+ message: 'where should your dojo live?',
30
+ placeholder: '~/dojo',
31
+ defaultValue: resolve(process.env.HOME ?? '~', 'dojo'),
32
+ });
33
+ if (clack.isCancel(input)) {
34
+ clack.cancel('cancelled.');
35
+ process.exit(1);
36
+ }
37
+ dojoPath = resolve(String(input).replace(/^~/, process.env.HOME ?? '~'));
38
+ }
39
+ // Check if already exists
40
+ if (existsSync(resolve(dojoPath, '.hacklab'))) {
41
+ error(`dojo already exists at ${dojoPath}`);
42
+ process.exit(1);
43
+ }
44
+ // Scaffold
45
+ const s = clack.spinner();
46
+ s.start('creating your dojo...');
47
+ await mkdir(resolve(dojoPath, '.hacklab'), { recursive: true });
48
+ await mkdir(resolve(dojoPath, 'quests'), { recursive: true });
49
+ await mkdir(resolve(dojoPath, 'journal'), { recursive: true });
50
+ await writeFile(resolve(dojoPath, '.gitignore'), '.hacklab/credentials.json\n.hacklab/agents/\n', 'utf8');
51
+ await writeFile(resolve(dojoPath, '.hacklab', 'credentials.json'), `${JSON.stringify({
52
+ email: session.email,
53
+ handle: session.handle,
54
+ authenticatedAt: new Date().toISOString(),
55
+ }, null, 2)}\n`, 'utf8');
56
+ // Git init (best-effort)
57
+ try {
58
+ execSync('git init', { cwd: dojoPath, stdio: 'ignore' });
59
+ execSync('git add .gitignore', { cwd: dojoPath, stdio: 'ignore' });
60
+ execSync('git commit -m "init dojo"', {
61
+ cwd: dojoPath,
62
+ stdio: 'ignore',
63
+ });
64
+ }
65
+ catch {
66
+ // fine
67
+ }
68
+ s.stop('dojo created.');
69
+ console.log('');
70
+ info(`path: ${dojoPath}`);
71
+ info(`next: ${dim(`cd ${dojoPath} && woz`)}`);
72
+ console.log('');
73
+ clack.outro(dim('hack the planet.'));
74
+ }
75
+ //# sourceMappingURL=dojo-init.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"dojo-init.js","sourceRoot":"","sources":["../../src/commands/dojo-init.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,2 @@
1
+ export declare function drop(content: string, url?: string): Promise<void>;
2
+ //# sourceMappingURL=drop.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"drop.d.ts","sourceRoot":"","sources":["../../src/commands/drop.ts"],"names":[],"mappings":"AAGA,wBAAsB,IAAI,CAAC,OAAO,EAAE,MAAM,EAAE,GAAG,CAAC,EAAE,MAAM,iBA8BvD"}
@@ -0,0 +1,29 @@
1
+ import { loadSession } from '../session.js';
2
+ import { dim, error, info, success } from '../ui.js';
3
+ export async function drop(content, url) {
4
+ const session = await loadSession();
5
+ if (!session) {
6
+ error('not logged in');
7
+ info(`run ${dim('hacklab login')} first`);
8
+ process.exit(1);
9
+ }
10
+ if (content.length > 280) {
11
+ error(`too long — ${content.length}/280 chars`);
12
+ process.exit(1);
13
+ }
14
+ const res = await fetch(`${session.appUrl}/api/drops`, {
15
+ method: 'POST',
16
+ headers: {
17
+ 'Content-Type': 'application/json',
18
+ Authorization: `Bearer ${session.token}`,
19
+ },
20
+ body: JSON.stringify({ content, url }),
21
+ });
22
+ if (!res.ok) {
23
+ const data = await res.json().catch(() => null);
24
+ error(data?.error ?? `failed (${res.status})`);
25
+ process.exit(1);
26
+ }
27
+ success('dropped.');
28
+ }
29
+ //# sourceMappingURL=drop.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"drop.js","sourceRoot":"","sources":["../../src/commands/drop.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,MAAM,eAAe,CAAA;AAC3C,OAAO,EAAE,GAAG,EAAE,KAAK,EAAE,IAAI,EAAE,OAAO,EAAE,MAAM,UAAU,CAAA;AAEpD,MAAM,CAAC,KAAK,UAAU,IAAI,CAAC,OAAe,EAAE,GAAY;IACtD,MAAM,OAAO,GAAG,MAAM,WAAW,EAAE,CAAA;IAEnC,IAAI,CAAC,OAAO,EAAE,CAAC;QACb,KAAK,CAAC,eAAe,CAAC,CAAA;QACtB,IAAI,CAAC,OAAO,GAAG,CAAC,eAAe,CAAC,QAAQ,CAAC,CAAA;QACzC,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAA;IACjB,CAAC;IAED,IAAI,OAAO,CAAC,MAAM,GAAG,GAAG,EAAE,CAAC;QACzB,KAAK,CAAC,cAAc,OAAO,CAAC,MAAM,YAAY,CAAC,CAAA;QAC/C,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAA;IACjB,CAAC;IAED,MAAM,GAAG,GAAG,MAAM,KAAK,CAAC,GAAG,OAAO,CAAC,MAAM,YAAY,EAAE;QACrD,MAAM,EAAE,MAAM;QACd,OAAO,EAAE;YACP,cAAc,EAAE,kBAAkB;YAClC,aAAa,EAAE,UAAU,OAAO,CAAC,KAAK,EAAE;SACzC;QACD,IAAI,EAAE,IAAI,CAAC,SAAS,CAAC,EAAE,OAAO,EAAE,GAAG,EAAE,CAAC;KACvC,CAAC,CAAA;IAEF,IAAI,CAAC,GAAG,CAAC,EAAE,EAAE,CAAC;QACZ,MAAM,IAAI,GAAG,MAAM,GAAG,CAAC,IAAI,EAAE,CAAC,KAAK,CAAC,GAAG,EAAE,CAAC,IAAI,CAAC,CAAA;QAC/C,KAAK,CAAC,IAAI,EAAE,KAAK,IAAI,WAAW,GAAG,CAAC,MAAM,GAAG,CAAC,CAAA;QAC9C,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAA;IACjB,CAAC;IAED,OAAO,CAAC,UAAU,CAAC,CAAA;AACrB,CAAC"}
@@ -0,0 +1,5 @@
1
+ export declare function exam(flags: {
2
+ pyro: boolean;
3
+ hacker: boolean;
4
+ }): Promise<void>;
5
+ //# sourceMappingURL=exam.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"exam.d.ts","sourceRoot":"","sources":["../../src/commands/exam.ts"],"names":[],"mappings":"AAsCA,wBAAsB,IAAI,CAAC,KAAK,EAAE;IAAE,IAAI,EAAE,OAAO,CAAC;IAAC,MAAM,EAAE,OAAO,CAAA;CAAE,iBA+PnE"}
@@ -0,0 +1,224 @@
1
+ import * as clack from '@clack/prompts';
2
+ import { loadSessionState } from '../session.js';
3
+ import { checkSession, fetchApi, formatBytes, formatTokens, LOGIN_EXPIRED_MESSAGE, runSync, } from '../sync.js';
4
+ import { bold, dim, error, info, success } from '../ui.js';
5
+ import { openBrowser } from '../utils/openBrowser.js';
6
+ async function runHackerExam(session) {
7
+ const res = await fetchApi(session, '/api/exam/hacker', {
8
+ method: 'POST',
9
+ headers: { Authorization: `Bearer ${session.token}` },
10
+ });
11
+ if (res.status === 401) {
12
+ throw new Error(LOGIN_EXPIRED_MESSAGE);
13
+ }
14
+ if (!res.ok) {
15
+ const data = await res.json().catch(() => null);
16
+ throw new Error(data?.error ??
17
+ `hacker exam failed (${res.status})`);
18
+ }
19
+ return res.json();
20
+ }
21
+ export async function exam(flags) {
22
+ const sessionState = await loadSessionState();
23
+ const session = sessionState.session;
24
+ if (!session) {
25
+ error(sessionState.status === 'expired' ? 'login expired' : 'not logged in');
26
+ info(`run ${dim('hacklab login')} first`);
27
+ process.exit(1);
28
+ }
29
+ const runPyro = flags.pyro || (!flags.pyro && !flags.hacker);
30
+ const runHacker = flags.hacker || (!flags.pyro && !flags.hacker);
31
+ const sessionCheck = await checkSession(session);
32
+ if (sessionCheck.status === 'unauthorized') {
33
+ error('login expired');
34
+ info(`run ${dim('hacklab login')} again`);
35
+ process.exit(1);
36
+ }
37
+ if (sessionCheck.status === 'failed') {
38
+ error(sessionCheck.message);
39
+ process.exit(1);
40
+ }
41
+ console.log('');
42
+ console.log(bold(' hacklab exam'));
43
+ if (runPyro && runHacker) {
44
+ console.log(dim(' scanning tokens + github in parallel...'));
45
+ }
46
+ else if (runPyro) {
47
+ console.log(dim(' scanning local AI tool usage...'));
48
+ }
49
+ else {
50
+ console.log(dim(' syncing github contributions...'));
51
+ }
52
+ console.log('');
53
+ let pyroData = null;
54
+ let hackerData = null;
55
+ let pyroError = null;
56
+ let hackerError = null;
57
+ const tasks = [];
58
+ if (runPyro) {
59
+ tasks.push(runSync(session)
60
+ .then((d) => {
61
+ pyroData = d;
62
+ })
63
+ .catch((e) => {
64
+ pyroError = e instanceof Error ? e.message : 'pyro exam failed';
65
+ }));
66
+ }
67
+ if (runHacker) {
68
+ tasks.push(runHackerExam(session)
69
+ .then((d) => {
70
+ hackerData = d;
71
+ })
72
+ .catch((e) => {
73
+ hackerError = e instanceof Error ? e.message : 'hacker exam failed';
74
+ }));
75
+ }
76
+ await Promise.all(tasks);
77
+ // --- Pyro results ---
78
+ if (runPyro) {
79
+ console.log(bold(' PYRO EXAM'));
80
+ if (pyroError) {
81
+ error(`pyro: ${pyroError}`);
82
+ }
83
+ else if (pyroData) {
84
+ const { claudeTotal, codexTotal, cursorTotal, openclawTotal, hermesTotal, opencodeTotal, messagesTotal, cursorStats, result, cursorScanStatus, } = pyroData;
85
+ if (claudeTotal > 0)
86
+ info(` Claude Code ${formatTokens(claudeTotal)} tokens`);
87
+ if (codexTotal > 0)
88
+ info(` Codex ${formatTokens(codexTotal)} tokens`);
89
+ if (cursorTotal > 0)
90
+ info(` Cursor ${formatTokens(cursorTotal)} tokens`);
91
+ if (openclawTotal > 0)
92
+ info(` OpenClaw ${formatTokens(openclawTotal)} tokens`);
93
+ if (hermesTotal > 0)
94
+ info(` Hermes ${formatTokens(hermesTotal)} tokens`);
95
+ if (opencodeTotal > 0)
96
+ info(` OpenCode ${formatTokens(opencodeTotal)} tokens`);
97
+ if (messagesTotal > 0)
98
+ info(` Messages ${formatTokens(messagesTotal)} sent`);
99
+ if (cursorScanStatus.source === 'api') {
100
+ info(` ${dim(`via cursor api · ${cursorScanStatus.events} events`)}`);
101
+ }
102
+ else if (cursorScanStatus.source === 'api-partial') {
103
+ info(` ${dim(`cursor api partial (${cursorScanStatus.reason}) · ${cursorScanStatus.events} events`)}`);
104
+ }
105
+ else if (cursorScanStatus.source === 'api-failed') {
106
+ info(` ${dim(`cursor api failed: ${cursorScanStatus.reason}`)}`);
107
+ }
108
+ else if (cursorStats) {
109
+ info(` ${dim(`local estimate · ${cursorStats.totalCommits} commits · ${cursorStats.avgAiPercent}% AI`)}`);
110
+ }
111
+ console.log('');
112
+ const r = result;
113
+ success(` ${bold(String(r.title))} lv.${r.level} — ${formatTokens(Number(r.tokensTotal))} total`);
114
+ if (Number(r.tokensDelta) > 0)
115
+ info(` +${formatTokens(Number(r.tokensDelta))} since last exam`);
116
+ if (r.rankAfter)
117
+ info(` pyro rank: #${r.rankAfter}`);
118
+ if (r.passedUsers?.length) {
119
+ info(` passed: ${r.passedUsers.map((u) => `@${u}`).join(', ')}`);
120
+ }
121
+ if (Number(r.streak) > 0)
122
+ info(` streak: ${r.streak}d (best ${r.longestStreak}d)`);
123
+ }
124
+ console.log('');
125
+ }
126
+ // --- Hacker results ---
127
+ if (runHacker) {
128
+ console.log(bold(' HACKER EXAM'));
129
+ if (hackerError) {
130
+ error(`hacker: ${hackerError}`);
131
+ }
132
+ else if (hackerData) {
133
+ const h = hackerData;
134
+ success(` ${bold(String(h.title))} lv.${h.level} — ${formatBytes(Number(h.hackerXp))} shipped`);
135
+ if (h.rank)
136
+ info(` rank: #${h.rank}`);
137
+ const topLangs = h.topLanguages;
138
+ if (topLangs?.length) {
139
+ info(` top languages:`);
140
+ for (const lang of topLangs) {
141
+ info(` ${lang.name.padEnd(16)} lv.${lang.level} ${dim(formatBytes(lang.bytes))}`);
142
+ }
143
+ }
144
+ }
145
+ console.log('');
146
+ }
147
+ // --- Share card (pyro only) ---
148
+ if (pyroData && !pyroError) {
149
+ const { claudeTotal, codexTotal, cursorTotal, allEntries, result, models } = pyroData;
150
+ const r = result;
151
+ const handle = session.handle ?? 'you';
152
+ const profileBaseUrl = session.appUrl.replace(/\/$/, '');
153
+ info(`profile: ${bold(`${profileBaseUrl}/${handle}`)}`);
154
+ let cardPath = null;
155
+ try {
156
+ const { generateShareCard, copyToClipboard, displayInTerminal } = await import('../share-card.js');
157
+ const dailyAgg = new Map();
158
+ for (const entry of allEntries) {
159
+ dailyAgg.set(entry.date, (dailyAgg.get(entry.date) ?? 0) + entry.tokens);
160
+ }
161
+ const dailyActivity = Array.from(dailyAgg.entries())
162
+ .map(([date, tokens]) => ({ date, tokens }))
163
+ .sort((a, b) => a.date.localeCompare(b.date));
164
+ const estimatedCost = (claudeTotal / 1_000_000) * 0.6 +
165
+ (codexTotal / 1_000_000) * 0.25 +
166
+ (cursorTotal / 1_000_000) * 0.4;
167
+ cardPath = await generateShareCard({
168
+ handle,
169
+ level: r.level,
170
+ title: r.title,
171
+ beltColor: r.beltColor,
172
+ tokensTotal: Number(r.tokensTotal),
173
+ rank: r.rankAfter ?? 0,
174
+ streak: r.streak ?? 0,
175
+ longestStreak: r.longestStreak ?? 0,
176
+ progressPercent: r.progressPercent ?? 0,
177
+ estimatedCost,
178
+ toolBreakdown: {
179
+ claudeCode: claudeTotal,
180
+ codex: codexTotal,
181
+ cursor: cursorTotal,
182
+ },
183
+ models,
184
+ dailyActivity,
185
+ });
186
+ const { readFile: rf } = await import('node:fs/promises');
187
+ const imgBuf = Buffer.from(await rf(cardPath));
188
+ const displayed = displayInTerminal(imgBuf);
189
+ if (!displayed)
190
+ info(dim('(terminal does not support inline images)'));
191
+ console.log('');
192
+ const copied = await copyToClipboard(cardPath);
193
+ if (copied)
194
+ success('image copied to clipboard!');
195
+ }
196
+ catch {
197
+ // share card is optional
198
+ }
199
+ console.log('');
200
+ if (cardPath) {
201
+ const saveCard = await clack.confirm({
202
+ message: 'Save image to ~/hacklab-card.png?',
203
+ });
204
+ if (saveCard && !clack.isCancel(saveCard)) {
205
+ const { copyFile, mkdir } = await import('node:fs/promises');
206
+ const { homedir } = await import('node:os');
207
+ const { join } = await import('node:path');
208
+ await mkdir(join(homedir(), '.hacklab'), { recursive: true });
209
+ const dest = join(homedir(), 'hacklab-card.png');
210
+ await copyFile(cardPath, dest);
211
+ success(`saved to ${dest}`);
212
+ }
213
+ }
214
+ const shareOnX = await clack.confirm({
215
+ message: `Share on X? Don't forget to attach your image!`,
216
+ });
217
+ if (shareOnX && !clack.isCancel(shareOnX)) {
218
+ const tweetText = encodeURIComponent(`I'm a lv.${r.level} ${r.title} (${r.beltColor} belt) on @hacklab_so with ${formatTokens(Number(r.tokensTotal))} tokens burned.\n\nWhat's your power level?\nhacklab.so/${handle}`);
219
+ await openBrowser(`https://x.com/intent/tweet?text=${tweetText}`);
220
+ }
221
+ }
222
+ clack.outro('hack the planet.');
223
+ }
224
+ //# sourceMappingURL=exam.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"exam.js","sourceRoot":"","sources":["../../src/commands/exam.ts"],"names":[],"mappings":"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"}
@@ -1,16 +1,2 @@
1
- type LoginOptions = {
2
- appUrl?: string;
3
- token?: string;
4
- nickname?: string;
5
- mode?: 'login' | 'join';
6
- showIntro?: boolean;
7
- showSuccess?: boolean;
8
- showAuthorizeUrlWhenOpened?: boolean;
9
- };
10
- export type LoginResult = {
11
- email: string;
12
- nickname?: string;
13
- };
14
- export declare function loginCommand(options?: LoginOptions): Promise<LoginResult>;
15
- export {};
1
+ export declare function login(): Promise<void>;
16
2
  //# sourceMappingURL=login.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"login.d.ts","sourceRoot":"","sources":["../../src/commands/login.ts"],"names":[],"mappings":"AAeA,KAAK,YAAY,GAAG;IAChB,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,IAAI,CAAC,EAAE,OAAO,GAAG,MAAM,CAAA;IACvB,SAAS,CAAC,EAAE,OAAO,CAAA;IACnB,WAAW,CAAC,EAAE,OAAO,CAAA;IACrB,0BAA0B,CAAC,EAAE,OAAO,CAAA;CACvC,CAAA;AAsCD,MAAM,MAAM,WAAW,GAAG;IACtB,KAAK,EAAE,MAAM,CAAA;IACb,QAAQ,CAAC,EAAE,MAAM,CAAA;CACpB,CAAA;AAED,wBAAsB,YAAY,CAAC,OAAO,GAAE,YAAiB,GAAG,OAAO,CAAC,WAAW,CAAC,CAiJnF"}
1
+ {"version":3,"file":"login.d.ts","sourceRoot":"","sources":["../../src/commands/login.ts"],"names":[],"mappings":"AAMA,wBAAsB,KAAK,kBAiF1B"}
@@ -1,140 +1,76 @@
1
- import { cancel, intro, isCancel, note, outro, spinner, text, } from '@clack/prompts';
2
- import { exchangeCliAuth, fetchCliWhoAmI, startCliAuth } from '../api/client.js';
3
- import { saveSession } from '../session.js';
1
+ import { createServer } from 'node:http';
2
+ import * as clack from '@clack/prompts';
3
+ import { getAppUrl, saveSession } from '../session.js';
4
+ import { dim, info, success } from '../ui.js';
4
5
  import { openBrowser } from '../utils/openBrowser.js';
5
- import { getAppUrl } from './login.utils.js';
6
- function sleep(ms) {
7
- return new Promise((resolve) => {
8
- setTimeout(resolve, ms);
9
- });
10
- }
11
- function normalizeNickname(rawValue) {
12
- return rawValue
13
- .trim()
14
- .toLowerCase()
15
- .replace(/\s+/g, '-')
16
- .replace(/[^a-z0-9._-]/g, '');
17
- }
18
- function deriveNicknameFromIdentity(user) {
19
- const email = user.email.trim().toLowerCase();
20
- const emailLocal = email.split('@')[0] || '';
21
- const noReplyMatch = email.match(/^[0-9]+\+([^@]+)@users\.noreply\.github\.com$/);
22
- const fromNoReply = noReplyMatch?.[1] || '';
23
- const candidates = [fromNoReply, user.name || '', emailLocal];
24
- for (const candidate of candidates) {
25
- const normalized = normalizeNickname(candidate);
26
- if (normalized.length >= 2) {
27
- return normalized;
28
- }
29
- }
30
- return undefined;
31
- }
32
- export async function loginCommand(options = {}) {
33
- const mode = options.mode ?? 'login';
34
- const authNoun = mode === 'join' ? 'signup' : 'login';
35
- const commandLabel = mode === 'join' ? 'join' : 'login';
36
- if (options.showIntro !== false) {
37
- intro(mode === 'join' ? 'join hacklab' : 'hacklab cli login');
38
- }
39
- const appUrl = getAppUrl(options.appUrl);
40
- if (options.token) {
41
- const accessToken = options.token.trim();
42
- if (!accessToken) {
43
- cancel('token is required.');
44
- process.exit(1);
45
- }
46
- const identity = await fetchCliWhoAmI(appUrl, accessToken);
47
- if (!identity) {
48
- cancel('invalid token. run `hacklab login` to authenticate again.');
49
- process.exit(1);
50
- }
51
- const nickname = options.nickname || deriveNicknameFromIdentity(identity.user);
52
- await saveSession({
53
- email: identity.user.email,
54
- token: accessToken,
55
- loggedInAt: new Date().toISOString(),
56
- systemMode: identity.user.systemMode,
57
- appUrl,
58
- nickname,
6
+ export async function login() {
7
+ clack.intro('hacklab login');
8
+ const appUrl = getAppUrl();
9
+ // Start a tiny local server to receive the callback
10
+ const { token, email, handle, expiresAt } = await new Promise((resolve, reject) => {
11
+ let timeout;
12
+ const server = createServer((req, res) => {
13
+ const url = new URL(req.url ?? '/', `http://localhost`);
14
+ const token = url.searchParams.get('token');
15
+ const email = url.searchParams.get('email');
16
+ const handle = url.searchParams.get('handle');
17
+ const expiresAt = normalizeDateParam(url.searchParams.get('expiresAt'));
18
+ if (!token || !email) {
19
+ res.writeHead(400, { 'Content-Type': 'text/html' });
20
+ res.end('<h1>login failed</h1><p>missing token. try again.</p>');
21
+ return;
22
+ }
23
+ res.writeHead(200, { 'Content-Type': 'text/html' });
24
+ res.end('<h1>logged in!</h1><p>you can close this tab and go back to your terminal.</p>');
25
+ stopServer();
26
+ resolve({
27
+ token,
28
+ email,
29
+ handle: handle ?? undefined,
30
+ expiresAt,
31
+ });
59
32
  });
60
- if (options.showSuccess !== false) {
61
- const nicknameLabel = nickname ? ` (${nickname})` : '';
62
- outro(mode === 'join'
63
- ? `joined with token as ${identity.user.email}${nicknameLabel}`
64
- : `logged in with token as ${identity.user.email}${nicknameLabel}`);
33
+ server.listen(0, '127.0.0.1', () => {
34
+ const addr = server.address();
35
+ if (!addr || typeof addr === 'string') {
36
+ stopServer();
37
+ reject(new Error('failed to start callback server'));
38
+ return;
39
+ }
40
+ const callbackUrl = `http://127.0.0.1:${addr.port}`;
41
+ const loginUrl = `${appUrl}/cli/auth?callback=${encodeURIComponent(callbackUrl)}`;
42
+ info(`opening browser...`);
43
+ info(`if it doesn't open, visit:`);
44
+ info(` ${loginUrl}`);
45
+ openBrowser(loginUrl);
46
+ });
47
+ // Timeout after 2 minutes
48
+ timeout = setTimeout(() => {
49
+ stopServer();
50
+ reject(new Error('login timed out. try again.'));
51
+ }, 120_000);
52
+ function stopServer() {
53
+ if (timeout)
54
+ clearTimeout(timeout);
55
+ server.close();
65
56
  }
66
- return {
67
- email: identity.user.email,
68
- nickname,
69
- };
70
- }
71
- const startPayload = await startCliAuth(appUrl, {
72
- nickname: options.nickname,
73
57
  });
74
- const opened = await openBrowser(startPayload.authorizeUrl);
75
- if (!opened) {
76
- note(`open this url to continue with github:\n${startPayload.authorizeUrl}`, `github ${authNoun}`);
77
- }
78
- else {
79
- note(options.showAuthorizeUrlWhenOpened === false
80
- ? `browser opened for github ${authNoun}.`
81
- : `browser opened for github ${authNoun}:\n${startPayload.authorizeUrl}`, `github ${authNoun}`);
82
- }
83
- const enterValue = await text({
84
- message: `finish github ${authNoun} in browser, then press enter`,
85
- placeholder: 'press enter',
58
+ await saveSession({
59
+ token,
60
+ email,
61
+ handle,
62
+ appUrl,
63
+ savedAt: new Date().toISOString(),
64
+ expiresAt,
86
65
  });
87
- if (isCancel(enterValue)) {
88
- cancel(`${commandLabel} cancelled.`);
89
- process.exit(1);
90
- }
91
- const waitSpinner = spinner();
92
- waitSpinner.start('waiting for browser approval...');
93
- const deadline = Date.now() + 2 * 60_000;
94
- while (Date.now() < deadline) {
95
- const exchange = await exchangeCliAuth(appUrl, startPayload.requestId);
96
- if (exchange.status === 'approved') {
97
- const nickname = options.nickname || deriveNicknameFromIdentity(exchange.user);
98
- await saveSession({
99
- email: exchange.user.email,
100
- token: exchange.accessToken,
101
- loggedInAt: new Date().toISOString(),
102
- systemMode: exchange.user.systemMode,
103
- appUrl,
104
- nickname,
105
- });
106
- waitSpinner.stop(`github ${authNoun} approved.`);
107
- if (options.showSuccess !== false) {
108
- const nicknameLabel = nickname ? ` (${nickname})` : '';
109
- outro(mode === 'join'
110
- ? `joined as ${exchange.user.email}${nicknameLabel}`
111
- : `logged in as ${exchange.user.email}${nicknameLabel}`);
112
- }
113
- return {
114
- email: exchange.user.email,
115
- nickname,
116
- };
117
- }
118
- if (exchange.status === 'pending') {
119
- await sleep(1_500);
120
- continue;
121
- }
122
- if (exchange.status === 'expired') {
123
- waitSpinner.stop(`cli ${commandLabel} request expired.`);
124
- cancel(`request expired. run \`hacklab ${commandLabel}\` again.`);
125
- process.exit(1);
126
- }
127
- if (exchange.status === 'consumed') {
128
- waitSpinner.stop(`cli ${commandLabel} request already used.`);
129
- cancel(`request already consumed. run \`hacklab ${commandLabel}\` again.`);
130
- process.exit(1);
131
- }
132
- waitSpinner.stop(`cli ${commandLabel} request not found.`);
133
- cancel(`request not found. run \`hacklab ${commandLabel}\` again.`);
134
- process.exit(1);
135
- }
136
- waitSpinner.stop('timed out waiting for approval.');
137
- cancel(`timed out waiting for ${commandLabel} approval. run \`hacklab ${commandLabel}\` again.`);
138
- process.exit(1);
66
+ const label = handle ? `${email} (${handle})` : email;
67
+ success(`logged in as ${label}`);
68
+ clack.outro(dim('hack the planet.'));
69
+ }
70
+ function normalizeDateParam(value) {
71
+ if (!value)
72
+ return undefined;
73
+ const date = new Date(value);
74
+ return Number.isNaN(date.getTime()) ? undefined : date.toISOString();
139
75
  }
140
76
  //# sourceMappingURL=login.js.map