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.
- package/README.md +39 -415
- package/dist/index.js +317 -23
- package/package.json +22 -40
- package/LICENSE +0 -21
- package/dist/agent-handoff.d.ts +0 -51
- package/dist/agent-handoff.d.ts.map +0 -1
- package/dist/agent-handoff.js +0 -91
- package/dist/agent-handoff.js.map +0 -1
- package/dist/api-client.d.ts +0 -18
- package/dist/api-client.d.ts.map +0 -1
- package/dist/api-client.js +0 -40
- package/dist/api-client.js.map +0 -1
- package/dist/belt.d.ts +0 -25
- package/dist/belt.d.ts.map +0 -1
- package/dist/belt.js +0 -113
- package/dist/belt.js.map +0 -1
- package/dist/card.d.ts +0 -66
- package/dist/card.d.ts.map +0 -1
- package/dist/card.js +0 -151
- package/dist/card.js.map +0 -1
- package/dist/cli.d.ts +0 -2
- package/dist/cli.d.ts.map +0 -1
- package/dist/cli.js +0 -238
- package/dist/cli.js.map +0 -1
- package/dist/commands/book.d.ts +0 -14
- package/dist/commands/book.d.ts.map +0 -1
- package/dist/commands/book.js +0 -90
- package/dist/commands/book.js.map +0 -1
- package/dist/commands/chat.d.ts +0 -36
- package/dist/commands/chat.d.ts.map +0 -1
- package/dist/commands/chat.js +0 -642
- package/dist/commands/chat.js.map +0 -1
- package/dist/commands/config.d.ts +0 -2
- package/dist/commands/config.d.ts.map +0 -1
- package/dist/commands/config.js +0 -84
- package/dist/commands/config.js.map +0 -1
- package/dist/commands/daemon.d.ts +0 -19
- package/dist/commands/daemon.d.ts.map +0 -1
- package/dist/commands/daemon.js +0 -72
- package/dist/commands/daemon.js.map +0 -1
- package/dist/commands/drop.d.ts +0 -12
- package/dist/commands/drop.d.ts.map +0 -1
- package/dist/commands/drop.js +0 -103
- package/dist/commands/drop.js.map +0 -1
- package/dist/commands/drops.d.ts +0 -9
- package/dist/commands/drops.d.ts.map +0 -1
- package/dist/commands/drops.js +0 -96
- package/dist/commands/drops.js.map +0 -1
- package/dist/commands/essay.d.ts +0 -40
- package/dist/commands/essay.d.ts.map +0 -1
- package/dist/commands/essay.js +0 -651
- package/dist/commands/essay.js.map +0 -1
- package/dist/commands/hackathon.d.ts +0 -2
- package/dist/commands/hackathon.d.ts.map +0 -1
- package/dist/commands/hackathon.js +0 -893
- package/dist/commands/hackathon.js.map +0 -1
- package/dist/commands/hacker.d.ts +0 -2
- package/dist/commands/hacker.d.ts.map +0 -1
- package/dist/commands/hacker.js +0 -87
- package/dist/commands/hacker.js.map +0 -1
- package/dist/commands/jobs.d.ts +0 -18
- package/dist/commands/jobs.d.ts.map +0 -1
- package/dist/commands/jobs.js +0 -138
- package/dist/commands/jobs.js.map +0 -1
- package/dist/commands/keys.d.ts +0 -2
- package/dist/commands/keys.d.ts.map +0 -1
- package/dist/commands/keys.js +0 -138
- package/dist/commands/keys.js.map +0 -1
- package/dist/commands/login.d.ts +0 -83
- package/dist/commands/login.d.ts.map +0 -1
- package/dist/commands/login.js +0 -236
- package/dist/commands/login.js.map +0 -1
- package/dist/commands/logout.d.ts +0 -2
- package/dist/commands/logout.d.ts.map +0 -1
- package/dist/commands/logout.js +0 -22
- package/dist/commands/logout.js.map +0 -1
- package/dist/commands/org-jobs.d.ts +0 -50
- package/dist/commands/org-jobs.d.ts.map +0 -1
- package/dist/commands/org-jobs.js +0 -383
- package/dist/commands/org-jobs.js.map +0 -1
- package/dist/commands/org.d.ts +0 -67
- package/dist/commands/org.d.ts.map +0 -1
- package/dist/commands/org.js +0 -1244
- package/dist/commands/org.js.map +0 -1
- package/dist/commands/ping.d.ts +0 -2
- package/dist/commands/ping.d.ts.map +0 -1
- package/dist/commands/ping.js +0 -50
- package/dist/commands/ping.js.map +0 -1
- package/dist/commands/profile.d.ts +0 -72
- package/dist/commands/profile.d.ts.map +0 -1
- package/dist/commands/profile.js +0 -672
- package/dist/commands/profile.js.map +0 -1
- package/dist/commands/project.d.ts +0 -2
- package/dist/commands/project.d.ts.map +0 -1
- package/dist/commands/project.js +0 -583
- package/dist/commands/project.js.map +0 -1
- package/dist/commands/referral.d.ts +0 -6
- package/dist/commands/referral.d.ts.map +0 -1
- package/dist/commands/referral.js +0 -93
- package/dist/commands/referral.js.map +0 -1
- package/dist/commands/rtfm.d.ts +0 -9
- package/dist/commands/rtfm.d.ts.map +0 -1
- package/dist/commands/rtfm.js +0 -114
- package/dist/commands/rtfm.js.map +0 -1
- package/dist/commands/scan.d.ts +0 -13
- package/dist/commands/scan.d.ts.map +0 -1
- package/dist/commands/scan.js +0 -286
- package/dist/commands/scan.js.map +0 -1
- package/dist/commands/scout.d.ts +0 -18
- package/dist/commands/scout.d.ts.map +0 -1
- package/dist/commands/scout.js +0 -232
- package/dist/commands/scout.js.map +0 -1
- package/dist/commands/setup-rail.d.ts +0 -58
- package/dist/commands/setup-rail.d.ts.map +0 -1
- package/dist/commands/setup-rail.js +0 -135
- package/dist/commands/setup-rail.js.map +0 -1
- package/dist/commands/setup.d.ts +0 -29
- package/dist/commands/setup.d.ts.map +0 -1
- package/dist/commands/setup.js +0 -442
- package/dist/commands/setup.js.map +0 -1
- package/dist/commands/sync.d.ts +0 -9
- package/dist/commands/sync.d.ts.map +0 -1
- package/dist/commands/sync.js +0 -345
- package/dist/commands/sync.js.map +0 -1
- package/dist/commands/update.d.ts +0 -2
- package/dist/commands/update.d.ts.map +0 -1
- package/dist/commands/update.js +0 -92
- package/dist/commands/update.js.map +0 -1
- package/dist/commands/whoami.d.ts +0 -2
- package/dist/commands/whoami.d.ts.map +0 -1
- package/dist/commands/whoami.js +0 -41
- package/dist/commands/whoami.js.map +0 -1
- package/dist/config.d.ts +0 -78
- package/dist/config.d.ts.map +0 -1
- package/dist/config.js +0 -93
- package/dist/config.js.map +0 -1
- package/dist/daily-sync.d.ts +0 -150
- package/dist/daily-sync.d.ts.map +0 -1
- package/dist/daily-sync.js +0 -665
- package/dist/daily-sync.js.map +0 -1
- package/dist/dossier.d.ts +0 -73
- package/dist/dossier.d.ts.map +0 -1
- package/dist/dossier.js +0 -141
- package/dist/dossier.js.map +0 -1
- package/dist/field-normalize.d.ts +0 -4
- package/dist/field-normalize.d.ts.map +0 -1
- package/dist/field-normalize.js +0 -8
- package/dist/field-normalize.js.map +0 -1
- package/dist/index.d.ts +0 -3
- package/dist/index.d.ts.map +0 -1
- package/dist/index.js.map +0 -1
- package/dist/job-fields.d.ts +0 -47
- package/dist/job-fields.d.ts.map +0 -1
- package/dist/job-fields.js +0 -148
- package/dist/job-fields.js.map +0 -1
- package/dist/machine.d.ts +0 -15
- package/dist/machine.d.ts.map +0 -1
- package/dist/machine.js +0 -64
- package/dist/machine.js.map +0 -1
- package/dist/org-fields.d.ts +0 -57
- package/dist/org-fields.d.ts.map +0 -1
- package/dist/org-fields.js +0 -167
- package/dist/org-fields.js.map +0 -1
- package/dist/posthog.d.ts +0 -38
- package/dist/posthog.d.ts.map +0 -1
- package/dist/posthog.js +0 -224
- package/dist/posthog.js.map +0 -1
- package/dist/project-fields.d.ts +0 -40
- package/dist/project-fields.d.ts.map +0 -1
- package/dist/project-fields.js +0 -79
- package/dist/project-fields.js.map +0 -1
- package/dist/prompt-consent.d.ts +0 -73
- package/dist/prompt-consent.d.ts.map +0 -1
- package/dist/prompt-consent.js +0 -191
- package/dist/prompt-consent.js.map +0 -1
- package/dist/prompt-stats.d.ts +0 -226
- package/dist/prompt-stats.d.ts.map +0 -1
- package/dist/prompt-stats.js +0 -561
- package/dist/prompt-stats.js.map +0 -1
- package/dist/referral.d.ts +0 -14
- package/dist/referral.d.ts.map +0 -1
- package/dist/referral.js +0 -28
- package/dist/referral.js.map +0 -1
- package/dist/registry.d.ts +0 -15
- package/dist/registry.d.ts.map +0 -1
- package/dist/registry.js +0 -180
- package/dist/registry.js.map +0 -1
- package/dist/resolve-command.d.ts +0 -20
- package/dist/resolve-command.d.ts.map +0 -1
- package/dist/resolve-command.js +0 -21
- package/dist/resolve-command.js.map +0 -1
- package/dist/scanners/antigravity.d.ts +0 -41
- package/dist/scanners/antigravity.d.ts.map +0 -1
- package/dist/scanners/antigravity.js +0 -210
- package/dist/scanners/antigravity.js.map +0 -1
- package/dist/scanners/github-copilot-telemetry.d.ts +0 -23
- package/dist/scanners/github-copilot-telemetry.d.ts.map +0 -1
- package/dist/scanners/github-copilot-telemetry.js +0 -220
- package/dist/scanners/github-copilot-telemetry.js.map +0 -1
- package/dist/scanners/github-copilot.d.ts +0 -68
- package/dist/scanners/github-copilot.d.ts.map +0 -1
- package/dist/scanners/github-copilot.js +0 -262
- package/dist/scanners/github-copilot.js.map +0 -1
- package/dist/scanners/incremental.d.ts +0 -282
- package/dist/scanners/incremental.d.ts.map +0 -1
- package/dist/scanners/incremental.js +0 -788
- package/dist/scanners/incremental.js.map +0 -1
- package/dist/scanners/index.d.ts +0 -116
- package/dist/scanners/index.d.ts.map +0 -1
- package/dist/scanners/index.js +0 -726
- package/dist/scanners/index.js.map +0 -1
- package/dist/scanners/sqlite.d.ts +0 -18
- package/dist/scanners/sqlite.d.ts.map +0 -1
- package/dist/scanners/sqlite.js +0 -63
- package/dist/scanners/sqlite.js.map +0 -1
- package/dist/scanners/util.d.ts +0 -112
- package/dist/scanners/util.d.ts.map +0 -1
- package/dist/scanners/util.js +0 -167
- package/dist/scanners/util.js.map +0 -1
- package/dist/session.d.ts +0 -106
- package/dist/session.d.ts.map +0 -1
- package/dist/session.js +0 -191
- package/dist/session.js.map +0 -1
- package/dist/share-card-colors.d.ts +0 -19
- package/dist/share-card-colors.d.ts.map +0 -1
- package/dist/share-card-colors.js +0 -36
- package/dist/share-card-colors.js.map +0 -1
- package/dist/share-card.d.ts +0 -31
- package/dist/share-card.d.ts.map +0 -1
- package/dist/share-card.js +0 -380
- package/dist/share-card.js.map +0 -1
- package/dist/share.d.ts +0 -17
- package/dist/share.d.ts.map +0 -1
- package/dist/share.js +0 -75
- package/dist/share.js.map +0 -1
- package/dist/sync.d.ts +0 -100
- package/dist/sync.d.ts.map +0 -1
- package/dist/sync.js +0 -304
- package/dist/sync.js.map +0 -1
- package/dist/ui.d.ts +0 -33
- package/dist/ui.d.ts.map +0 -1
- package/dist/ui.js +0 -127
- package/dist/ui.js.map +0 -1
- package/dist/utils/npmGlobal.d.ts +0 -30
- package/dist/utils/npmGlobal.d.ts.map +0 -1
- package/dist/utils/npmGlobal.js +0 -69
- package/dist/utils/npmGlobal.js.map +0 -1
- package/dist/utils/openBrowser.d.ts +0 -14
- package/dist/utils/openBrowser.d.ts.map +0 -1
- package/dist/utils/openBrowser.js +0 -47
- package/dist/utils/openBrowser.js.map +0 -1
- package/dist/utils/updateCheck.d.ts +0 -16
- package/dist/utils/updateCheck.d.ts.map +0 -1
- package/dist/utils/updateCheck.js +0 -111
- package/dist/utils/updateCheck.js.map +0 -1
- package/dist/utils/waitForEnter.d.ts +0 -12
- package/dist/utils/waitForEnter.d.ts.map +0 -1
- package/dist/utils/waitForEnter.js +0 -43
- package/dist/utils/waitForEnter.js.map +0 -1
package/dist/commands/profile.js
DELETED
|
@@ -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
|