noslopui 0.1.0 → 0.2.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.
@@ -1,18 +1,22 @@
1
1
  // `noslopui init` — the whole setup, in one command.
2
2
  //
3
- // detect the agent → sign in in the browser → write the MCP entry →
4
- // install the skill → say what to do next
3
+ // find the agents → one sign-in in the browser → write each agent's MCP
4
+ // entry → install the skill → say what to do next
5
5
  //
6
6
  // Nothing here is silent: every file that gets written is named on screen,
7
7
  // with its backup, because this command edits configuration the user didn't
8
8
  // ask us to know about.
9
+ //
10
+ // The same approval also gives the CLI itself a key (agent id `cli`), saved
11
+ // for `noslopui search`, `get` and `whoami` — unless it already has one.
9
12
  import { spawn } from 'node:child_process';
10
13
  import { ApiError, mcpUrl, origin, startSignIn, waitForApproval, fetchSkill } from '../lib/api.js';
11
- import { adapterById, detectAdapters } from '../adapters/index.js';
14
+ import { ADAPTERS, adapterById, detectAdapters } from '../adapters/index.js';
12
15
  import { color, confirm, hint, interactive, machineLabel, maskKey, ok, say, step, warn } from '../lib/ui.js';
13
16
  import { ConfigUnreadableError } from '../lib/json-file.js';
17
+ import { readSession, saveSession } from '../lib/session.js';
14
18
  /** Best-effort: if it doesn't open, the URL is on screen anyway. */
15
- function openBrowser(url) {
19
+ export function openBrowser(url) {
16
20
  const [command, args] = process.platform === 'win32'
17
21
  ? ['cmd', ['/c', 'start', '', url]]
18
22
  : process.platform === 'darwin'
@@ -27,29 +31,57 @@ function openBrowser(url) {
27
31
  // Headless, locked down, or no desktop — the printed URL covers it.
28
32
  }
29
33
  }
34
+ /**
35
+ * The browser half: open a request for these agent ids, show the code, wait
36
+ * for the user to approve it, and hand back one key per id.
37
+ */
38
+ export async function signIn(agentIds, browser) {
39
+ const machine = machineLabel();
40
+ const started = await startSignIn(machine, agentIds);
41
+ say();
42
+ step('Approve this in your browser, signed in to noslopUI:');
43
+ say();
44
+ say(` ${color.bold(color.cyan(started.verificationUriComplete))}`);
45
+ say();
46
+ say(` code ${color.bold(started.userCode)} · this machine is shown as "${machine}" · expires in ${Math.round(started.expiresIn / 60)} minutes`);
47
+ say();
48
+ if (browser)
49
+ openBrowser(started.verificationUriComplete);
50
+ step('Waiting for approval…');
51
+ const keys = await waitForApproval(started);
52
+ ok(`Approved. ${keys.length === 1 ? 'One key issued' : `${keys.length} keys issued`}.`);
53
+ return keys;
54
+ }
55
+ const supportedList = () => ADAPTERS.map((a) => a.id).join(', ');
30
56
  async function chooseAdapters(options) {
31
57
  if (options.agents?.length) {
32
58
  const chosen = [];
33
59
  for (const id of options.agents) {
34
60
  const adapter = adapterById(id);
35
61
  if (!adapter)
36
- throw new Error(`Unknown agent "${id}". Supported: ${(await detectAdapters()).map((a) => a.id).join(', ') || 'claude-code'}.`);
62
+ throw new Error(`Unknown agent "${id}". Supported: ${supportedList()}.`);
37
63
  chosen.push(adapter);
38
64
  }
39
65
  return chosen;
40
66
  }
41
67
  const detected = await detectAdapters();
42
68
  if (!detected.length) {
43
- throw new Error('No supported agent found on this machine. noslopUI sets up Claude Code today; install it first, or add the server by hand from https://noslopui.com/mcp.');
69
+ throw new Error(`No supported agent found on this machine. noslopUI sets up ${ADAPTERS.map((a) => a.label).join(', ')}.\n For anything else, add the server by hand: ${origin()}/mcp`);
44
70
  }
71
+ say(`Found ${detected.map((a) => color.bold(a.label)).join(', ')}.`);
72
+ if (options.yes || !interactive() || detected.length === 1) {
73
+ if (detected.length === 1 && interactive() && !options.yes) {
74
+ if (!(await confirm(`Set up ${color.bold(detected[0].label)}?`, true)))
75
+ throw new Error('Nothing selected, so nothing was changed.');
76
+ }
77
+ return detected;
78
+ }
79
+ if (await confirm(`Set up all ${detected.length}?`, true))
80
+ return detected;
45
81
  const chosen = [];
46
82
  for (const adapter of detected) {
47
- if (options.yes || !interactive()) {
48
- chosen.push(adapter);
49
- }
50
- else if (await confirm(`Set up ${color.bold(adapter.label)}?`, true)) {
83
+ if (await confirm(`Set up ${color.bold(adapter.label)}?`, true))
51
84
  chosen.push(adapter);
52
- }
53
85
  }
54
86
  if (!chosen.length)
55
87
  throw new Error('Nothing selected, so nothing was changed.');
@@ -60,8 +92,6 @@ export async function init(options) {
60
92
  say(`${color.bold('noslopUI')} ${color.dim('— hand-crafted components and design systems for your agent')}`);
61
93
  say();
62
94
  const adapters = await chooseAdapters(options);
63
- for (const adapter of adapters)
64
- ok(`Found ${adapter.label}`);
65
95
  // Either a key the user already has, or one issued through the browser.
66
96
  let keys;
67
97
  if (options.key) {
@@ -69,25 +99,20 @@ export async function init(options) {
69
99
  step('Using the key you passed with --key.');
70
100
  }
71
101
  else {
72
- const machine = machineLabel();
73
- const started = await startSignIn(machine, adapters.map((a) => a.id));
74
- say();
75
- step(`Approve this in your browser, signed in to noslopUI:`);
76
- say();
77
- say(` ${color.bold(color.cyan(started.verificationUriComplete))}`);
78
- say();
79
- say(` code ${color.bold(started.userCode)} · this machine is shown as "${machine}" · expires in ${Math.round(started.expiresIn / 60)} minutes`);
80
- say();
81
- if (options.browser !== false)
82
- openBrowser(started.verificationUriComplete);
83
- step('Waiting for approval…');
84
- keys = await waitForApproval(started);
85
- ok(`Approved. ${keys.length === 1 ? 'One key issued' : `${keys.length} keys issued`}.`);
102
+ const wantSession = !readSession();
103
+ keys = await signIn([...adapters.map((a) => a.id), ...(wantSession ? ['cli'] : [])], options.browser !== false);
104
+ const session = keys.find((k) => k.agent === 'cli');
105
+ if (session) {
106
+ const path = saveSession(session.key);
107
+ hint(`CLI signed in too, for "noslopui search" and "get" — ${path}`);
108
+ }
86
109
  }
87
110
  say();
88
111
  const restartNeeded = [];
112
+ const skillsWritten = new Map();
113
+ let skillText = null;
89
114
  for (const adapter of adapters) {
90
- const issued = keys.find((k) => k.agent === adapter.id) ?? keys[0];
115
+ const issued = keys.find((k) => k.agent === adapter.id);
91
116
  if (!issued)
92
117
  throw new Error(`No key was issued for ${adapter.label}.`);
93
118
  const result = await adapter.install({ url: mcpUrl(), key: issued.key });
@@ -97,19 +122,28 @@ export async function init(options) {
97
122
  hint(`backup ${result.backupPath}`);
98
123
  hint(`key ${maskKey(issued.key)} — named "${issued.name}" on your account`);
99
124
  restartNeeded.push(adapter.label);
100
- if (options.skill) {
101
- const download = await fetchSkill();
102
- const skill = await adapter.installSkill(download.markdown);
103
- ok(`${adapter.label}: ${skill.changed ? 'installed' : 'already had'} the noslopUI skill`);
104
- hint(`skill ${skill.path}`);
105
- if (download.source === 'bundled') {
125
+ if (!options.skill)
126
+ continue;
127
+ const path = adapter.skillPath();
128
+ if (!path) {
129
+ hint(`skill ${adapter.label} has no skills folder; the rules reach it through the server`);
130
+ continue;
131
+ }
132
+ // Several agents read one folder (~/.agents/skills); write it once.
133
+ if (!skillsWritten.has(path)) {
134
+ skillText ??= await fetchSkill();
135
+ const skill = await adapter.installSkill(skillText.markdown);
136
+ skillsWritten.set(path, skill?.changed ?? false);
137
+ if (skillText.source === 'bundled') {
106
138
  warn('That skill came from this package, not the site — it may be behind. Run "npx noslopui update" when you are back online.');
107
139
  }
108
140
  }
141
+ ok(`${adapter.label}: ${skillsWritten.get(path) ? 'installed' : 'already had'} the noslopUI skill`);
142
+ hint(`skill ${path}`);
109
143
  }
110
144
  say();
111
145
  say(color.bold('Done. Two things:'));
112
- say(` 1. Restart ${restartNeeded.join(' and ')} so it picks up the new server.`);
146
+ say(` 1. Restart ${restartNeeded.join(', ')} so ${restartNeeded.length === 1 ? 'it picks' : 'they pick'} up the new server.`);
113
147
  say(` 2. Try it: ${color.cyan('"build me a landing page for a coffee roastery"')}`);
114
148
  say();
115
149
  hint(`Revoke a key any time at ${origin()}/account?tab=mcp · check the setup with "npx noslopui doctor"`);
@@ -1,26 +1,36 @@
1
- // `noslopui login` — a fresh key for an agent that's already set up.
1
+ // `noslopui login` — sign the CLI itself in, like `21st login`.
2
2
  //
3
- // The case this exists for: the key was revoked, or it belongs to the wrong
4
- // account. It swaps the key in place and leaves everything else — the skill,
5
- // the server entry's other fields, every other configured server — alone.
6
- import { init } from './init.js';
7
- import { ADAPTERS } from '../adapters/index.js';
8
- import { say, warn, hint } from '../lib/ui.js';
3
+ // Saves a key for the agent id `cli` to this machine's config folder, which is
4
+ // what `search`, `get`, `design-system`, `collections` and `whoami` use. It
5
+ // doesn't touch any agent's config: `init` does that.
6
+ //
7
+ // `login --agent <id>` is the other job: a fresh key for an agent that's
8
+ // already set up (its key was revoked, or belongs to the wrong account). It
9
+ // swaps the key in place and leaves the skill and every other server alone.
10
+ import { init, signIn } from './init.js';
11
+ import { whoami } from '../lib/api.js';
12
+ import { saveSession } from '../lib/session.js';
13
+ import { hint, ok, say } from '../lib/ui.js';
9
14
  export async function login(options) {
10
- const configured = [];
11
- for (const adapter of ADAPTERS) {
12
- if (!(await adapter.detect()))
13
- continue;
14
- if ((await adapter.status()).installed)
15
- configured.push(adapter.id);
15
+ if (options.agents?.length) {
16
+ return init({ ...options, skill: false });
17
+ }
18
+ say();
19
+ let key;
20
+ if (options.key) {
21
+ key = options.key;
16
22
  }
17
- if (!configured.length) {
18
- say();
19
- warn('No agent here has noslopUI set up yet.');
20
- hint('Run "npx noslopui init" — it does the sign-in and the setup together.');
21
- say();
22
- return 1;
23
+ else {
24
+ const keys = await signIn(['cli'], options.browser);
25
+ key = keys.find((k) => k.agent === 'cli')?.key ?? '';
26
+ if (!key)
27
+ throw new Error('No key was issued for the CLI.');
23
28
  }
24
- // Same path as init, minus the skill: this is only about the key.
25
- return init({ ...options, agents: options.agents?.length ? options.agents : configured, skill: false });
29
+ const account = await whoami(key);
30
+ const path = saveSession(key);
31
+ ok(`Signed in as ${account.email ?? 'your account'}.`);
32
+ hint(`saved to ${path}`);
33
+ hint('Set up an agent with "npx noslopui init", or search with "npx noslopui search <query>".');
34
+ say();
35
+ return 0;
26
36
  }
@@ -3,14 +3,26 @@
3
3
  // Removing the config entry does NOT revoke the key: the key belongs to the
4
4
  // account, not to this machine, and someone removing a local config hasn't
5
5
  // asked us to change their account. We say where to revoke it instead.
6
- import { ADAPTERS } from '../adapters/index.js';
6
+ //
7
+ // Several agents share a skills folder (~/.agents/skills), so a skill is only
8
+ // removed once no agent that's still set up reads it.
9
+ import { ADAPTERS, adapterById } from '../adapters/index.js';
7
10
  import { origin } from '../lib/api.js';
8
11
  import { color, confirm, hint, ok, say, warn } from '../lib/ui.js';
9
12
  export async function remove(options) {
10
13
  say();
11
14
  let removedAnything = false;
12
- for (const adapter of ADAPTERS) {
13
- if (!(await adapter.detect()))
15
+ const targets = options.agents?.length
16
+ ? options.agents.map((id) => {
17
+ const a = adapterById(id);
18
+ if (!a)
19
+ throw new Error(`Unknown agent "${id}". Supported: ${ADAPTERS.map((x) => x.id).join(', ')}.`);
20
+ return a;
21
+ })
22
+ : ADAPTERS;
23
+ const removed = [];
24
+ for (const adapter of targets) {
25
+ if (!options.agents?.length && !(await adapter.detect()))
14
26
  continue;
15
27
  const status = await adapter.status();
16
28
  if (!status.installed) {
@@ -24,9 +36,32 @@ export async function remove(options) {
24
36
  if (await adapter.uninstall()) {
25
37
  ok(`${adapter.label}: removed the noslopui server (other servers untouched)`);
26
38
  removedAnything = true;
39
+ removed.push(adapter);
27
40
  }
28
- if (options.skill && (await adapter.removeSkill())) {
29
- ok(`${adapter.label}: removed the skill`);
41
+ }
42
+ if (options.skill) {
43
+ const stillUsing = async (path) => {
44
+ for (const other of ADAPTERS) {
45
+ if (removed.includes(other) || other.skillPath() !== path)
46
+ continue;
47
+ if ((await other.status().catch(() => ({ installed: false }))).installed)
48
+ return other;
49
+ }
50
+ return null;
51
+ };
52
+ const done = new Set();
53
+ for (const adapter of removed) {
54
+ const path = adapter.skillPath();
55
+ if (!path || done.has(path))
56
+ continue;
57
+ done.add(path);
58
+ const user = await stillUsing(path);
59
+ if (user) {
60
+ say(`${color.dim('–')} kept the skill at ${path}: ${user.label} still uses it`);
61
+ }
62
+ else if (await adapter.removeSkill()) {
63
+ ok(`removed the skill at ${path}`);
64
+ }
30
65
  }
31
66
  }
32
67
  say();
@@ -0,0 +1,42 @@
1
+ // `noslopui skill` — install only the skill, for someone who connected an
2
+ // agent by hand (a connector sign-in, or a config they wrote). It never
3
+ // touches an agent's MCP config, so a sign-in stays a sign-in.
4
+ import { fetchSkill } from '../lib/api.js';
5
+ import { ADAPTERS, adapterById, detectAdapters } from '../adapters/index.js';
6
+ import { hint, ok, say, warn } from '../lib/ui.js';
7
+ export async function skill(options) {
8
+ say();
9
+ let targets;
10
+ if (options.agents?.length) {
11
+ targets = options.agents.map((id) => {
12
+ const a = adapterById(id);
13
+ if (!a)
14
+ throw new Error(`Unknown agent "${id}". Supported: ${ADAPTERS.map((x) => x.id).join(', ')}.`);
15
+ return a;
16
+ });
17
+ }
18
+ else {
19
+ targets = await detectAdapters();
20
+ if (!targets.length)
21
+ throw new Error(`No supported agent found. Pass one with --agent: ${ADAPTERS.map((a) => a.id).join(', ')}.`);
22
+ }
23
+ const download = await fetchSkill();
24
+ if (download.source === 'bundled')
25
+ warn('Using the copy in this package — the site was unreachable, so it may be behind.');
26
+ const written = new Map();
27
+ for (const adapter of targets) {
28
+ const path = adapter.skillPath();
29
+ if (!path) {
30
+ ok(`${adapter.label}: no skills folder; the rules reach it through the server`);
31
+ continue;
32
+ }
33
+ if (!written.has(path))
34
+ written.set(path, (await adapter.installSkill(download.markdown))?.changed ?? false);
35
+ ok(`${adapter.label}: skill ${written.get(path) ? 'installed' : 'already current'}`);
36
+ hint(path);
37
+ }
38
+ say();
39
+ say('Restart your agent to pick it up.');
40
+ say();
41
+ return 0;
42
+ }
@@ -3,7 +3,8 @@
3
3
  // In practice that means the skill: it's served from noslopui.com and changes
4
4
  // as the workflow improves, while the config entry only changes if the server
5
5
  // URL does. Nothing here needs a sign-in, so an expired trial or a revoked key
6
- // doesn't block it.
6
+ // doesn't block it. Several agents share one skills folder, so each folder is
7
+ // written once.
7
8
  import { fetchSkill, mcpUrl } from '../lib/api.js';
8
9
  import { ADAPTERS } from '../adapters/index.js';
9
10
  import { color, hint, ok, say, warn } from '../lib/ui.js';
@@ -11,6 +12,7 @@ export async function update() {
11
12
  say();
12
13
  let touched = false;
13
14
  let markdown = null;
15
+ const written = new Map();
14
16
  for (const adapter of ADAPTERS) {
15
17
  if (!(await adapter.detect()))
16
18
  continue;
@@ -21,22 +23,26 @@ export async function update() {
21
23
  }
22
24
  if (status.url !== mcpUrl()) {
23
25
  warn(`${adapter.label}: configured for ${status.url}, not ${mcpUrl()}.`);
24
- hint('Run "npx noslopui init" to move it — that needs a key, so it is not done here.');
26
+ hint(`Run "npx noslopui init --agent ${adapter.id}" to move it — that needs a key, so it is not done here.`);
25
27
  }
26
- markdown ??= (await fetchSkill()).markdown;
27
- const skill = await adapter.installSkill(markdown);
28
- if (skill.changed) {
29
- ok(`${adapter.label}: skill updated`);
30
- hint(skill.path);
31
- touched = true;
28
+ const path = adapter.skillPath();
29
+ if (!path) {
30
+ ok(`${adapter.label}: no skills folder; the rules reach it through the server`);
31
+ continue;
32
32
  }
33
- else {
34
- ok(`${adapter.label}: skill already current`);
33
+ if (!written.has(path)) {
34
+ markdown ??= (await fetchSkill()).markdown;
35
+ const skill = await adapter.installSkill(markdown);
36
+ written.set(path, skill?.changed ?? false);
37
+ if (skill?.changed)
38
+ touched = true;
35
39
  }
40
+ ok(`${adapter.label}: skill ${written.get(path) ? 'updated' : 'already current'}`);
41
+ hint(path);
36
42
  }
37
43
  say();
38
44
  if (touched)
39
- say(`Restart your agent to pick up the new workflow.`);
45
+ say('Restart your agents to pick up the new workflow.');
40
46
  say();
41
47
  return 0;
42
48
  }
package/dist/index.js CHANGED
@@ -1,5 +1,6 @@
1
1
  #!/usr/bin/env node
2
- // noslopui — connect a coding agent to noslopUI.
2
+ // noslopui — connect a coding agent to noslopUI, and use the catalog from the
3
+ // terminal.
3
4
  //
4
5
  // Argument parsing is by hand and stays that way: a tool people run with
5
6
  // `npx` should download in one tick, and a CLI-argument library is a
@@ -9,6 +10,10 @@ import { doctor } from './commands/doctor.js';
9
10
  import { remove } from './commands/remove.js';
10
11
  import { update } from './commands/update.js';
11
12
  import { login } from './commands/login.js';
13
+ import { logout, whoami } from './commands/account.js';
14
+ import { skill } from './commands/skill.js';
15
+ import * as catalog from './commands/catalog.js';
16
+ import { ADAPTERS } from './adapters/index.js';
12
17
  import { color, fail, say } from './lib/ui.js';
13
18
  import { origin } from './lib/api.js';
14
19
  import { CLI_VERSION as VERSION } from './lib/version.js';
@@ -19,52 +24,94 @@ ${color.bold('noslopui')} — hand-crafted UI components and design systems, in
19
24
  ${color.bold('Usage')}
20
25
  npx noslopui <command> [options]
21
26
 
22
- ${color.bold('Commands')}
23
- init Set up an agent: sign in, add the MCP server, install the skill
24
- doctor Check what is set up and what is wrong with it
25
- login Get a fresh key for an agent that is already set up
26
- update Re-download the skill for agents that are set up
27
- remove Take the noslopUI server back out (never touches other servers)
27
+ ${color.bold('Set up')}
28
+ init Set up your agents: one sign-in, then the MCP server and the skill
29
+ doctor Check what is set up and what is wrong with it
30
+ update Re-download the skill for agents that are set up
31
+ skill Install only the skill (for an agent you connected by hand)
32
+ remove Take the noslopUI server back out (never touches other servers)
33
+
34
+ ${color.bold('Account')}
35
+ login Sign this CLI in (for search and get); --agent <id> re-keys an agent
36
+ whoami Show the account and plan this CLI is signed in to
37
+ logout Forget this CLI's sign-in
38
+
39
+ ${color.bold('Catalog')}
40
+ search <query> Search components and blocks --tag <facet:value>, --framework, --limit
41
+ get <id> Print a component's code --format tsx|html|vue, --write <file>
42
+ prompt <id> Print a component's AI-prompt version
43
+ design-systems [query] Search design systems --tag theme:dark …
44
+ design-system <id> Print a design system's DESIGN.md --write DESIGN.md
45
+ collections List your saved collections
46
+ collection <name|id> Show one collection
28
47
 
29
48
  ${color.bold('Options')}
30
- --agent <id> Set up a specific agent instead of detecting (claude-code)
49
+ --agent <id> Only this agent (repeatable): ${ADAPTERS.map((a) => a.id).join(', ')}
31
50
  --no-skill Do not install the skill (init)
32
51
  --no-browser Do not open a browser; print the URL instead (init, login)
33
52
  --skill Also remove the skill (remove)
34
53
  --key <key> Use a key you already have instead of signing in (init, login)
54
+ --json Machine-readable output (catalog commands, whoami)
55
+ --force Overwrite the file --write points at
35
56
  -y, --yes Take the default answer for every question
36
57
  -h, --help Show this
37
58
  -v, --version Show the version
38
59
 
39
- ${color.dim(`Server: ${origin()} · set NOSLOPUI_ORIGIN to point somewhere else.`)}
60
+ ${color.dim(`Server: ${origin()} · NOSLOPUI_ORIGIN points somewhere else · NOSLOPUI_API_KEY signs in without a saved login.`)}
40
61
  `);
41
62
  }
42
63
  function parse(argv) {
43
- const parsed = { command: '', agents: [], skill: null, yes: false, browser: true, help: false, version: false };
64
+ const parsed = { command: '', args: [], agents: [], tags: [], skill: null, yes: false, browser: true, help: false, version: false, json: false, force: false };
65
+ const value = (i, name) => {
66
+ const v = argv[i];
67
+ if (v === undefined || v.startsWith('-'))
68
+ throw new Error(`${name} needs a value.`);
69
+ return v;
70
+ };
44
71
  for (let i = 0; i < argv.length; i++) {
45
72
  const arg = argv[i];
46
- if (arg === '--help' || arg === '-h')
73
+ const [flag, inline] = arg.startsWith('--') && arg.includes('=') ? [arg.slice(0, arg.indexOf('=')), arg.slice(arg.indexOf('=') + 1)] : [arg, undefined];
74
+ const take = () => inline ?? value(++i, flag);
75
+ if (flag === '--help' || flag === '-h')
47
76
  parsed.help = true;
48
- else if (arg === '--version' || arg === '-v')
77
+ else if (flag === '--version' || flag === '-v')
49
78
  parsed.version = true;
50
- else if (arg === '--yes' || arg === '-y')
79
+ else if (flag === '--yes' || flag === '-y')
51
80
  parsed.yes = true;
52
- else if (arg === '--no-skill')
81
+ else if (flag === '--no-skill')
53
82
  parsed.skill = false;
54
- else if (arg === '--no-browser')
83
+ else if (flag === '--no-browser')
55
84
  parsed.browser = false;
56
- else if (arg === '--skill')
85
+ else if (flag === '--skill')
57
86
  parsed.skill = true;
58
- else if (arg === '--agent')
59
- parsed.agents.push(String(argv[++i] ?? ''));
60
- else if (arg.startsWith('--agent='))
61
- parsed.agents.push(arg.slice('--agent='.length));
62
- else if (arg === '--key')
63
- parsed.key = String(argv[++i] ?? '');
64
- else if (arg.startsWith('--key='))
65
- parsed.key = arg.slice('--key='.length);
66
- else if (!arg.startsWith('-') && !parsed.command)
67
- parsed.command = arg;
87
+ else if (flag === '--json')
88
+ parsed.json = true;
89
+ else if (flag === '--force')
90
+ parsed.force = true;
91
+ else if (flag === '--agent')
92
+ parsed.agents.push(take());
93
+ else if (flag === '--tag')
94
+ parsed.tags.push(take());
95
+ else if (flag === '--key')
96
+ parsed.key = take();
97
+ else if (flag === '--format')
98
+ parsed.format = take();
99
+ else if (flag === '--framework')
100
+ parsed.framework = take();
101
+ else if (flag === '--write' || flag === '-o')
102
+ parsed.write = take();
103
+ else if (flag === '--limit') {
104
+ const n = Number(take());
105
+ if (!Number.isInteger(n) || n < 1)
106
+ throw new Error('--limit needs a whole number.');
107
+ parsed.limit = n;
108
+ }
109
+ else if (!arg.startsWith('-')) {
110
+ if (!parsed.command)
111
+ parsed.command = arg;
112
+ else
113
+ parsed.args.push(arg);
114
+ }
68
115
  else
69
116
  throw new Error(`Unknown option "${arg}". Run "npx noslopui --help".`);
70
117
  }
@@ -81,6 +128,16 @@ async function main() {
81
128
  usage();
82
129
  return args.command ? 0 : 1;
83
130
  }
131
+ const catalogOptions = {
132
+ args: args.args,
133
+ json: args.json,
134
+ tags: args.tags,
135
+ force: args.force,
136
+ ...(args.limit ? { limit: args.limit } : {}),
137
+ ...(args.format ? { format: args.format } : {}),
138
+ ...(args.framework ? { framework: args.framework } : {}),
139
+ ...(args.write ? { write: args.write } : {}),
140
+ };
84
141
  switch (args.command) {
85
142
  case 'init':
86
143
  return init({ agents: args.agents, skill: args.skill !== false, yes: args.yes, browser: args.browser, ...(args.key ? { key: args.key } : {}) });
@@ -88,10 +145,30 @@ async function main() {
88
145
  return doctor();
89
146
  case 'login':
90
147
  return login({ agents: args.agents, yes: args.yes, browser: args.browser, ...(args.key ? { key: args.key } : {}) });
148
+ case 'logout':
149
+ return logout();
150
+ case 'whoami':
151
+ return whoami({ json: args.json });
91
152
  case 'update':
92
153
  return update();
154
+ case 'skill':
155
+ return skill({ agents: args.agents });
93
156
  case 'remove':
94
- return remove({ yes: args.yes, skill: args.skill === true });
157
+ return remove({ yes: args.yes, skill: args.skill === true, agents: args.agents });
158
+ case 'search':
159
+ return catalog.search(catalogOptions);
160
+ case 'get':
161
+ return catalog.get(catalogOptions);
162
+ case 'prompt':
163
+ return catalog.prompt(catalogOptions);
164
+ case 'design-systems':
165
+ return catalog.designSystems(catalogOptions);
166
+ case 'design-system':
167
+ return catalog.designSystem(catalogOptions);
168
+ case 'collections':
169
+ return catalog.collections(catalogOptions);
170
+ case 'collection':
171
+ return catalog.collection(catalogOptions);
95
172
  default:
96
173
  fail(`Unknown command "${args.command}".`);
97
174
  usage();
@@ -108,8 +185,8 @@ main()
108
185
  process.exitCode = code;
109
186
  })
110
187
  .catch((err) => {
111
- say();
188
+ process.stderr.write('\n');
112
189
  fail(explain(err));
113
- say();
190
+ process.stderr.write('\n');
114
191
  process.exitCode = 1;
115
192
  });