@sellable/install 0.1.0 → 0.1.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 +13 -1
- package/bin/sellable-install.mjs +133 -18
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -6,6 +6,8 @@ Installs Sellable MCP for Claude Code and Codex.
|
|
|
6
6
|
npx -y @sellable/install --host all
|
|
7
7
|
```
|
|
8
8
|
|
|
9
|
+
If you do not pass a token, the installer prompts you for one and shows where to get it.
|
|
10
|
+
|
|
9
11
|
The installer uses package stdio MCP by default:
|
|
10
12
|
|
|
11
13
|
```bash
|
|
@@ -18,4 +20,14 @@ Get a Sellable API token from:
|
|
|
18
20
|
https://app.sellable.dev/settings
|
|
19
21
|
```
|
|
20
22
|
|
|
21
|
-
Then pass it with `--token`
|
|
23
|
+
Then paste it into the installer, or pass it with `--token` / `SELLABLE_TOKEN`.
|
|
24
|
+
|
|
25
|
+
Auth is stored once at:
|
|
26
|
+
|
|
27
|
+
```text
|
|
28
|
+
~/.sellable/config.json
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
Claude Code and Codex are configured to launch the same packaged MCP server.
|
|
32
|
+
|
|
33
|
+
If only one host is installed, `--host all` installs the available host and tells you how to add the other one later.
|
package/bin/sellable-install.mjs
CHANGED
|
@@ -3,6 +3,8 @@ import { spawnSync } from "node:child_process";
|
|
|
3
3
|
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
4
4
|
import { dirname, join } from "node:path";
|
|
5
5
|
import { homedir } from "node:os";
|
|
6
|
+
import { stdin as input, stdout as output } from "node:process";
|
|
7
|
+
import { createInterface } from "node:readline/promises";
|
|
6
8
|
|
|
7
9
|
const DEFAULT_API_URL = "https://app.sellable.dev";
|
|
8
10
|
const DEFAULT_SERVER_PACKAGE =
|
|
@@ -30,7 +32,8 @@ Options:
|
|
|
30
32
|
|
|
31
33
|
Auth:
|
|
32
34
|
Sign up or log in at ${DEFAULT_API_URL}/settings, create an API token, then
|
|
33
|
-
pass it with --token or SELLABLE_TOKEN.
|
|
35
|
+
pass it with --token or SELLABLE_TOKEN. In an interactive terminal, the
|
|
36
|
+
installer will prompt for missing auth values.
|
|
34
37
|
`;
|
|
35
38
|
}
|
|
36
39
|
|
|
@@ -124,6 +127,87 @@ function commandExists(command) {
|
|
|
124
127
|
return result.status === 0;
|
|
125
128
|
}
|
|
126
129
|
|
|
130
|
+
function isInteractive() {
|
|
131
|
+
return Boolean(input.isTTY && output.isTTY);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function authPath() {
|
|
135
|
+
return join(homedir(), ".sellable", "config.json");
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function normalizeStoredAuth(raw) {
|
|
139
|
+
if (raw?.activeEnv && raw?.environments) {
|
|
140
|
+
const env = raw.environments[raw.activeEnv];
|
|
141
|
+
if (!env) return null;
|
|
142
|
+
return {
|
|
143
|
+
token: env.token,
|
|
144
|
+
workspaceId: env.activeWorkspaceId || env.workspaceId,
|
|
145
|
+
apiUrl: env.apiUrl,
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
return {
|
|
150
|
+
token: raw?.token,
|
|
151
|
+
workspaceId: raw?.activeWorkspaceId || raw?.workspaceId,
|
|
152
|
+
apiUrl: raw?.apiUrl,
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function readStoredAuth() {
|
|
157
|
+
const raw = readExisting(authPath());
|
|
158
|
+
return raw ? normalizeStoredAuth(raw) : null;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
async function promptForMissingAuth(opts) {
|
|
162
|
+
if (opts.token && opts.workspaceId) return opts;
|
|
163
|
+
|
|
164
|
+
const stored = readStoredAuth();
|
|
165
|
+
if (stored?.token && stored?.workspaceId) {
|
|
166
|
+
opts.token ||= stored.token;
|
|
167
|
+
opts.workspaceId ||= stored.workspaceId;
|
|
168
|
+
opts.apiUrl = opts.apiUrl || stored.apiUrl || DEFAULT_API_URL;
|
|
169
|
+
opts.authFromExistingConfig = true;
|
|
170
|
+
console.log(`Using existing Sellable auth config: ${authPath()}`);
|
|
171
|
+
return opts;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
if (!isInteractive()) {
|
|
175
|
+
throw new Error(
|
|
176
|
+
[
|
|
177
|
+
"Missing Sellable token/workspace id.",
|
|
178
|
+
`Create a token at ${opts.apiUrl}/settings, then rerun:`,
|
|
179
|
+
" npx -y @sellable/install --host all --token <token> --workspace-id <workspace_id>",
|
|
180
|
+
"",
|
|
181
|
+
"You can also use SELLABLE_TOKEN and SELLABLE_WORKSPACE_ID.",
|
|
182
|
+
].join("\n")
|
|
183
|
+
);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
console.log("");
|
|
187
|
+
console.log("Sellable needs one API token to connect Claude/Codex to your workspace.");
|
|
188
|
+
console.log(`Open ${opts.apiUrl}/settings, create a token, then paste the values below.`);
|
|
189
|
+
console.log(`Auth will be stored once at ${authPath()}.`);
|
|
190
|
+
console.log("");
|
|
191
|
+
|
|
192
|
+
const rl = createInterface({ input, output });
|
|
193
|
+
try {
|
|
194
|
+
if (!opts.token) {
|
|
195
|
+
opts.token = (await rl.question("Sellable API token: ")).trim();
|
|
196
|
+
}
|
|
197
|
+
if (!opts.workspaceId) {
|
|
198
|
+
opts.workspaceId = (await rl.question("Sellable workspace id: ")).trim();
|
|
199
|
+
}
|
|
200
|
+
} finally {
|
|
201
|
+
rl.close();
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
if (!opts.token || !opts.workspaceId) {
|
|
205
|
+
throw new Error("Sellable token and workspace id are both required.");
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
return opts;
|
|
209
|
+
}
|
|
210
|
+
|
|
127
211
|
function writeJson(path, data, opts) {
|
|
128
212
|
const redacted = JSON.stringify({ ...data, token: redact(data.token) }, null, 2);
|
|
129
213
|
console.log(`Writing ${path}: ${redacted}`);
|
|
@@ -154,13 +238,13 @@ function writeAuth(opts) {
|
|
|
154
238
|
apiUrl: opts.apiUrl,
|
|
155
239
|
};
|
|
156
240
|
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
241
|
+
if (opts.authFromExistingConfig) {
|
|
242
|
+
console.log(`Leaving existing Sellable auth config unchanged: ${authPath()}`);
|
|
243
|
+
return;
|
|
244
|
+
}
|
|
160
245
|
|
|
246
|
+
const sellablePath = authPath();
|
|
161
247
|
writeJson(sellablePath, config, opts);
|
|
162
|
-
if (opts.host === "claude" || opts.host === "all") writeJson(claudePath, config, opts);
|
|
163
|
-
if (opts.host === "codex" || opts.host === "all") writeJson(codexPath, config, opts);
|
|
164
248
|
}
|
|
165
249
|
|
|
166
250
|
function mcpCommand(opts) {
|
|
@@ -183,11 +267,16 @@ function mcpCommand(opts) {
|
|
|
183
267
|
|
|
184
268
|
function installClaude(opts) {
|
|
185
269
|
if (!commandExists("claude")) {
|
|
186
|
-
|
|
270
|
+
const message = "Claude CLI not found. Install/login to Claude Code, then rerun: sellable --host claude";
|
|
271
|
+
if (opts.host === "all") {
|
|
272
|
+
console.log(`Skipping Claude Code: ${message}`);
|
|
273
|
+
return false;
|
|
274
|
+
}
|
|
275
|
+
throw new Error(message);
|
|
187
276
|
}
|
|
188
277
|
if (opts.server === "hosted") {
|
|
189
278
|
run("claude", ["mcp", "add", "--transport", "http", "sellable", opts.hostedUrl], opts);
|
|
190
|
-
return;
|
|
279
|
+
return true;
|
|
191
280
|
}
|
|
192
281
|
const [command, args] = mcpCommand(opts);
|
|
193
282
|
run("claude", ["mcp", "remove", "sellable"], {
|
|
@@ -196,15 +285,21 @@ function installClaude(opts) {
|
|
|
196
285
|
allowFail: true,
|
|
197
286
|
});
|
|
198
287
|
run("claude", ["mcp", "add", "--transport", "stdio", "sellable", "--", command, ...args], opts);
|
|
288
|
+
return true;
|
|
199
289
|
}
|
|
200
290
|
|
|
201
291
|
function installCodex(opts) {
|
|
202
292
|
if (!commandExists("codex")) {
|
|
203
|
-
|
|
293
|
+
const message = "Codex CLI not found. Install/login to Codex, then rerun: sellable --host codex";
|
|
294
|
+
if (opts.host === "all") {
|
|
295
|
+
console.log(`Skipping Codex: ${message}`);
|
|
296
|
+
return false;
|
|
297
|
+
}
|
|
298
|
+
throw new Error(message);
|
|
204
299
|
}
|
|
205
300
|
if (opts.server === "hosted") {
|
|
206
301
|
run("codex", ["mcp", "add", "sellable", "--url", opts.hostedUrl], opts);
|
|
207
|
-
return;
|
|
302
|
+
return true;
|
|
208
303
|
}
|
|
209
304
|
const [command, args] = mcpCommand(opts);
|
|
210
305
|
run("codex", ["mcp", "remove", "sellable"], {
|
|
@@ -213,15 +308,15 @@ function installCodex(opts) {
|
|
|
213
308
|
allowFail: true,
|
|
214
309
|
});
|
|
215
310
|
run("codex", ["mcp", "add", "sellable", "--", command, ...args], opts);
|
|
311
|
+
return true;
|
|
216
312
|
}
|
|
217
313
|
|
|
218
314
|
function verify(opts) {
|
|
219
|
-
const
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
throw new Error(`Sellable auth config missing or incomplete: ${authPath}`);
|
|
315
|
+
const stored = readStoredAuth();
|
|
316
|
+
if (!stored?.token || !stored?.workspaceId) {
|
|
317
|
+
throw new Error(`Sellable auth config missing or incomplete: ${authPath()}`);
|
|
223
318
|
}
|
|
224
|
-
console.log(`Sellable auth config present: ${authPath}`);
|
|
319
|
+
console.log(`Sellable auth config present: ${authPath()}`);
|
|
225
320
|
if (opts.host === "claude" || opts.host === "all") {
|
|
226
321
|
console.log(commandExists("claude") ? "Claude CLI present" : "Claude CLI missing");
|
|
227
322
|
}
|
|
@@ -230,7 +325,20 @@ function verify(opts) {
|
|
|
230
325
|
}
|
|
231
326
|
}
|
|
232
327
|
|
|
233
|
-
function
|
|
328
|
+
function printNextSteps(installedHosts) {
|
|
329
|
+
console.log("");
|
|
330
|
+
console.log("Next steps:");
|
|
331
|
+
if (installedHosts.length > 0) {
|
|
332
|
+
console.log(`1. Fully quit and reopen ${installedHosts.join(" and ")} so MCP tools reload.`);
|
|
333
|
+
console.log("2. Start a new thread and choose Sellable Create Campaign.");
|
|
334
|
+
console.log("3. If tools do not appear, run: sellable --verify-only --host all");
|
|
335
|
+
} else {
|
|
336
|
+
console.log("1. Install Claude Code or Codex, then rerun this installer for that host.");
|
|
337
|
+
console.log("2. Verify auth later with: sellable --verify-only --host all");
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
async function main() {
|
|
234
342
|
try {
|
|
235
343
|
const opts = parseArgs(process.argv.slice(2));
|
|
236
344
|
if (opts.help) {
|
|
@@ -244,10 +352,16 @@ function main() {
|
|
|
244
352
|
console.log(`- api: ${opts.apiUrl}`);
|
|
245
353
|
console.log(`- token: ${opts.token ? redact(opts.token) : "(missing)"}`);
|
|
246
354
|
|
|
355
|
+
const installedHosts = [];
|
|
247
356
|
if (!opts.verifyOnly) {
|
|
357
|
+
await promptForMissingAuth(opts);
|
|
248
358
|
writeAuth(opts);
|
|
249
|
-
if (opts.host === "claude" || opts.host === "all")
|
|
250
|
-
|
|
359
|
+
if (opts.host === "claude" || opts.host === "all") {
|
|
360
|
+
if (installClaude(opts)) installedHosts.push("Claude Code");
|
|
361
|
+
}
|
|
362
|
+
if (opts.host === "codex" || opts.host === "all") {
|
|
363
|
+
if (installCodex(opts)) installedHosts.push("Codex");
|
|
364
|
+
}
|
|
251
365
|
}
|
|
252
366
|
if (opts.dryRun) {
|
|
253
367
|
console.log("Dry run complete; verification skipped because no files were written.");
|
|
@@ -255,6 +369,7 @@ function main() {
|
|
|
255
369
|
verify(opts);
|
|
256
370
|
}
|
|
257
371
|
console.log("Sellable install complete.");
|
|
372
|
+
if (!opts.verifyOnly && !opts.dryRun) printNextSteps(installedHosts);
|
|
258
373
|
} catch (error) {
|
|
259
374
|
console.error(error instanceof Error ? error.message : String(error));
|
|
260
375
|
process.exitCode = 1;
|