noslopui 0.1.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +58 -18
- package/SKILL.md +104 -39
- 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 +214 -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 +109 -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
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
// The catalog from the terminal: search, get, prompt, design-system,
|
|
2
|
+
// design-systems, collections, collection.
|
|
3
|
+
//
|
|
4
|
+
// Every command is one call to the MCP server's own tools with the CLI's key,
|
|
5
|
+
// so the terminal and an agent always see the same thing, and every gate —
|
|
6
|
+
// plan, trial, fair use — stays on the server where it belongs.
|
|
7
|
+
//
|
|
8
|
+
// Output rule: the thing asked for goes to stdout, everything about it goes to
|
|
9
|
+
// stderr. `npx noslopui get hero-aurora > Hero.tsx` has to produce a file with
|
|
10
|
+
// nothing in it but the component.
|
|
11
|
+
import { existsSync, mkdirSync, writeFileSync } from 'node:fs';
|
|
12
|
+
import { dirname, resolve } from 'node:path';
|
|
13
|
+
import { callTool } from '../lib/api.js';
|
|
14
|
+
import { readSession } from '../lib/session.js';
|
|
15
|
+
import { color } from '../lib/ui.js';
|
|
16
|
+
// The catalog's two component sections, as search_components names them.
|
|
17
|
+
// Hyphens are accepted too, since that's how they read in URLs.
|
|
18
|
+
const SECTIONS = {
|
|
19
|
+
ui_blocks: 'ui_blocks',
|
|
20
|
+
'ui-blocks': 'ui_blocks',
|
|
21
|
+
blocks: 'ui_blocks',
|
|
22
|
+
motion_lab: 'motion_lab',
|
|
23
|
+
'motion-lab': 'motion_lab',
|
|
24
|
+
effects: 'motion_lab',
|
|
25
|
+
};
|
|
26
|
+
/** search_components arguments for these options; throws on a section it doesn't know. */
|
|
27
|
+
export function searchArguments(options) {
|
|
28
|
+
let section;
|
|
29
|
+
if (options.section !== undefined) {
|
|
30
|
+
section = SECTIONS[options.section.toLowerCase()];
|
|
31
|
+
if (!section)
|
|
32
|
+
throw new Error(`--section takes ui-blocks or motion-lab, not "${options.section}".`);
|
|
33
|
+
}
|
|
34
|
+
return {
|
|
35
|
+
...(options.args.length ? { query: options.args.join(' ') } : {}),
|
|
36
|
+
...(section ? { section } : {}),
|
|
37
|
+
...(options.tags.length ? { tags: options.tags } : {}),
|
|
38
|
+
...(options.framework ? { framework: options.framework } : {}),
|
|
39
|
+
...(options.limit ? { limit: options.limit } : {}),
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
const out = (s = '') => process.stdout.write(`${s}\n`);
|
|
43
|
+
const note = (s = '') => process.stderr.write(`${s}\n`);
|
|
44
|
+
function requireKey() {
|
|
45
|
+
const session = readSession();
|
|
46
|
+
if (!session)
|
|
47
|
+
throw new Error('Not signed in. Run "npx noslopui login" first — it takes one approval in the browser.');
|
|
48
|
+
return session.key;
|
|
49
|
+
}
|
|
50
|
+
function requireArg(options, what) {
|
|
51
|
+
const value = options.args.join(' ').trim();
|
|
52
|
+
if (!value)
|
|
53
|
+
throw new Error(`Say which ${what}.`);
|
|
54
|
+
return value;
|
|
55
|
+
}
|
|
56
|
+
/** A tool error is the server telling the user something; print it as-is and fail. */
|
|
57
|
+
function check(result) {
|
|
58
|
+
if (result.isError)
|
|
59
|
+
throw new Error(result.text);
|
|
60
|
+
return result;
|
|
61
|
+
}
|
|
62
|
+
/** Notices the server attaches to a result (the trial just started, and so on). */
|
|
63
|
+
function notices(data) {
|
|
64
|
+
if (typeof data?.trial === 'string')
|
|
65
|
+
note(color.yellow(`! ${data.trial}`));
|
|
66
|
+
}
|
|
67
|
+
function writeOut(path, text, force) {
|
|
68
|
+
const target = resolve(path);
|
|
69
|
+
if (existsSync(target) && !force)
|
|
70
|
+
throw new Error(`${target} already exists. Pass --force to overwrite it.`);
|
|
71
|
+
mkdirSync(dirname(target), { recursive: true });
|
|
72
|
+
writeFileSync(target, text.endsWith('\n') ? text : `${text}\n`, 'utf8');
|
|
73
|
+
note(`${color.green('✔')} wrote ${target}`);
|
|
74
|
+
}
|
|
75
|
+
const truncate = (s, n) => (s && s.length > n ? `${s.slice(0, n - 1)}…` : s ?? '');
|
|
76
|
+
export async function search(options) {
|
|
77
|
+
const args = searchArguments(options);
|
|
78
|
+
const key = requireKey();
|
|
79
|
+
const r = check(await callTool(key, 'search_components', args));
|
|
80
|
+
if (options.json)
|
|
81
|
+
return out(JSON.stringify(r.data, null, 2)), 0;
|
|
82
|
+
const results = r.data?.results ?? [];
|
|
83
|
+
if (!results.length) {
|
|
84
|
+
note('Nothing matched. Try fewer words, or no tags.');
|
|
85
|
+
return 1;
|
|
86
|
+
}
|
|
87
|
+
for (const item of results) {
|
|
88
|
+
const kind = item.kind === 'motion_lab' ? ` ${color.cyan('Motion Lab')}` : '';
|
|
89
|
+
out(`${color.bold(item.name)} ${color.dim(item.id)}${kind}`);
|
|
90
|
+
if (item.description)
|
|
91
|
+
out(` ${truncate(item.description, 110)}`);
|
|
92
|
+
out(` ${color.cyan(item.url)}`);
|
|
93
|
+
out();
|
|
94
|
+
}
|
|
95
|
+
note(color.dim(`Get one with "npx noslopui get <id>".`));
|
|
96
|
+
return 0;
|
|
97
|
+
}
|
|
98
|
+
export async function designSystems(options) {
|
|
99
|
+
const key = requireKey();
|
|
100
|
+
const r = check(await callTool(key, 'search_design_systems', {
|
|
101
|
+
...(options.args.length ? { query: options.args.join(' ') } : {}),
|
|
102
|
+
...(options.tags.length ? { tags: options.tags } : {}),
|
|
103
|
+
...(options.limit ? { limit: options.limit } : {}),
|
|
104
|
+
}));
|
|
105
|
+
if (options.json)
|
|
106
|
+
return out(JSON.stringify(r.data, null, 2)), 0;
|
|
107
|
+
const results = r.data?.results ?? [];
|
|
108
|
+
if (!results.length) {
|
|
109
|
+
note('Nothing matched.');
|
|
110
|
+
return 1;
|
|
111
|
+
}
|
|
112
|
+
for (const ds of results) {
|
|
113
|
+
const inspired = ds.inspiredBy ? color.dim(` · inspired by ${ds.inspiredBy}`) : '';
|
|
114
|
+
out(`${color.bold(ds.name)} ${color.dim(ds.id)}${inspired}`);
|
|
115
|
+
if (ds.description)
|
|
116
|
+
out(` ${truncate(ds.description, 110)}`);
|
|
117
|
+
if (ds.tags?.length)
|
|
118
|
+
out(` ${color.dim(ds.tags.join(' · '))}`);
|
|
119
|
+
out(` ${color.cyan(ds.previewUrl)}`);
|
|
120
|
+
out();
|
|
121
|
+
}
|
|
122
|
+
note(color.dim(`Get its DESIGN.md with "npx noslopui design-system <id> --write DESIGN.md".`));
|
|
123
|
+
return 0;
|
|
124
|
+
}
|
|
125
|
+
export async function get(options) {
|
|
126
|
+
const key = requireKey();
|
|
127
|
+
const id = requireArg(options, 'component (its id, from "npx noslopui search")');
|
|
128
|
+
const r = check(await callTool(key, 'get_component_code', { id }));
|
|
129
|
+
notices(r.data);
|
|
130
|
+
if (options.json)
|
|
131
|
+
return out(JSON.stringify(r.data, null, 2)), 0;
|
|
132
|
+
const snippets = r.data?.snippets ?? [];
|
|
133
|
+
if (!snippets.length)
|
|
134
|
+
throw new Error('This item has no published code.');
|
|
135
|
+
const chosen = options.format ? snippets.filter((s) => s.language === options.format) : snippets;
|
|
136
|
+
if (!chosen.length) {
|
|
137
|
+
throw new Error(`No ${options.format} version. Available: ${snippets.map((s) => s.language).join(', ')}.`);
|
|
138
|
+
}
|
|
139
|
+
if (options.write) {
|
|
140
|
+
if (chosen.length > 1)
|
|
141
|
+
throw new Error(`Several formats (${chosen.map((s) => s.language).join(', ')}) — pick one with --format.`);
|
|
142
|
+
writeOut(options.write, chosen[0].code, options.force);
|
|
143
|
+
return 0;
|
|
144
|
+
}
|
|
145
|
+
if (chosen.length === 1) {
|
|
146
|
+
out(chosen[0].code);
|
|
147
|
+
return 0;
|
|
148
|
+
}
|
|
149
|
+
for (const s of chosen) {
|
|
150
|
+
note(color.dim(`── ${s.language} ──`));
|
|
151
|
+
out(s.code);
|
|
152
|
+
}
|
|
153
|
+
return 0;
|
|
154
|
+
}
|
|
155
|
+
export async function prompt(options) {
|
|
156
|
+
const key = requireKey();
|
|
157
|
+
const id = requireArg(options, 'component');
|
|
158
|
+
const r = check(await callTool(key, 'get_component_prompt', { id }));
|
|
159
|
+
notices(r.data);
|
|
160
|
+
if (options.json)
|
|
161
|
+
return out(JSON.stringify(r.data, null, 2)), 0;
|
|
162
|
+
if (options.write)
|
|
163
|
+
writeOut(options.write, r.data?.promptText ?? '', options.force);
|
|
164
|
+
else
|
|
165
|
+
out(r.data?.promptText ?? '');
|
|
166
|
+
return 0;
|
|
167
|
+
}
|
|
168
|
+
export async function designSystem(options) {
|
|
169
|
+
const key = requireKey();
|
|
170
|
+
const id = requireArg(options, 'design system (its id, from "npx noslopui design-systems")');
|
|
171
|
+
const r = check(await callTool(key, 'get_design_system_file', { id }));
|
|
172
|
+
notices(r.data);
|
|
173
|
+
if (options.json)
|
|
174
|
+
return out(JSON.stringify(r.data, null, 2)), 0;
|
|
175
|
+
if (options.write)
|
|
176
|
+
writeOut(options.write, r.data?.designMd ?? '', options.force);
|
|
177
|
+
else
|
|
178
|
+
out(r.data?.designMd ?? '');
|
|
179
|
+
return 0;
|
|
180
|
+
}
|
|
181
|
+
export async function collections(options) {
|
|
182
|
+
const key = requireKey();
|
|
183
|
+
const r = check(await callTool(key, 'list_collections'));
|
|
184
|
+
if (options.json)
|
|
185
|
+
return out(JSON.stringify(r.data, null, 2)), 0;
|
|
186
|
+
const list = r.data?.collections ?? [];
|
|
187
|
+
if (!list.length) {
|
|
188
|
+
note('No collections yet. Save items on noslopui.com to make one.');
|
|
189
|
+
return 0;
|
|
190
|
+
}
|
|
191
|
+
for (const c of list)
|
|
192
|
+
out(`${color.bold(c.name)} ${color.dim(`${c.itemCount} ${c.itemCount === 1 ? 'item' : 'items'} · ${c.id}`)}`);
|
|
193
|
+
return 0;
|
|
194
|
+
}
|
|
195
|
+
export async function collection(options) {
|
|
196
|
+
const key = requireKey();
|
|
197
|
+
const which = requireArg(options, 'collection (a name or an id)');
|
|
198
|
+
// Ids are opaque; try the name first, since that's what people type.
|
|
199
|
+
let r = await callTool(key, 'get_collection', { name: which });
|
|
200
|
+
if (r.isError)
|
|
201
|
+
r = await callTool(key, 'get_collection', { listId: which });
|
|
202
|
+
check(r);
|
|
203
|
+
if (options.json)
|
|
204
|
+
return out(JSON.stringify(r.data, null, 2)), 0;
|
|
205
|
+
out(color.bold(r.data?.collection ?? which));
|
|
206
|
+
const systems = r.data?.designSystems ?? [];
|
|
207
|
+
if (systems.length)
|
|
208
|
+
out(`${color.dim('design system:')} ${systems.map((d) => `${d.name} (${d.id})`).join(', ')}`);
|
|
209
|
+
out();
|
|
210
|
+
for (const item of r.data?.items ?? []) {
|
|
211
|
+
out(`${item.name} ${color.dim(`${item.kind === 'design_systems' ? 'design system' : 'block'} · ${item.id}`)}`);
|
|
212
|
+
}
|
|
213
|
+
return 0;
|
|
214
|
+
}
|
package/dist/commands/doctor.js
CHANGED
|
@@ -4,25 +4,11 @@
|
|
|
4
4
|
// fixes it. A diagnostic that only reports a state leaves the user exactly
|
|
5
5
|
// where they started.
|
|
6
6
|
import { existsSync, readFileSync } from 'node:fs';
|
|
7
|
-
import { checkServer, mcpUrl, origin } from '../lib/api.js';
|
|
7
|
+
import { checkServer, mcpUrl, origin, whoami } from '../lib/api.js';
|
|
8
8
|
import { ADAPTERS } from '../adapters/index.js';
|
|
9
|
-
import { ConfigUnreadableError
|
|
10
|
-
import {
|
|
9
|
+
import { ConfigUnreadableError } from '../lib/json-file.js';
|
|
10
|
+
import { readSession } from '../lib/session.js';
|
|
11
11
|
import { color, fail, hint, ok, say, warn } from '../lib/ui.js';
|
|
12
|
-
/** Read back the configured key so we can ask the server whether it still works. */
|
|
13
|
-
function configuredKey(configPath) {
|
|
14
|
-
try {
|
|
15
|
-
const config = readJsonFile(configPath);
|
|
16
|
-
const entry = config?.mcpServers?.[SERVER_NAME];
|
|
17
|
-
const headers = (entry?.headers ?? {});
|
|
18
|
-
const auth = typeof headers.Authorization === 'string' ? headers.Authorization : '';
|
|
19
|
-
const match = auth.match(/^Bearer\s+(\S+)$/i);
|
|
20
|
-
return match?.[1] ?? null;
|
|
21
|
-
}
|
|
22
|
-
catch {
|
|
23
|
-
return null;
|
|
24
|
-
}
|
|
25
|
-
}
|
|
26
12
|
export async function doctor() {
|
|
27
13
|
let problems = 0;
|
|
28
14
|
say();
|
|
@@ -37,13 +23,30 @@ export async function doctor() {
|
|
|
37
23
|
hint('If you are behind a proxy or VPN, that is the usual cause.');
|
|
38
24
|
problems++;
|
|
39
25
|
}
|
|
26
|
+
say();
|
|
27
|
+
say(color.bold('This CLI'));
|
|
28
|
+
const session = readSession();
|
|
29
|
+
if (!session) {
|
|
30
|
+
say(`${color.dim('–')} ${color.dim('not signed in (only needed for "noslopui search" and "get"; run "npx noslopui login")')}`);
|
|
31
|
+
}
|
|
32
|
+
else {
|
|
33
|
+
try {
|
|
34
|
+
const account = await whoami(session.key);
|
|
35
|
+
ok(`Signed in as ${account.email ?? 'your account'} (${account.plan})`);
|
|
36
|
+
}
|
|
37
|
+
catch (err) {
|
|
38
|
+
fail(err.message);
|
|
39
|
+
hint('Run "npx noslopui login" to sign in again.');
|
|
40
|
+
problems++;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
let anyAgent = false;
|
|
40
44
|
for (const adapter of ADAPTERS) {
|
|
45
|
+
if (!(await adapter.detect()))
|
|
46
|
+
continue;
|
|
47
|
+
anyAgent = true;
|
|
41
48
|
say();
|
|
42
49
|
say(color.bold(adapter.label));
|
|
43
|
-
if (!(await adapter.detect())) {
|
|
44
|
-
say(`${color.dim('–')} ${color.dim('not installed on this machine')}`);
|
|
45
|
-
continue;
|
|
46
|
-
}
|
|
47
50
|
let status;
|
|
48
51
|
try {
|
|
49
52
|
status = await adapter.status();
|
|
@@ -59,22 +62,22 @@ export async function doctor() {
|
|
|
59
62
|
}
|
|
60
63
|
if (!status.installed) {
|
|
61
64
|
warn('noslopUI is not set up here.');
|
|
62
|
-
hint(
|
|
65
|
+
hint(`Run "npx noslopui init --agent ${adapter.id}".`);
|
|
63
66
|
problems++;
|
|
64
67
|
}
|
|
65
68
|
else {
|
|
66
69
|
ok(`Configured in ${status.configPath}`);
|
|
67
70
|
if (status.url !== mcpUrl()) {
|
|
68
71
|
warn(`The configured url is ${status.url}, but this CLI expects ${mcpUrl()}.`);
|
|
69
|
-
hint(
|
|
72
|
+
hint(`Run "npx noslopui init --agent ${adapter.id}" to update it.`);
|
|
70
73
|
problems++;
|
|
71
74
|
}
|
|
72
75
|
if (status.problem) {
|
|
73
76
|
warn(status.problem);
|
|
74
|
-
hint(
|
|
77
|
+
hint(`Run "npx noslopui init --agent ${adapter.id}" to repair the entry.`);
|
|
75
78
|
problems++;
|
|
76
79
|
}
|
|
77
|
-
const key = configuredKey(
|
|
80
|
+
const key = await adapter.configuredKey();
|
|
78
81
|
if (key) {
|
|
79
82
|
const probe = await checkServer(key);
|
|
80
83
|
if (probe.keyState === 'ok') {
|
|
@@ -82,13 +85,16 @@ export async function doctor() {
|
|
|
82
85
|
}
|
|
83
86
|
else if (probe.keyState === 'rejected') {
|
|
84
87
|
fail(probe.keyDetail ?? 'The configured key was refused.');
|
|
85
|
-
hint(`
|
|
88
|
+
hint(`Run "npx noslopui login --agent ${adapter.id}" for a fresh key.`);
|
|
86
89
|
problems++;
|
|
87
90
|
}
|
|
88
91
|
}
|
|
89
92
|
}
|
|
90
93
|
const skillPath = adapter.skillPath();
|
|
91
|
-
if (
|
|
94
|
+
if (!skillPath) {
|
|
95
|
+
say(`${color.dim('–')} ${color.dim('no skills folder; the rules reach it through the server')}`);
|
|
96
|
+
}
|
|
97
|
+
else if (existsSync(skillPath)) {
|
|
92
98
|
let current = '';
|
|
93
99
|
try {
|
|
94
100
|
current = readFileSync(skillPath, 'utf8');
|
|
@@ -103,9 +109,14 @@ export async function doctor() {
|
|
|
103
109
|
}
|
|
104
110
|
else {
|
|
105
111
|
warn('The skill is not installed.');
|
|
106
|
-
hint('Optional, but it is what makes "build me a website" use noslopUI at all. Run "npx noslopui
|
|
112
|
+
hint('Optional, but it is what makes "build me a website" use noslopUI at all. Run "npx noslopui update".');
|
|
107
113
|
}
|
|
108
114
|
}
|
|
115
|
+
if (!anyAgent) {
|
|
116
|
+
say();
|
|
117
|
+
warn(`None of the agents this CLI sets up were found (${ADAPTERS.map((a) => a.label).join(', ')}).`);
|
|
118
|
+
hint(`Any other MCP client can be connected by hand: ${origin()}/mcp`);
|
|
119
|
+
}
|
|
109
120
|
say();
|
|
110
121
|
if (problems === 0) {
|
|
111
122
|
ok('Everything checks out.');
|
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();
|