stoaken 0.3.0 → 0.4.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 +114 -85
- package/dist/index.js +223 -19
- package/package.json +51 -51
package/README.md
CHANGED
|
@@ -1,85 +1,114 @@
|
|
|
1
|
-
# stoaken
|
|
2
|
-
|
|
3
|
-
The official CLI for [Stoaken](https://www.stoaken.com) -- a design token management platform (Brands, Token Sets, Themes, DTCG-conformant token storage, versioned Releases with an approval gate) that AI coding agents and MCP-capable tools can read and propose changes to directly.
|
|
4
|
-
|
|
5
|
-
## Quickstart
|
|
6
|
-
|
|
7
|
-
```
|
|
8
|
-
npx stoaken init
|
|
9
|
-
```
|
|
10
|
-
|
|
11
|
-
Run it in the root of the project you want connected to a Stoaken Brand. It will:
|
|
12
|
-
|
|
13
|
-
1. Ask how you'd like to sign in -- open your browser for a real OAuth 2.1 login, or let you paste an API key you've already created at `/settings/api-keys`.
|
|
14
|
-
2. List the real Brands your account can see, and let you pick one.
|
|
15
|
-
3. Write `stoaken.config.json` (which Brand, which server -- safe to commit, holds no secret) and `.stoaken/credentials.json` (your bearer credential, `chmod 600`, automatically added to `.gitignore`).
|
|
16
|
-
4.
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
```
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
the
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
1
|
+
# stoaken
|
|
2
|
+
|
|
3
|
+
The official CLI for [Stoaken](https://www.stoaken.com) -- a design token management platform (Brands, Token Sets, Themes, DTCG-conformant token storage, versioned Releases with an approval gate) that AI coding agents and MCP-capable tools can read and propose changes to directly.
|
|
4
|
+
|
|
5
|
+
## Quickstart
|
|
6
|
+
|
|
7
|
+
```
|
|
8
|
+
npx stoaken init
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
Run it in the root of the project you want connected to a Stoaken Brand. It will:
|
|
12
|
+
|
|
13
|
+
1. Ask how you'd like to sign in -- open your browser for a real OAuth 2.1 login, or let you paste an API key you've already created at `/settings/api-keys`.
|
|
14
|
+
2. List the real Brands your account can see, and let you pick one.
|
|
15
|
+
3. Write `stoaken.config.json` (which Brand, which server -- safe to commit, holds no secret) and `.stoaken/credentials.json` (your bearer credential, `chmod 600`, automatically added to `.gitignore`).
|
|
16
|
+
4. Ask which AI tool to connect (or take `--client`) and write that tool's MCP config.
|
|
17
|
+
5. Generate `.stoaken/SKILL.md` -- a short brief for AI coding agents explaining how this project connects to Stoaken's MCP server and what its tools do.
|
|
18
|
+
|
|
19
|
+
Run `stoaken init` again in an already-configured directory to continue with the same Brand, switch to a different one, or re-authenticate.
|
|
20
|
+
|
|
21
|
+
## Connect an AI tool
|
|
22
|
+
|
|
23
|
+
```
|
|
24
|
+
npx stoaken init --client cursor
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
| `--client` | What init writes |
|
|
28
|
+
| --- | --- |
|
|
29
|
+
| `claude-code` | `.mcp.json` (`mcpServers.stoaken`, `type: http`) |
|
|
30
|
+
| `cursor` | `.cursor/mcp.json` (`mcpServers.stoaken`) |
|
|
31
|
+
| `codex` | `.codex/config.toml` (`[mcp_servers.stoaken]`; loads for trusted projects only) |
|
|
32
|
+
| `copilot` | `.vscode/mcp.json` (`servers.stoaken`, VS Code agent mode) |
|
|
33
|
+
| `claude-desktop` | Nothing: prints the steps to add a custom connector (Customize > Connectors) |
|
|
34
|
+
| `v0` | Nothing: prints the steps to add a custom MCP server in v0 (+ > MCPs) |
|
|
35
|
+
|
|
36
|
+
init merges: other servers in the file are kept and only the `stoaken` entry is replaced. If the
|
|
37
|
+
file isn't plain JSON (for example `.vscode/mcp.json` with comments) it is left alone and you're told.
|
|
38
|
+
No secret goes into these files, so they are safe to commit:
|
|
39
|
+
|
|
40
|
+
- Signed in with the browser: the entry has just the URL, and the tool signs you in itself with
|
|
41
|
+
OAuth (Claude Code `/mcp`, Cursor Connect, `codex mcp login stoaken`, VS Code Start).
|
|
42
|
+
- Pasted an API key: the entry reads the `STOAKEN_API_KEY` environment variable (VS Code instead
|
|
43
|
+
asks for the key once and stores it securely).
|
|
44
|
+
|
|
45
|
+
## Authentication
|
|
46
|
+
|
|
47
|
+
Two paths, both landing on the same bearer credential:
|
|
48
|
+
|
|
49
|
+
- **Log in with browser** -- a real OAuth 2.1 Authorization Code + PKCE flow. The CLI discovers Stoaken's Authorization Server automatically (via its `/.well-known/oauth-protected-resource` metadata), registers itself as an OAuth client on first run, and opens your browser to sign in.
|
|
50
|
+
- **Paste an API key** -- for CI, scripts, or anywhere a browser isn't available. Create one at `/settings/api-keys` in the Stoaken app.
|
|
51
|
+
|
|
52
|
+
## Export your tokens as code
|
|
53
|
+
|
|
54
|
+
`stoaken export` writes a theme's live release as a file: CSS variables, W3C JSON, Style
|
|
55
|
+
Dictionary v4 JSON, or a JS/TS module. There are no timestamps in the output, so committed files
|
|
56
|
+
give clean diffs.
|
|
57
|
+
|
|
58
|
+
```sh
|
|
59
|
+
npx stoaken export --theme Default --format css --out src/styles/tokens.css
|
|
60
|
+
npx stoaken export --theme Default --format json-dtcg --mode color-scheme:dark --out tokens/dark.json
|
|
61
|
+
npx stoaken export --theme Default --format ts --out src/tokens.ts
|
|
62
|
+
npx stoaken export --theme Default --out - # to stdout
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
| Option | |
|
|
66
|
+
| --- | --- |
|
|
67
|
+
| `--theme <name\|id>` | Which theme (needed when the Brand has more than one) |
|
|
68
|
+
| `--format` | `css` (default), `json-dtcg`, `json-sd4`, `js`, `ts` |
|
|
69
|
+
| `--mode <key>` | A mode combination for the one-mode formats; CSS includes every mode |
|
|
70
|
+
| `--prefix <prefix>` | CSS only: a prefix for every variable name |
|
|
71
|
+
| `--release <id>` | One release (e.g. one still in review) instead of the live one |
|
|
72
|
+
| `--out <file\|->` | Where to write; default is the server's file name |
|
|
73
|
+
| `--check` | Write nothing; exit 1 if the file on disk is out of date |
|
|
74
|
+
|
|
75
|
+
Save your targets in `stoaken.config.json` and a plain `npx stoaken export` runs them all
|
|
76
|
+
(`npx stoaken export --check` checks them all):
|
|
77
|
+
|
|
78
|
+
```json
|
|
79
|
+
{
|
|
80
|
+
"brandId": "…",
|
|
81
|
+
"brandName": "Acme",
|
|
82
|
+
"serverUrl": "https://www.stoaken.com",
|
|
83
|
+
"exports": [
|
|
84
|
+
{ "theme": "Default", "format": "css", "out": "src/styles/tokens.css" },
|
|
85
|
+
{ "theme": "Default", "format": "ts", "out": "src/tokens.ts" }
|
|
86
|
+
]
|
|
87
|
+
}
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
**In CI**, set `STOAKEN_TOKEN` to an API key (Settings > API keys); it's used instead of the
|
|
91
|
+
sign-in saved by `stoaken init`. For example, to fail a build when the committed tokens are behind
|
|
92
|
+
the live release:
|
|
93
|
+
|
|
94
|
+
```sh
|
|
95
|
+
STOAKEN_TOKEN=${{ secrets.STOAKEN_TOKEN }} npx stoaken export --check
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
`--check` ignores CRLF vs LF line endings (unreleased change, Stage 332), so Windows runners
|
|
99
|
+
that check files out as CRLF don't fail it. Ready-made pipelines for GitHub Actions, GitLab CI,
|
|
100
|
+
Bitbucket Pipelines, Azure Pipelines and Vercel builds: see /docs/webhooks#ci and /docs/cli#ci.
|
|
101
|
+
|
|
102
|
+
## What this connects to
|
|
103
|
+
|
|
104
|
+
Once set up, any MCP-capable client (Claude Code, Claude Desktop, Cursor, Codex, Copilot, v0) or a direct HTTP client can reach Stoaken's MCP server at `/mcp`. Its main tools (full list at /docs/mcp):
|
|
105
|
+
|
|
106
|
+
- `list_brands` -- every Brand you hold at least Viewer on.
|
|
107
|
+
- `get_theme_tokens(themeId)` -- a Theme's cascade-merged token tree.
|
|
108
|
+
- `list_releases(themeId)` -- a Theme's Releases with version, status, and change summary.
|
|
109
|
+
- `propose_token_change(themeId, tokenSetId, document, version, versionType)` -- proposes a token change and submits it for approval. **This never publishes.** A human reviewer always approves through the Stoaken Command Center before it goes live.
|
|
110
|
+
|
|
111
|
+
## Links
|
|
112
|
+
|
|
113
|
+
- [Stoaken on GitHub](https://github.com/craig-astravale/stoaken)
|
|
114
|
+
- [Report an issue](https://github.com/craig-astravale/stoaken/issues)
|
package/dist/index.js
CHANGED
|
@@ -209,13 +209,12 @@ async function promptForApiKey() {
|
|
|
209
209
|
}
|
|
210
210
|
|
|
211
211
|
// src/mcp-client.ts
|
|
212
|
-
import { Client } from "@modelcontextprotocol/
|
|
213
|
-
import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
|
|
212
|
+
import { Client, StreamableHTTPClientTransport } from "@modelcontextprotocol/client";
|
|
214
213
|
async function listBrands(serverUrl, credential) {
|
|
215
214
|
const transport = new StreamableHTTPClientTransport(new URL("/mcp", serverUrl), {
|
|
216
215
|
requestInit: { headers: { Authorization: `Bearer ${credential}` } }
|
|
217
216
|
});
|
|
218
|
-
const client = new Client({ name: "stoaken-cli", version: "0.
|
|
217
|
+
const client = new Client({ name: "stoaken-cli", version: "0.4.0" });
|
|
219
218
|
await client.connect(transport);
|
|
220
219
|
try {
|
|
221
220
|
const result = await client.callTool({ name: "list_brands", arguments: {} });
|
|
@@ -229,16 +228,169 @@ async function listBrands(serverUrl, credential) {
|
|
|
229
228
|
}
|
|
230
229
|
}
|
|
231
230
|
|
|
231
|
+
// src/client-config.ts
|
|
232
|
+
var CLIENTS = ["claude-code", "claude-desktop", "cursor", "codex", "copilot", "v0"];
|
|
233
|
+
var SERVER_NAME = "stoaken";
|
|
234
|
+
var API_KEY_ENV = "STOAKEN_API_KEY";
|
|
235
|
+
var CLIENT_LABELS = {
|
|
236
|
+
"claude-code": "Claude Code",
|
|
237
|
+
"claude-desktop": "Claude Desktop / claude.ai",
|
|
238
|
+
cursor: "Cursor",
|
|
239
|
+
codex: "OpenAI Codex",
|
|
240
|
+
copilot: "GitHub Copilot (VS Code)",
|
|
241
|
+
v0: "v0 (Vercel)"
|
|
242
|
+
};
|
|
243
|
+
function isClient(value) {
|
|
244
|
+
return CLIENTS.includes(value);
|
|
245
|
+
}
|
|
246
|
+
function mcpUrl(serverUrl) {
|
|
247
|
+
return `${serverUrl.replace(/\/+$/, "")}/mcp`;
|
|
248
|
+
}
|
|
249
|
+
function planFor(client, auth, serverUrl) {
|
|
250
|
+
const url = mcpUrl(serverUrl);
|
|
251
|
+
const keyStep = `Set ${API_KEY_ENV} in your shell to an API key from Settings > API keys (never commit it).`;
|
|
252
|
+
switch (client) {
|
|
253
|
+
case "claude-code":
|
|
254
|
+
return {
|
|
255
|
+
file: ".mcp.json",
|
|
256
|
+
nextSteps: auth === "oauth" ? ["Start Claude Code, approve the project server, then run /mcp and choose stoaken to sign in."] : [keyStep, "Start Claude Code and approve the project server."]
|
|
257
|
+
};
|
|
258
|
+
case "cursor":
|
|
259
|
+
return {
|
|
260
|
+
file: ".cursor/mcp.json",
|
|
261
|
+
nextSteps: auth === "oauth" ? ["Open Cursor Settings > MCP, enable stoaken and click Connect to sign in."] : [keyStep, "Restart Cursor so it picks up the variable, then enable stoaken in Settings > MCP."]
|
|
262
|
+
};
|
|
263
|
+
case "codex":
|
|
264
|
+
return {
|
|
265
|
+
file: ".codex/config.toml",
|
|
266
|
+
nextSteps: auth === "oauth" ? ["Trust this project in Codex (project config only loads for trusted projects).", "Run: codex mcp login stoaken"] : [keyStep, "Trust this project in Codex (project config only loads for trusted projects)."]
|
|
267
|
+
};
|
|
268
|
+
case "copilot":
|
|
269
|
+
return {
|
|
270
|
+
file: ".vscode/mcp.json",
|
|
271
|
+
nextSteps: auth === "oauth" ? ["In VS Code open .vscode/mcp.json, click Start above stoaken and sign in in the browser.", "Use Copilot Chat in Agent mode."] : ["In VS Code open .vscode/mcp.json, click Start above stoaken and paste an API key when asked (VS Code stores it securely).", "Use Copilot Chat in Agent mode."]
|
|
272
|
+
};
|
|
273
|
+
case "claude-desktop":
|
|
274
|
+
return {
|
|
275
|
+
file: null,
|
|
276
|
+
nextSteps: [
|
|
277
|
+
"Claude Desktop and claude.ai add remote servers as connectors, not through a project file.",
|
|
278
|
+
"Customize > Connectors > + Add > Add custom connector.",
|
|
279
|
+
`Name: Stoaken. URL: ${url}. Sign in with OAuth (Register automatically), or add the header Authorization: Bearer <API key>.`,
|
|
280
|
+
"On Team/Enterprise only an Owner can add it. Connections come from Anthropic's cloud, so this needs the public server."
|
|
281
|
+
]
|
|
282
|
+
};
|
|
283
|
+
case "v0":
|
|
284
|
+
return {
|
|
285
|
+
file: null,
|
|
286
|
+
nextSteps: [
|
|
287
|
+
"v0 adds MCP servers in its own UI, not through a project file.",
|
|
288
|
+
"In a v0 chat: + > MCPs > custom server.",
|
|
289
|
+
`URL: ${url}. Auth: OAuth, or Bearer Token with an API key from Settings > API keys.`,
|
|
290
|
+
"v0 uses the tools while it generates; the app it builds cannot call them."
|
|
291
|
+
]
|
|
292
|
+
};
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
function jsonEntry(client, auth, serverUrl) {
|
|
296
|
+
const url = mcpUrl(serverUrl);
|
|
297
|
+
if (client === "claude-code") {
|
|
298
|
+
return auth === "oauth" ? { type: "http", url } : { type: "http", url, headers: { Authorization: `Bearer \${${API_KEY_ENV}}` } };
|
|
299
|
+
}
|
|
300
|
+
if (client === "cursor") {
|
|
301
|
+
return auth === "oauth" ? { url } : { url, headers: { Authorization: `Bearer \${env:${API_KEY_ENV}}` } };
|
|
302
|
+
}
|
|
303
|
+
return auth === "oauth" ? { type: "http", url } : { type: "http", url, headers: { Authorization: "Bearer ${input:stoaken-api-key}" } };
|
|
304
|
+
}
|
|
305
|
+
var ConfigMergeError = class extends Error {
|
|
306
|
+
};
|
|
307
|
+
function parseObject(existing, file) {
|
|
308
|
+
if (existing === null || existing.trim() === "") return {};
|
|
309
|
+
let parsed;
|
|
310
|
+
try {
|
|
311
|
+
parsed = JSON.parse(existing);
|
|
312
|
+
} catch {
|
|
313
|
+
throw new ConfigMergeError(`${file} isn't plain JSON (comments?), so it was left alone. Add the stoaken entry by hand.`);
|
|
314
|
+
}
|
|
315
|
+
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
|
|
316
|
+
throw new ConfigMergeError(`${file} isn't a JSON object, so it was left alone.`);
|
|
317
|
+
}
|
|
318
|
+
return parsed;
|
|
319
|
+
}
|
|
320
|
+
var COPILOT_INPUT = {
|
|
321
|
+
type: "promptString",
|
|
322
|
+
id: "stoaken-api-key",
|
|
323
|
+
description: "Stoaken API key (Settings > API keys)",
|
|
324
|
+
password: true
|
|
325
|
+
};
|
|
326
|
+
function mergeJsonConfig(client, existing, auth, serverUrl) {
|
|
327
|
+
const file = planFor(client, auth, serverUrl).file;
|
|
328
|
+
const doc = parseObject(existing, file);
|
|
329
|
+
const key = client === "copilot" ? "servers" : "mcpServers";
|
|
330
|
+
const current = doc[key];
|
|
331
|
+
if (current !== void 0 && (typeof current !== "object" || current === null || Array.isArray(current))) {
|
|
332
|
+
throw new ConfigMergeError(`${file} has a "${key}" that isn't an object, so it was left alone.`);
|
|
333
|
+
}
|
|
334
|
+
doc[key] = { ...current, [SERVER_NAME]: jsonEntry(client, auth, serverUrl) };
|
|
335
|
+
if (client === "copilot") {
|
|
336
|
+
const inputs = Array.isArray(doc.inputs) ? doc.inputs : [];
|
|
337
|
+
const others = inputs.filter((i) => !(typeof i === "object" && i !== null && i.id === COPILOT_INPUT.id));
|
|
338
|
+
if (auth === "api_key") doc.inputs = [...others, COPILOT_INPUT];
|
|
339
|
+
else if (doc.inputs !== void 0) doc.inputs = others;
|
|
340
|
+
}
|
|
341
|
+
return `${JSON.stringify(doc, null, 2)}
|
|
342
|
+
`;
|
|
343
|
+
}
|
|
344
|
+
function codexTable(auth, serverUrl) {
|
|
345
|
+
const lines = [`[mcp_servers.${SERVER_NAME}]`, `url = ${JSON.stringify(mcpUrl(serverUrl))}`];
|
|
346
|
+
if (auth === "api_key") lines.push(`bearer_token_env_var = "${API_KEY_ENV}"`);
|
|
347
|
+
return `${lines.join("\n")}
|
|
348
|
+
`;
|
|
349
|
+
}
|
|
350
|
+
function mergeCodexToml(existing, auth, serverUrl) {
|
|
351
|
+
const table = codexTable(auth, serverUrl);
|
|
352
|
+
if (existing === null || existing.trim() === "") return table;
|
|
353
|
+
const header = /^\s*\[\s*mcp_servers\.(?:stoaken|"stoaken")\s*(?:\]|\.)/;
|
|
354
|
+
const anyHeader = /^\s*\[/;
|
|
355
|
+
const kept = [];
|
|
356
|
+
let skipping = false;
|
|
357
|
+
for (const line of existing.split("\n")) {
|
|
358
|
+
if (header.test(line)) {
|
|
359
|
+
skipping = true;
|
|
360
|
+
continue;
|
|
361
|
+
}
|
|
362
|
+
if (skipping && anyHeader.test(line)) skipping = false;
|
|
363
|
+
if (!skipping) kept.push(line);
|
|
364
|
+
}
|
|
365
|
+
const body = kept.join("\n").replace(/\n+$/, "");
|
|
366
|
+
return body === "" ? table : `${body}
|
|
367
|
+
|
|
368
|
+
${table}`;
|
|
369
|
+
}
|
|
370
|
+
|
|
232
371
|
// src/skill-template.ts
|
|
233
|
-
function
|
|
372
|
+
function connectSection(target, serverUrl) {
|
|
373
|
+
if (!target) {
|
|
374
|
+
return `Connect with the credential in \`.stoaken/credentials.json\` as a Bearer token (never commit that
|
|
375
|
+
file -- it's already in \`.gitignore\`).
|
|
376
|
+
`;
|
|
377
|
+
}
|
|
378
|
+
const { client, auth } = target;
|
|
379
|
+
const plan = planFor(client, auth, serverUrl);
|
|
380
|
+
const how = auth === "oauth" ? "the client signs in with OAuth" : `it reads the \`${API_KEY_ENV}\` environment variable`;
|
|
381
|
+
const where = plan.file ? `It is configured for ${CLIENT_LABELS[client]} in \`${plan.file}\` (no secret in that file: ${how}).` : `${CLIENT_LABELS[client]} connects through its own settings, not a project file.`;
|
|
382
|
+
return `${where}
|
|
383
|
+
|
|
384
|
+
${plan.nextSteps.map((step) => `- ${step}`).join("\n")}
|
|
385
|
+
`;
|
|
386
|
+
}
|
|
387
|
+
function generateSkillFile(config, client) {
|
|
234
388
|
return `# Working with Stoaken
|
|
235
389
|
|
|
236
390
|
This project is connected to the **${config.brandName}** Brand on Stoaken (\`${config.serverUrl}\`).
|
|
237
391
|
|
|
238
|
-
Stoaken's MCP server exposes the tools below
|
|
239
|
-
|
|
240
|
-
it's already in \`.gitignore\`).
|
|
241
|
-
|
|
392
|
+
Stoaken's MCP server exposes the tools below at \`${mcpUrl(config.serverUrl)}\`.
|
|
393
|
+
${connectSection(client, config.serverUrl)}
|
|
242
394
|
## Tools
|
|
243
395
|
|
|
244
396
|
- **list_brands** -- every Brand you hold at least Viewer on.
|
|
@@ -283,6 +435,9 @@ Re-run \`stoaken init\` if you need to switch Brands or re-authenticate.
|
|
|
283
435
|
}
|
|
284
436
|
|
|
285
437
|
// src/commands/init.ts
|
|
438
|
+
import { existsSync as existsSync2 } from "fs";
|
|
439
|
+
import { mkdir as mkdir2, readFile as readFile2, writeFile as writeFile2 } from "fs/promises";
|
|
440
|
+
import { dirname, join as join2 } from "path";
|
|
286
441
|
var DEFAULT_SERVER_URL = "https://www.stoaken.com";
|
|
287
442
|
async function runInit(options) {
|
|
288
443
|
const cwd = process.cwd();
|
|
@@ -378,13 +533,48 @@ async function runInit(options) {
|
|
|
378
533
|
};
|
|
379
534
|
await writeConfig(cwd, config);
|
|
380
535
|
await ensureGitignored(cwd);
|
|
381
|
-
|
|
536
|
+
let client = null;
|
|
537
|
+
if (options.client) {
|
|
538
|
+
client = options.client;
|
|
539
|
+
} else {
|
|
540
|
+
const pick = await p2.select({
|
|
541
|
+
message: "Which AI tool should connect to Stoaken?",
|
|
542
|
+
options: [
|
|
543
|
+
...CLIENTS.map((c) => ({ value: c, label: CLIENT_LABELS[c] })),
|
|
544
|
+
{ value: "none", label: "None for now (just write .stoaken/SKILL.md)" }
|
|
545
|
+
]
|
|
546
|
+
});
|
|
547
|
+
if (p2.isCancel(pick)) {
|
|
548
|
+
p2.cancel("Cancelled.");
|
|
549
|
+
return;
|
|
550
|
+
}
|
|
551
|
+
client = pick === "none" ? null : pick;
|
|
552
|
+
}
|
|
553
|
+
const auth = (await readCredentials(cwd))?.kind === "api_key" ? "api_key" : "oauth";
|
|
554
|
+
if (client) {
|
|
555
|
+
const plan = planFor(client, auth, serverUrl);
|
|
556
|
+
if (plan.file) {
|
|
557
|
+
const path = join2(cwd, plan.file);
|
|
558
|
+
const existingText = existsSync2(path) ? await readFile2(path, "utf8") : null;
|
|
559
|
+
try {
|
|
560
|
+
const next = client === "codex" ? mergeCodexToml(existingText, auth, serverUrl) : mergeJsonConfig(client, existingText, auth, serverUrl);
|
|
561
|
+
await mkdir2(dirname(path), { recursive: true });
|
|
562
|
+
await writeFile2(path, next, "utf8");
|
|
563
|
+
p2.log.success(`${existingText === null ? "Wrote" : "Updated"} ${plan.file} (other servers kept).`);
|
|
564
|
+
} catch (error) {
|
|
565
|
+
if (!(error instanceof ConfigMergeError)) throw error;
|
|
566
|
+
p2.log.warn(error.message);
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
p2.note(plan.nextSteps.map((s) => `- ${s}`).join("\n"), `Next, in ${CLIENT_LABELS[client]}`);
|
|
570
|
+
}
|
|
571
|
+
await writeSkillFile(cwd, generateSkillFile(config, client ? { client, auth } : void 0));
|
|
382
572
|
p2.outro(`Done -- this project is set up for ${config.brandName}.`);
|
|
383
573
|
}
|
|
384
574
|
|
|
385
575
|
// src/commands/export.ts
|
|
386
|
-
import { mkdir as
|
|
387
|
-
import { dirname, relative, resolve } from "path";
|
|
576
|
+
import { mkdir as mkdir3, readFile as readFile3, writeFile as writeFile3 } from "fs/promises";
|
|
577
|
+
import { dirname as dirname2, relative, resolve } from "path";
|
|
388
578
|
import * as p3 from "@clack/prompts";
|
|
389
579
|
|
|
390
580
|
// src/api.ts
|
|
@@ -400,7 +590,7 @@ async function call(serverUrl, token, path) {
|
|
|
400
590
|
let response;
|
|
401
591
|
try {
|
|
402
592
|
response = await fetch(new URL(path, serverUrl), {
|
|
403
|
-
headers: { Authorization: `Bearer ${token}`, "User-Agent": "stoaken-cli/0.
|
|
593
|
+
headers: { Authorization: `Bearer ${token}`, "User-Agent": "stoaken-cli/0.4.0" }
|
|
404
594
|
});
|
|
405
595
|
} catch (error) {
|
|
406
596
|
throw new Error(`Couldn't reach ${serverUrl}: ${error instanceof Error ? error.message : String(error)}`);
|
|
@@ -562,6 +752,11 @@ function problemMessage(problem, context) {
|
|
|
562
752
|
return `Stoaken answered ${problem.status}${problem.detail ? `: ${problem.detail}` : problem.title ? `: ${problem.title}` : "."}`;
|
|
563
753
|
}
|
|
564
754
|
}
|
|
755
|
+
function isUpToDate(onDisk, live) {
|
|
756
|
+
if (onDisk === null) return false;
|
|
757
|
+
const lf = (s) => s.replace(/\r\n/g, "\n");
|
|
758
|
+
return lf(onDisk) === lf(live);
|
|
759
|
+
}
|
|
565
760
|
function formatBytes(n) {
|
|
566
761
|
return n < 1024 ? `${n} B` : `${(n / 1024).toFixed(1)} KB`;
|
|
567
762
|
}
|
|
@@ -659,17 +854,17 @@ async function exportOne(serverUrl, token, target, theme, check, cwd) {
|
|
|
659
854
|
if (check) {
|
|
660
855
|
let current = null;
|
|
661
856
|
try {
|
|
662
|
-
current = await
|
|
857
|
+
current = await readFile3(path, "utf8");
|
|
663
858
|
} catch {
|
|
664
859
|
}
|
|
665
|
-
if (current
|
|
860
|
+
if (!isUpToDate(current, file.text)) {
|
|
666
861
|
throw new ExportUsageError(`${shown} is out of date with ${label}${current === null ? " (it doesn't exist)" : ""}. Run "stoaken export" and commit the result.`);
|
|
667
862
|
}
|
|
668
863
|
say(`\u2714 ${shown} is up to date with ${label}`);
|
|
669
864
|
return;
|
|
670
865
|
}
|
|
671
|
-
await
|
|
672
|
-
await
|
|
866
|
+
await mkdir3(dirname2(path), { recursive: true });
|
|
867
|
+
await writeFile3(path, file.text, "utf8");
|
|
673
868
|
say(`\u2714 ${label} \u2192 ${shown} (${target.format}, ${formatBytes(Buffer.byteLength(file.text))})${warnings}`);
|
|
674
869
|
}
|
|
675
870
|
function describe(error, themeName, usingEnvToken) {
|
|
@@ -684,9 +879,16 @@ function flagValue(args, name) {
|
|
|
684
879
|
return index === -1 ? void 0 : args[index + 1];
|
|
685
880
|
}
|
|
686
881
|
switch (command) {
|
|
687
|
-
case "init":
|
|
688
|
-
|
|
882
|
+
case "init": {
|
|
883
|
+
const client = flagValue(rest, "client");
|
|
884
|
+
if (client !== void 0 && !isClient(client)) {
|
|
885
|
+
p4.log.error(`Unknown --client "${client}". Use one of: ${CLIENTS.join(", ")}`);
|
|
886
|
+
process.exitCode = 1;
|
|
887
|
+
break;
|
|
888
|
+
}
|
|
889
|
+
await runInit({ server: flagValue(rest, "server"), client });
|
|
689
890
|
break;
|
|
891
|
+
}
|
|
690
892
|
case "export":
|
|
691
893
|
process.exitCode = await runExport(rest);
|
|
692
894
|
break;
|
|
@@ -698,7 +900,9 @@ switch (command) {
|
|
|
698
900
|
"stoaken -- Stoaken CLI",
|
|
699
901
|
"",
|
|
700
902
|
"Usage:",
|
|
701
|
-
" stoaken init [--server <url>]
|
|
903
|
+
" stoaken init [--server <url>] [--client <name>]",
|
|
904
|
+
" Authenticate, pick a Brand, and connect an AI tool",
|
|
905
|
+
` (${CLIENTS.join(", ")})`,
|
|
702
906
|
" stoaken export [options] Write your tokens as code (stoaken export --help)"
|
|
703
907
|
].join("\n")
|
|
704
908
|
);
|
package/package.json
CHANGED
|
@@ -1,51 +1,51 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "stoaken",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "Stoaken CLI -- set up a project for MCP/API access, and export your design tokens as code.",
|
|
5
|
-
"license": "UNLICENSED",
|
|
6
|
-
"type": "module",
|
|
7
|
-
"bin": {
|
|
8
|
-
"stoaken": "dist/index.js"
|
|
9
|
-
},
|
|
10
|
-
"files": [
|
|
11
|
-
"dist"
|
|
12
|
-
],
|
|
13
|
-
"repository": {
|
|
14
|
-
"type": "git",
|
|
15
|
-
"url": "git+https://github.com/craig-astravale/stoaken.git",
|
|
16
|
-
"directory": "cli"
|
|
17
|
-
},
|
|
18
|
-
"bugs": {
|
|
19
|
-
"url": "https://github.com/craig-astravale/stoaken/issues"
|
|
20
|
-
},
|
|
21
|
-
"homepage": "https://github.com/craig-astravale/stoaken/tree/main/cli#readme",
|
|
22
|
-
"keywords": [
|
|
23
|
-
"design-tokens",
|
|
24
|
-
"dtcg",
|
|
25
|
-
"mcp",
|
|
26
|
-
"model-context-protocol",
|
|
27
|
-
"cli",
|
|
28
|
-
"design-system"
|
|
29
|
-
],
|
|
30
|
-
"author": "Stoaken",
|
|
31
|
-
"engines": {
|
|
32
|
-
"node": ">=
|
|
33
|
-
},
|
|
34
|
-
"scripts": {
|
|
35
|
-
"build": "tsup src/index.ts --format esm --clean --shims",
|
|
36
|
-
"dev": "tsx src/index.ts",
|
|
37
|
-
"typecheck": "tsc --noEmit",
|
|
38
|
-
"prepublishOnly": "npm run build"
|
|
39
|
-
},
|
|
40
|
-
"dependencies": {
|
|
41
|
-
"@clack/prompts": "^1.8.0",
|
|
42
|
-
"
|
|
43
|
-
"
|
|
44
|
-
},
|
|
45
|
-
"devDependencies": {
|
|
46
|
-
"@types/node": "^26.5.0",
|
|
47
|
-
"tsup": "^8.5.1",
|
|
48
|
-
"tsx": "^4.23.13",
|
|
49
|
-
"typescript": "^7.0.2"
|
|
50
|
-
}
|
|
51
|
-
}
|
|
1
|
+
{
|
|
2
|
+
"name": "stoaken",
|
|
3
|
+
"version": "0.4.0",
|
|
4
|
+
"description": "Stoaken CLI -- set up a project for MCP/API access, and export your design tokens as code.",
|
|
5
|
+
"license": "UNLICENSED",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"bin": {
|
|
8
|
+
"stoaken": "dist/index.js"
|
|
9
|
+
},
|
|
10
|
+
"files": [
|
|
11
|
+
"dist"
|
|
12
|
+
],
|
|
13
|
+
"repository": {
|
|
14
|
+
"type": "git",
|
|
15
|
+
"url": "git+https://github.com/craig-astravale/stoaken.git",
|
|
16
|
+
"directory": "cli"
|
|
17
|
+
},
|
|
18
|
+
"bugs": {
|
|
19
|
+
"url": "https://github.com/craig-astravale/stoaken/issues"
|
|
20
|
+
},
|
|
21
|
+
"homepage": "https://github.com/craig-astravale/stoaken/tree/main/cli#readme",
|
|
22
|
+
"keywords": [
|
|
23
|
+
"design-tokens",
|
|
24
|
+
"dtcg",
|
|
25
|
+
"mcp",
|
|
26
|
+
"model-context-protocol",
|
|
27
|
+
"cli",
|
|
28
|
+
"design-system"
|
|
29
|
+
],
|
|
30
|
+
"author": "Stoaken",
|
|
31
|
+
"engines": {
|
|
32
|
+
"node": ">=20"
|
|
33
|
+
},
|
|
34
|
+
"scripts": {
|
|
35
|
+
"build": "tsup src/index.ts --format esm --clean --shims",
|
|
36
|
+
"dev": "tsx src/index.ts",
|
|
37
|
+
"typecheck": "tsc --noEmit",
|
|
38
|
+
"prepublishOnly": "npm run build"
|
|
39
|
+
},
|
|
40
|
+
"dependencies": {
|
|
41
|
+
"@clack/prompts": "^1.8.0",
|
|
42
|
+
"open": "^11.0.2",
|
|
43
|
+
"@modelcontextprotocol/client": "^2.4.0"
|
|
44
|
+
},
|
|
45
|
+
"devDependencies": {
|
|
46
|
+
"@types/node": "^26.5.0",
|
|
47
|
+
"tsup": "^8.5.1",
|
|
48
|
+
"tsx": "^4.23.13",
|
|
49
|
+
"typescript": "^7.0.2"
|
|
50
|
+
}
|
|
51
|
+
}
|