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.
- package/README.md +58 -18
- package/SKILL.md +9 -13
- package/dist/adapters/claude-code.js +21 -121
- package/dist/adapters/codex.js +196 -0
- package/dist/adapters/index.js +16 -5
- package/dist/adapters/json-adapter.js +135 -0
- package/dist/adapters/json-agents.js +88 -0
- package/dist/adapters/types.js +4 -4
- package/dist/commands/account.js +49 -0
- package/dist/commands/catalog.js +191 -0
- package/dist/commands/doctor.js +39 -28
- package/dist/commands/init.js +69 -35
- package/dist/commands/login.js +31 -21
- package/dist/commands/remove.js +40 -5
- package/dist/commands/skill.js +42 -0
- package/dist/commands/update.js +17 -11
- package/dist/index.js +106 -29
- package/dist/lib/api.js +92 -15
- package/dist/lib/json-file.js +7 -2
- package/dist/lib/paths.js +41 -0
- package/dist/lib/session.js +49 -0
- package/dist/lib/skill-file.js +26 -0
- package/dist/lib/version.js +1 -1
- package/package.json +1 -1
package/dist/commands/init.js
CHANGED
|
@@ -1,18 +1,22 @@
|
|
|
1
1
|
// `noslopui init` — the whole setup, in one command.
|
|
2
2
|
//
|
|
3
|
-
//
|
|
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: ${(
|
|
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(
|
|
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 (
|
|
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
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
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)
|
|
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
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
hint(`skill ${
|
|
105
|
-
|
|
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('
|
|
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"`);
|
package/dist/commands/login.js
CHANGED
|
@@ -1,26 +1,36 @@
|
|
|
1
|
-
// `noslopui login` —
|
|
1
|
+
// `noslopui login` — sign the CLI itself in, like `21st login`.
|
|
2
2
|
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
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
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
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
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
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
|
-
|
|
25
|
-
|
|
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
|
}
|
package/dist/commands/remove.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
13
|
-
|
|
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
|
-
|
|
29
|
-
|
|
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
|
+
}
|
package/dist/commands/update.js
CHANGED
|
@@ -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(
|
|
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
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
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
|
-
|
|
34
|
-
|
|
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(
|
|
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('
|
|
23
|
-
init
|
|
24
|
-
doctor
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
remove
|
|
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>
|
|
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()} ·
|
|
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
|
-
|
|
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 (
|
|
77
|
+
else if (flag === '--version' || flag === '-v')
|
|
49
78
|
parsed.version = true;
|
|
50
|
-
else if (
|
|
79
|
+
else if (flag === '--yes' || flag === '-y')
|
|
51
80
|
parsed.yes = true;
|
|
52
|
-
else if (
|
|
81
|
+
else if (flag === '--no-skill')
|
|
53
82
|
parsed.skill = false;
|
|
54
|
-
else if (
|
|
83
|
+
else if (flag === '--no-browser')
|
|
55
84
|
parsed.browser = false;
|
|
56
|
-
else if (
|
|
85
|
+
else if (flag === '--skill')
|
|
57
86
|
parsed.skill = true;
|
|
58
|
-
else if (
|
|
59
|
-
parsed.
|
|
60
|
-
else if (
|
|
61
|
-
parsed.
|
|
62
|
-
else if (
|
|
63
|
-
parsed.
|
|
64
|
-
else if (
|
|
65
|
-
parsed.
|
|
66
|
-
else if (
|
|
67
|
-
parsed.
|
|
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
|
-
|
|
188
|
+
process.stderr.write('\n');
|
|
112
189
|
fail(explain(err));
|
|
113
|
-
|
|
190
|
+
process.stderr.write('\n');
|
|
114
191
|
process.exitCode = 1;
|
|
115
192
|
});
|