stoaken 0.1.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/LICENSE +17 -0
- package/README.md +39 -0
- package/dist/index.js +375 -0
- package/package.json +51 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
Copyright (c) 2026 Stoaken. All rights reserved.
|
|
2
|
+
|
|
3
|
+
This software and associated documentation are proprietary and confidential.
|
|
4
|
+
No license, express or implied, to any intellectual property rights is
|
|
5
|
+
granted by this document, except that you may install and run this software
|
|
6
|
+
as distributed for its intended purpose. Unauthorized copying, modification,
|
|
7
|
+
distribution, sublicensing, or reverse engineering of this software, in
|
|
8
|
+
whole or in part, is strictly prohibited without prior written permission
|
|
9
|
+
from Stoaken.
|
|
10
|
+
|
|
11
|
+
THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
12
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
13
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
14
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
15
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
|
16
|
+
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
|
17
|
+
DEALINGS IN THE SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# stoaken
|
|
2
|
+
|
|
3
|
+
The official CLI for [Stoaken](https://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. Generate `.stoaken/SKILL.md` -- a short brief for AI coding agents (Claude Code, Cursor) explaining how to connect to this project's Stoaken MCP server and what its tools do.
|
|
17
|
+
|
|
18
|
+
Run `stoaken init` again in an already-configured directory to continue with the same Brand, switch to a different one, or re-authenticate.
|
|
19
|
+
|
|
20
|
+
## Authentication
|
|
21
|
+
|
|
22
|
+
Two paths, both landing on the same bearer credential:
|
|
23
|
+
|
|
24
|
+
- **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.
|
|
25
|
+
- **Paste an API key** -- for CI, scripts, or anywhere a browser isn't available. Create one at `/settings/api-keys` in the Stoaken app.
|
|
26
|
+
|
|
27
|
+
## What this connects to
|
|
28
|
+
|
|
29
|
+
Once set up, any MCP-capable client (Claude Code, Cursor, Claude Desktop) or a direct HTTP client can use the credential in `.stoaken/credentials.json` as a Bearer token against Stoaken's MCP server, which exposes four tools:
|
|
30
|
+
|
|
31
|
+
- `list_brands` -- every Brand you hold at least Viewer on.
|
|
32
|
+
- `get_theme_tokens(themeId)` -- a Theme's cascade-merged token tree.
|
|
33
|
+
- `list_releases(themeId)` -- a Theme's Releases with version, status, and change summary.
|
|
34
|
+
- `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.
|
|
35
|
+
|
|
36
|
+
## Links
|
|
37
|
+
|
|
38
|
+
- [Stoaken on GitHub](https://github.com/craig-astravale/stoaken)
|
|
39
|
+
- [Report an issue](https://github.com/craig-astravale/stoaken/issues)
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,375 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/index.ts
|
|
4
|
+
import * as p3 from "@clack/prompts";
|
|
5
|
+
|
|
6
|
+
// src/commands/init.ts
|
|
7
|
+
import * as p2 from "@clack/prompts";
|
|
8
|
+
|
|
9
|
+
// src/auth.ts
|
|
10
|
+
import { createHash, randomBytes } from "crypto";
|
|
11
|
+
import { createServer } from "http";
|
|
12
|
+
import * as p from "@clack/prompts";
|
|
13
|
+
import open from "open";
|
|
14
|
+
|
|
15
|
+
// src/config.ts
|
|
16
|
+
import { existsSync } from "fs";
|
|
17
|
+
import { appendFile, mkdir, readFile, writeFile } from "fs/promises";
|
|
18
|
+
import { join } from "path";
|
|
19
|
+
var CONFIG_FILE = "stoaken.config.json";
|
|
20
|
+
var STOAKEN_DIR = ".stoaken";
|
|
21
|
+
var CREDENTIALS_FILE = join(STOAKEN_DIR, "credentials.json");
|
|
22
|
+
var OAUTH_CLIENT_FILE = join(STOAKEN_DIR, "oauth-client.json");
|
|
23
|
+
function configPath(cwd) {
|
|
24
|
+
return join(cwd, CONFIG_FILE);
|
|
25
|
+
}
|
|
26
|
+
async function readConfig(cwd) {
|
|
27
|
+
try {
|
|
28
|
+
const raw = await readFile(configPath(cwd), "utf8");
|
|
29
|
+
return JSON.parse(raw);
|
|
30
|
+
} catch {
|
|
31
|
+
return null;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
async function writeConfig(cwd, config) {
|
|
35
|
+
await writeFile(configPath(cwd), `${JSON.stringify(config, null, 2)}
|
|
36
|
+
`, "utf8");
|
|
37
|
+
}
|
|
38
|
+
async function ensureStoakenDir(cwd) {
|
|
39
|
+
await mkdir(join(cwd, STOAKEN_DIR), { recursive: true });
|
|
40
|
+
}
|
|
41
|
+
async function writeCredentials(cwd, credentials) {
|
|
42
|
+
await ensureStoakenDir(cwd);
|
|
43
|
+
const path = join(cwd, CREDENTIALS_FILE);
|
|
44
|
+
await writeFile(path, `${JSON.stringify(credentials, null, 2)}
|
|
45
|
+
`, { encoding: "utf8", mode: 384 });
|
|
46
|
+
}
|
|
47
|
+
async function readOAuthClient(cwd) {
|
|
48
|
+
try {
|
|
49
|
+
const raw = await readFile(join(cwd, OAUTH_CLIENT_FILE), "utf8");
|
|
50
|
+
return JSON.parse(raw);
|
|
51
|
+
} catch {
|
|
52
|
+
return null;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
async function writeOAuthClient(cwd, client) {
|
|
56
|
+
await ensureStoakenDir(cwd);
|
|
57
|
+
await writeFile(join(cwd, OAUTH_CLIENT_FILE), `${JSON.stringify(client, null, 2)}
|
|
58
|
+
`, "utf8");
|
|
59
|
+
}
|
|
60
|
+
async function writeSkillFile(cwd, content) {
|
|
61
|
+
await ensureStoakenDir(cwd);
|
|
62
|
+
await writeFile(join(cwd, STOAKEN_DIR, "SKILL.md"), content, "utf8");
|
|
63
|
+
}
|
|
64
|
+
async function ensureGitignored(cwd) {
|
|
65
|
+
const gitignorePath = join(cwd, ".gitignore");
|
|
66
|
+
const entry = `${STOAKEN_DIR}/`;
|
|
67
|
+
let existing = "";
|
|
68
|
+
if (existsSync(gitignorePath)) {
|
|
69
|
+
existing = await readFile(gitignorePath, "utf8");
|
|
70
|
+
}
|
|
71
|
+
const alreadyPresent = existing.split("\n").map((line) => line.trim()).includes(entry);
|
|
72
|
+
if (alreadyPresent) {
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
const needsLeadingNewline = existing.length > 0 && !existing.endsWith("\n");
|
|
76
|
+
const prefix = needsLeadingNewline ? "\n" : "";
|
|
77
|
+
await appendFile(gitignorePath, `${prefix}${entry}
|
|
78
|
+
`, "utf8");
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// src/auth.ts
|
|
82
|
+
var CALLBACK_PORT = 51820;
|
|
83
|
+
var REDIRECT_URI = `http://127.0.0.1:${CALLBACK_PORT}/callback`;
|
|
84
|
+
async function discoverAuthorizationServer(serverUrl) {
|
|
85
|
+
const metadataUrl = new URL("/.well-known/oauth-protected-resource", serverUrl);
|
|
86
|
+
const response = await fetch(metadataUrl);
|
|
87
|
+
if (!response.ok) {
|
|
88
|
+
throw new Error(`Failed to discover the authorization server (${response.status}) at ${metadataUrl}`);
|
|
89
|
+
}
|
|
90
|
+
const metadata = await response.json();
|
|
91
|
+
const authServer = metadata.authorization_servers?.[0];
|
|
92
|
+
if (!authServer) {
|
|
93
|
+
throw new Error(`${metadataUrl} did not name an authorization server`);
|
|
94
|
+
}
|
|
95
|
+
return authServer;
|
|
96
|
+
}
|
|
97
|
+
async function registerOAuthClient(authServer) {
|
|
98
|
+
const response = await fetch(`${authServer}/oauth2/register`, {
|
|
99
|
+
method: "POST",
|
|
100
|
+
headers: { "Content-Type": "application/json" },
|
|
101
|
+
body: JSON.stringify({
|
|
102
|
+
client_name: "Stoaken CLI",
|
|
103
|
+
redirect_uris: [REDIRECT_URI],
|
|
104
|
+
grant_types: ["authorization_code"],
|
|
105
|
+
response_types: ["code"],
|
|
106
|
+
token_endpoint_auth_method: "none"
|
|
107
|
+
})
|
|
108
|
+
});
|
|
109
|
+
if (!response.ok) {
|
|
110
|
+
throw new Error(`Dynamic Client Registration failed (${response.status}): ${await response.text()}`);
|
|
111
|
+
}
|
|
112
|
+
const data = await response.json();
|
|
113
|
+
return data.client_id;
|
|
114
|
+
}
|
|
115
|
+
function base64url(buffer) {
|
|
116
|
+
return buffer.toString("base64url");
|
|
117
|
+
}
|
|
118
|
+
function generatePkce() {
|
|
119
|
+
const verifier = base64url(randomBytes(32));
|
|
120
|
+
const challenge = base64url(createHash("sha256").update(verifier).digest());
|
|
121
|
+
return { verifier, challenge };
|
|
122
|
+
}
|
|
123
|
+
function waitForCallback() {
|
|
124
|
+
return new Promise((resolve, reject) => {
|
|
125
|
+
const server = createServer((req, res) => {
|
|
126
|
+
const url = new URL(req.url ?? "/", REDIRECT_URI);
|
|
127
|
+
if (url.pathname !== "/callback") {
|
|
128
|
+
res.writeHead(404).end();
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
const code = url.searchParams.get("code");
|
|
132
|
+
const error = url.searchParams.get("error");
|
|
133
|
+
res.writeHead(200, { "Content-Type": "text/html" });
|
|
134
|
+
res.end(
|
|
135
|
+
error ? "<p>Login failed. You can close this tab and return to the terminal.</p>" : "<p>Login complete -- you can close this tab and return to the terminal.</p>"
|
|
136
|
+
);
|
|
137
|
+
server.close();
|
|
138
|
+
if (error) {
|
|
139
|
+
reject(new Error(`WorkOS returned an error: ${error}`));
|
|
140
|
+
} else if (!code) {
|
|
141
|
+
reject(new Error("No authorization code in the callback"));
|
|
142
|
+
} else {
|
|
143
|
+
resolve(code);
|
|
144
|
+
}
|
|
145
|
+
});
|
|
146
|
+
server.on("error", reject);
|
|
147
|
+
server.listen(CALLBACK_PORT, "127.0.0.1");
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
async function exchangeCodeForToken(authServer, clientId, code, verifier) {
|
|
151
|
+
const response = await fetch(`${authServer}/oauth2/token`, {
|
|
152
|
+
method: "POST",
|
|
153
|
+
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
154
|
+
body: new URLSearchParams({
|
|
155
|
+
grant_type: "authorization_code",
|
|
156
|
+
code,
|
|
157
|
+
redirect_uri: REDIRECT_URI,
|
|
158
|
+
client_id: clientId,
|
|
159
|
+
code_verifier: verifier
|
|
160
|
+
})
|
|
161
|
+
});
|
|
162
|
+
if (!response.ok) {
|
|
163
|
+
throw new Error(`Token exchange failed (${response.status}): ${await response.text()}`);
|
|
164
|
+
}
|
|
165
|
+
const data = await response.json();
|
|
166
|
+
return data.access_token;
|
|
167
|
+
}
|
|
168
|
+
async function loginWithBrowser(cwd, serverUrl) {
|
|
169
|
+
const authServer = await discoverAuthorizationServer(serverUrl);
|
|
170
|
+
let clientId = (await readOAuthClient(cwd))?.clientId;
|
|
171
|
+
if (!clientId) {
|
|
172
|
+
clientId = await registerOAuthClient(authServer);
|
|
173
|
+
await writeOAuthClient(cwd, { clientId });
|
|
174
|
+
}
|
|
175
|
+
const { verifier, challenge } = generatePkce();
|
|
176
|
+
const authorizeUrl = new URL("/oauth2/authorize", authServer);
|
|
177
|
+
authorizeUrl.searchParams.set("client_id", clientId);
|
|
178
|
+
authorizeUrl.searchParams.set("redirect_uri", REDIRECT_URI);
|
|
179
|
+
authorizeUrl.searchParams.set("response_type", "code");
|
|
180
|
+
authorizeUrl.searchParams.set("code_challenge", challenge);
|
|
181
|
+
authorizeUrl.searchParams.set("code_challenge_method", "S256");
|
|
182
|
+
const callbackPromise = waitForCallback();
|
|
183
|
+
await open(authorizeUrl.toString());
|
|
184
|
+
p.log.info(`Opened your browser to sign in. Waiting for you to finish...`);
|
|
185
|
+
const code = await callbackPromise;
|
|
186
|
+
return exchangeCodeForToken(authServer, clientId, code, verifier);
|
|
187
|
+
}
|
|
188
|
+
async function promptForApiKey() {
|
|
189
|
+
const key = await p.password({
|
|
190
|
+
message: "Paste your Stoaken API key (from /settings/api-keys):",
|
|
191
|
+
validate: (value) => {
|
|
192
|
+
if (!value?.startsWith("twk_")) {
|
|
193
|
+
return "That doesn't look like a Stoaken API key (should start with twk_)";
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
});
|
|
197
|
+
if (p.isCancel(key)) {
|
|
198
|
+
return null;
|
|
199
|
+
}
|
|
200
|
+
return key;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// src/mcp-client.ts
|
|
204
|
+
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
|
|
205
|
+
import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
|
|
206
|
+
async function listBrands(serverUrl, credential) {
|
|
207
|
+
const transport = new StreamableHTTPClientTransport(new URL("/mcp", serverUrl), {
|
|
208
|
+
requestInit: { headers: { Authorization: `Bearer ${credential}` } }
|
|
209
|
+
});
|
|
210
|
+
const client = new Client({ name: "stoaken-cli", version: "0.1.0" });
|
|
211
|
+
await client.connect(transport);
|
|
212
|
+
try {
|
|
213
|
+
const result = await client.callTool({ name: "list_brands", arguments: {} });
|
|
214
|
+
const firstContent = result.content[0];
|
|
215
|
+
if (!firstContent || firstContent.type !== "text" || !firstContent.text) {
|
|
216
|
+
throw new Error("list_brands returned an unexpected response shape");
|
|
217
|
+
}
|
|
218
|
+
return JSON.parse(firstContent.text);
|
|
219
|
+
} finally {
|
|
220
|
+
await client.close();
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// src/skill-template.ts
|
|
225
|
+
function generateSkillFile(config) {
|
|
226
|
+
return `# Working with Stoaken
|
|
227
|
+
|
|
228
|
+
This project is connected to the **${config.brandName}** Brand on Stoaken (\`${config.serverUrl}\`).
|
|
229
|
+
|
|
230
|
+
Stoaken's MCP server exposes four tools. Connect to it at \`${config.serverUrl}/mcp\` using
|
|
231
|
+
the credential in \`.stoaken/credentials.json\` as a Bearer token (never commit that file --
|
|
232
|
+
it's already in \`.gitignore\`).
|
|
233
|
+
|
|
234
|
+
## Tools
|
|
235
|
+
|
|
236
|
+
- **list_brands** -- every Brand you hold at least Viewer on.
|
|
237
|
+
- **get_theme_tokens(themeId)** -- a Theme's cascade-merged token tree.
|
|
238
|
+
- **list_releases(themeId)** -- a Theme's Releases with version, status, and change summary.
|
|
239
|
+
- **propose_token_change(themeId, tokenSetId, document, version, versionType)** -- updates a Token
|
|
240
|
+
Set's document and submits a new Release for approval. **This never publishes.** The Release
|
|
241
|
+
lands in \`pending_approval\` and a human reviewer must approve it through the Stoaken
|
|
242
|
+
Command Center before it goes live -- exactly the same gate a person editing through the UI
|
|
243
|
+
goes through. There is no tool that publishes a Release; propose changes, then ask a human to
|
|
244
|
+
review them.
|
|
245
|
+
|
|
246
|
+
## This Brand
|
|
247
|
+
|
|
248
|
+
- Brand ID: \`${config.brandId}\`
|
|
249
|
+
- Brand name: ${config.brandName}
|
|
250
|
+
- Server: ${config.serverUrl}
|
|
251
|
+
|
|
252
|
+
Re-run \`stoaken init\` if you need to switch Brands or re-authenticate.
|
|
253
|
+
`;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
// src/commands/init.ts
|
|
257
|
+
var DEFAULT_SERVER_URL = "https://stoaken.com";
|
|
258
|
+
async function runInit(options) {
|
|
259
|
+
const cwd = process.cwd();
|
|
260
|
+
const serverUrl = options.server ?? DEFAULT_SERVER_URL;
|
|
261
|
+
p2.intro("stoaken init");
|
|
262
|
+
const existing = await readConfig(cwd);
|
|
263
|
+
let skipToWrite = null;
|
|
264
|
+
if (existing) {
|
|
265
|
+
const choice = await p2.select({
|
|
266
|
+
message: `This project is already set up for ${existing.brandName}. What would you like to do?`,
|
|
267
|
+
options: [
|
|
268
|
+
{ value: "continue", label: `Continue with ${existing.brandName}` },
|
|
269
|
+
{ value: "switch", label: "Switch Brand" },
|
|
270
|
+
{ value: "reauth", label: "Re-authenticate" }
|
|
271
|
+
]
|
|
272
|
+
});
|
|
273
|
+
if (p2.isCancel(choice)) {
|
|
274
|
+
p2.cancel("Cancelled.");
|
|
275
|
+
return;
|
|
276
|
+
}
|
|
277
|
+
if (choice === "continue") {
|
|
278
|
+
skipToWrite = { brandId: existing.brandId, brandName: existing.brandName };
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
let credential;
|
|
282
|
+
let credentialKind;
|
|
283
|
+
if (!skipToWrite) {
|
|
284
|
+
const authChoice = await p2.select({
|
|
285
|
+
message: "How would you like to sign in?",
|
|
286
|
+
options: [
|
|
287
|
+
{ value: "browser", label: "Log in with browser" },
|
|
288
|
+
{ value: "key", label: "Paste an API key" }
|
|
289
|
+
]
|
|
290
|
+
});
|
|
291
|
+
if (p2.isCancel(authChoice)) {
|
|
292
|
+
p2.cancel("Cancelled.");
|
|
293
|
+
return;
|
|
294
|
+
}
|
|
295
|
+
if (authChoice === "browser") {
|
|
296
|
+
const s2 = p2.spinner();
|
|
297
|
+
s2.start("Waiting for you to finish signing in...");
|
|
298
|
+
try {
|
|
299
|
+
credential = await loginWithBrowser(cwd, serverUrl);
|
|
300
|
+
credentialKind = "oauth";
|
|
301
|
+
s2.stop("Signed in.");
|
|
302
|
+
} catch (error) {
|
|
303
|
+
s2.stop("Sign-in failed.");
|
|
304
|
+
p2.log.error(error instanceof Error ? error.message : String(error));
|
|
305
|
+
return;
|
|
306
|
+
}
|
|
307
|
+
} else {
|
|
308
|
+
const key = await promptForApiKey();
|
|
309
|
+
if (!key) {
|
|
310
|
+
p2.cancel("Cancelled.");
|
|
311
|
+
return;
|
|
312
|
+
}
|
|
313
|
+
credential = key;
|
|
314
|
+
credentialKind = "api_key";
|
|
315
|
+
}
|
|
316
|
+
const s = p2.spinner();
|
|
317
|
+
s.start("Fetching your Brands...");
|
|
318
|
+
let brands;
|
|
319
|
+
try {
|
|
320
|
+
brands = await listBrands(serverUrl, credential);
|
|
321
|
+
} catch (error) {
|
|
322
|
+
s.stop("Failed to reach Stoaken.");
|
|
323
|
+
p2.log.error(error instanceof Error ? error.message : String(error));
|
|
324
|
+
return;
|
|
325
|
+
}
|
|
326
|
+
s.stop(`Found ${brands.length} Brand${brands.length === 1 ? "" : "s"}.`);
|
|
327
|
+
if (brands.length === 0) {
|
|
328
|
+
p2.outro("No Brands available to this account. Ask an editor to invite you, then run init again.");
|
|
329
|
+
return;
|
|
330
|
+
}
|
|
331
|
+
const brandChoice = await p2.select({
|
|
332
|
+
message: "Which Brand is this project for?",
|
|
333
|
+
options: brands.map((brand2) => ({ value: brand2.id, label: brand2.name }))
|
|
334
|
+
});
|
|
335
|
+
if (p2.isCancel(brandChoice)) {
|
|
336
|
+
p2.cancel("Cancelled.");
|
|
337
|
+
return;
|
|
338
|
+
}
|
|
339
|
+
const brand = brands.find((b) => b.id === brandChoice);
|
|
340
|
+
await writeCredentials(cwd, { kind: credentialKind, token: credential });
|
|
341
|
+
skipToWrite = { brandId: brand.id, brandName: brand.name };
|
|
342
|
+
}
|
|
343
|
+
const config = { brandId: skipToWrite.brandId, brandName: skipToWrite.brandName, serverUrl };
|
|
344
|
+
await writeConfig(cwd, config);
|
|
345
|
+
await ensureGitignored(cwd);
|
|
346
|
+
await writeSkillFile(cwd, generateSkillFile(config));
|
|
347
|
+
p2.outro(`Done -- this project is set up for ${config.brandName}.`);
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
// src/index.ts
|
|
351
|
+
var [command, ...rest] = process.argv.slice(2);
|
|
352
|
+
function flagValue(args, name) {
|
|
353
|
+
const index = args.indexOf(`--${name}`);
|
|
354
|
+
return index === -1 ? void 0 : args[index + 1];
|
|
355
|
+
}
|
|
356
|
+
switch (command) {
|
|
357
|
+
case "init":
|
|
358
|
+
await runInit({ server: flagValue(rest, "server") });
|
|
359
|
+
break;
|
|
360
|
+
case void 0:
|
|
361
|
+
case "--help":
|
|
362
|
+
case "-h":
|
|
363
|
+
p3.log.message(
|
|
364
|
+
[
|
|
365
|
+
"stoaken -- Stoaken CLI",
|
|
366
|
+
"",
|
|
367
|
+
"Usage:",
|
|
368
|
+
" stoaken init [--server <url>] Authenticate, pick a Brand, and set up this project"
|
|
369
|
+
].join("\n")
|
|
370
|
+
);
|
|
371
|
+
break;
|
|
372
|
+
default:
|
|
373
|
+
p3.log.error(`Unknown command: ${command}`);
|
|
374
|
+
process.exitCode = 1;
|
|
375
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "stoaken",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Stoaken CLI -- authenticate, pick a Brand, and get set up for MCP/API access in one command.",
|
|
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": ">=18"
|
|
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
|
+
"@modelcontextprotocol/sdk": "^1.30.0",
|
|
43
|
+
"open": "^11.0.2"
|
|
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
|
+
}
|