@keystone-os/cli 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/README.md +15 -0
  2. package/dist/index.js +1316 -19
  3. package/package.json +19 -7
package/dist/index.js CHANGED
@@ -1,5 +1,4 @@
1
1
  #!/usr/bin/env node
2
- #!/usr/bin/env node
3
2
  "use strict";
4
3
  var __create = Object.create;
5
4
  var __defProp = Object.defineProperty;
@@ -30,22 +29,114 @@ var import_commander = require("commander");
30
29
  // src/commands/init.ts
31
30
  var fs = __toESM(require("fs"));
32
31
  var path = __toESM(require("path"));
33
- var STARTER_APP = `import { useVault, useFetch } from '@keystone-os/sdk';
32
+ var STARTER_APP = `import React, { useState } from 'react';
33
+ import {
34
+ useVault,
35
+ useJupiterSwap,
36
+ useImpactReport,
37
+ useTurnkey,
38
+ } from '@keystone-os/sdk';
34
39
 
35
40
  export default function App() {
36
- const { tokens, balances } = useVault();
41
+ const { tokens, activeVault } = useVault();
42
+ const { getQuote, swap, loading: swapLoading, error: swapError } = useJupiterSwap();
43
+ const { simulate, report } = useImpactReport();
44
+ const { signTransaction } = useTurnkey();
45
+ const [quote, setQuote] = useState(null);
46
+ const [amount, setAmount] = useState('1000000');
47
+
48
+ const handleGetQuote = async () => {
49
+ const q = await getQuote({
50
+ inputMint: 'So11111111111111111111111111111111111111112',
51
+ outputMint: 'EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v',
52
+ amount,
53
+ slippageBps: 50,
54
+ });
55
+ setQuote(q);
56
+ };
37
57
 
38
58
  return (
39
- <div className="p-6 bg-zinc-900 text-white min-h-screen">
40
- <h1 className="text-2xl font-bold text-emerald-400 mb-4">My Mini-App</h1>
41
- <div className="space-y-2 font-mono">
59
+ <div className="min-h-screen bg-zinc-950 text-white p-8">
60
+ {/* \u2500\u2500\u2500 Header \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */}
61
+ <div className="mb-8">
62
+ <h1 className="text-3xl font-bold bg-gradient-to-r from-emerald-400 to-cyan-400 bg-clip-text text-transparent">
63
+ Sovereign OS Mini-App
64
+ </h1>
65
+ <p className="text-zinc-500 mt-1 text-sm">
66
+ Vault: <span className="text-emerald-400">{activeVault}</span>
67
+ </p>
68
+ </div>
69
+
70
+ {/* \u2500\u2500\u2500 Token Balances \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */}
71
+ <div className="grid grid-cols-1 sm:grid-cols-2 gap-4 mb-8">
42
72
  {tokens.map((t) => (
43
- <div key={t.symbol} className="flex justify-between border-b border-zinc-800 py-2">
44
- <span>{t.symbol}</span>
45
- <span>{t.balance.toLocaleString()}</span>
73
+ <div
74
+ key={t.symbol}
75
+ className="bg-zinc-900 border border-zinc-800 rounded-xl p-4 flex items-center justify-between"
76
+ >
77
+ <div>
78
+ <p className="text-lg font-semibold">{t.symbol}</p>
79
+ <p className="text-xs text-zinc-500">{t.name}</p>
80
+ </div>
81
+ <div className="text-right">
82
+ <p className="text-lg font-mono">{t.balance.toLocaleString()}</p>
83
+ <p className="text-xs text-emerald-400">
84
+ \${(t.balance * t.price).toLocaleString(undefined, { maximumFractionDigits: 2 })}
85
+ </p>
86
+ </div>
46
87
  </div>
47
88
  ))}
48
89
  </div>
90
+
91
+ {/* \u2500\u2500\u2500 Jupiter Swap \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */}
92
+ <div className="bg-zinc-900 border border-zinc-800 rounded-xl p-6 mb-6">
93
+ <h2 className="text-lg font-bold text-cyan-400 mb-4">\u26A1 Jupiter Swap</h2>
94
+ <div className="flex gap-3 items-end">
95
+ <div className="flex-1">
96
+ <label className="text-xs text-zinc-500 block mb-1">Amount (lamports)</label>
97
+ <input
98
+ value={amount}
99
+ onChange={(e) => setAmount(e.target.value)}
100
+ className="w-full bg-zinc-800 rounded-lg px-3 py-2 text-sm border border-zinc-700 focus:border-emerald-500 outline-none"
101
+ />
102
+ </div>
103
+ <button
104
+ onClick={handleGetQuote}
105
+ disabled={swapLoading}
106
+ className="px-4 py-2 bg-emerald-600 hover:bg-emerald-500 rounded-lg text-sm font-bold transition-colors disabled:opacity-50"
107
+ >
108
+ {swapLoading ? 'Loading...' : 'Get Quote'}
109
+ </button>
110
+ </div>
111
+
112
+ {quote && (
113
+ <div className="mt-4 bg-zinc-800/50 rounded-lg p-4 text-sm font-mono">
114
+ <p>Input: {quote.inAmount} \u2192 Output: {quote.outAmount}</p>
115
+ <p className="text-zinc-500">Price Impact: {quote.priceImpactPct}%</p>
116
+ </div>
117
+ )}
118
+
119
+ {swapError && (
120
+ <p className="mt-3 text-red-400 text-sm">{swapError}</p>
121
+ )}
122
+ </div>
123
+
124
+ {/* \u2500\u2500\u2500 Impact Report \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */}
125
+ {report && (
126
+ <div className="bg-zinc-900 border border-zinc-800 rounded-xl p-6">
127
+ <h2 className="text-lg font-bold text-amber-400 mb-3">\u{1F4CA} Impact Report</h2>
128
+ <div className="space-y-2 text-sm">
129
+ {report.diff.map((d) => (
130
+ <div key={d.symbol} className="flex justify-between">
131
+ <span>{d.symbol}</span>
132
+ <span className={d.delta >= 0 ? 'text-emerald-400' : 'text-red-400'}>
133
+ {d.delta >= 0 ? '+' : ''}{d.delta.toFixed(4)} ({d.percentChange.toFixed(2)}%)
134
+ </span>
135
+ </div>
136
+ ))}
137
+ </div>
138
+ </div>
139
+ )}
49
140
  </div>
50
141
  );
51
142
  }
@@ -75,23 +166,69 @@ function runInit(dir) {
75
166
  if (fs.existsSync(targetDir) && fs.readdirSync(targetDir).length > 0) {
76
167
  throw new Error(`Directory ${targetDir} is not empty.`);
77
168
  }
169
+ const appName = path.basename(targetDir) || "my-keystone-app";
78
170
  fs.mkdirSync(targetDir, { recursive: true });
79
171
  fs.writeFileSync(path.join(targetDir, "App.tsx"), STARTER_APP);
80
172
  fs.writeFileSync(
81
173
  path.join(targetDir, "keystone.lock.json"),
82
174
  JSON.stringify(LOCKFILE, null, 2)
83
175
  );
176
+ const config = {
177
+ name: appName,
178
+ description: `A Keystone mini-app`,
179
+ wallet: "",
180
+ cluster: "devnet",
181
+ category: "utility",
182
+ provider: "cloudflare"
183
+ };
184
+ fs.writeFileSync(
185
+ path.join(targetDir, "keystone.config.json"),
186
+ JSON.stringify(config, null, 2) + "\n"
187
+ );
84
188
  fs.writeFileSync(
85
189
  path.join(targetDir, "README.md"),
86
- `# Keystone Mini-App
190
+ `# ${appName}
191
+
192
+ Built with \`@keystone-os/sdk\` \u2014 Keystone Sovereign OS.
193
+
194
+ ## Quick Start
195
+ \`\`\`bash
196
+ # Preview locally
197
+ keystone dev
87
198
 
88
- Built with \`@keystone-os/sdk\`. Open in Keystone Studio to run.
199
+ # Ship to marketplace (ONE command)
200
+ keystone ship
201
+ \`\`\`
202
+
203
+ ## SDK Hooks Available
204
+ - **useVault** \u2014 Token balances
205
+ - **usePortfolio** \u2014 Portfolio data with USD values
206
+ - **useJupiterSwap** \u2014 Token swaps via Jupiter
207
+ - **useTheme** \u2014 Dark/light mode
208
+ - **useTokenPrice** \u2014 Live token prices
209
+ - **useNotification** \u2014 In-app notifications
210
+ - **useStorage** \u2014 Persistent key-value storage
211
+ - **useImpactReport** \u2014 Transaction simulation
212
+ - **useTurnkey** \u2014 Institutional signing
213
+
214
+ ## Configuration
215
+ Edit \`keystone.config.json\` to set your wallet, app name, and cluster.
89
216
  `
90
217
  );
91
- console.log(`Created Mini-App in ${targetDir}`);
92
- console.log(" - App.tsx");
93
- console.log(" - keystone.lock.json");
94
- console.log(" - README.md");
218
+ console.log(`
219
+ Created Keystone Mini-App in ${targetDir}
220
+ `);
221
+ console.log(" App.tsx \u2014 Starter app with Vault + Jupiter Swap");
222
+ console.log(" keystone.config.json \u2014 Project configuration");
223
+ console.log(" keystone.lock.json \u2014 Pinned dependency map");
224
+ console.log(" README.md \u2014 Getting started guide");
225
+ console.log(`
226
+ Next steps:`);
227
+ console.log(` cd ${dir || "."}`);
228
+ console.log(` Edit keystone.config.json (set your wallet address)`);
229
+ console.log(` keystone dev \u2014 Preview locally`);
230
+ console.log(` keystone ship \u2014 Ship to marketplace
231
+ `);
95
232
  }
96
233
 
97
234
  // src/commands/validate.ts
@@ -106,6 +243,72 @@ var FORBIDDEN_PATTERNS = [
106
243
  { pattern: /\beval\s*\(/g, msg: "eval() is blocked by CSP." },
107
244
  { pattern: /\bnew\s+Function\s*\(/g, msg: "new Function() is blocked by CSP." }
108
245
  ];
246
+ var AST_PATTERNS = [
247
+ {
248
+ // Dynamic import() — can load arbitrary code
249
+ pattern: /\bimport\s*\(\s*[^)]+\)/g,
250
+ msg: "Dynamic import() is blocked in sandbox. Declare imports statically.",
251
+ suggestion: "Use static imports: import { ... } from '@keystone-os/sdk';"
252
+ },
253
+ {
254
+ // XMLHttpRequest — bypass proxy gate
255
+ pattern: /\bnew\s+XMLHttpRequest\b/g,
256
+ msg: "XMLHttpRequest is blocked. Use useFetch() from '@keystone-os/sdk'.",
257
+ suggestion: "Replace with: const { data } = useFetch(url);"
258
+ },
259
+ {
260
+ // WebSocket — non-proxied network access
261
+ pattern: /\bnew\s+WebSocket\b/g,
262
+ msg: "WebSocket is blocked in sandbox. Use useMCPClient() for real-time communication.",
263
+ suggestion: "Replace with: const mcp = useMCPClient(serverUrl);"
264
+ },
265
+ {
266
+ // Worker — spawn threads
267
+ pattern: /\bnew\s+Worker\s*\(/g,
268
+ msg: "Web Workers are blocked in sandbox."
269
+ },
270
+ {
271
+ // SharedWorker
272
+ pattern: /\bnew\s+SharedWorker\s*\(/g,
273
+ msg: "SharedWorker is blocked in sandbox."
274
+ },
275
+ {
276
+ // window.open — popup
277
+ pattern: /\bwindow\.open\s*\(/g,
278
+ msg: "window.open() is blocked in sandbox."
279
+ },
280
+ {
281
+ // document.write — XSS vector
282
+ pattern: /\bdocument\.write\s*\(/g,
283
+ msg: "document.write() is blocked \u2014 XSS risk."
284
+ },
285
+ {
286
+ // innerHTML assignment — XSS vector
287
+ pattern: /\.innerHTML\s*=/g,
288
+ msg: "Direct innerHTML assignment is a XSS risk. Use React's JSX for DOM manipulation.",
289
+ suggestion: "Use React state and JSX instead of innerHTML."
290
+ },
291
+ {
292
+ // Crypto mining detection
293
+ pattern: /\bCoinHive\b|\bcoinhive\b|\bcryptominer\b/gi,
294
+ msg: "Suspected crypto mining code detected."
295
+ },
296
+ {
297
+ // Dangerous protocol URLs
298
+ pattern: /['"`]javascript:/gi,
299
+ msg: "javascript: protocol URLs are blocked \u2014 XSS risk."
300
+ },
301
+ {
302
+ // Access to __proto__ — prototype pollution
303
+ pattern: /\b__proto__\b/g,
304
+ msg: "__proto__ access is blocked \u2014 prototype pollution risk."
305
+ },
306
+ {
307
+ // Access to constructor.constructor — function creation bypass
308
+ pattern: /\.constructor\s*\.\s*constructor/g,
309
+ msg: "constructor.constructor chain is blocked \u2014 code execution bypass risk."
310
+ }
311
+ ];
109
312
  function getSuggestion(error) {
110
313
  if (error.message.includes("fetch()")) {
111
314
  return `Replace fetch(url) with: const { data } = useFetch(url);`;
@@ -127,6 +330,7 @@ function getSuggestion(error) {
127
330
  function runValidate(dir = ".", options) {
128
331
  const targetDir = path2.resolve(process.cwd(), dir);
129
332
  const errors = [];
333
+ const warnings = [];
130
334
  const files = ["App.tsx", "app.tsx"];
131
335
  for (const file of files) {
132
336
  const filePath = path2.join(targetDir, file);
@@ -137,20 +341,42 @@ function runValidate(dir = ".", options) {
137
341
  let m;
138
342
  while ((m = re.exec(content)) !== null) {
139
343
  const lineNum = content.slice(0, m.index).split("\n").length;
140
- const err = { file, line: lineNum, message: msg };
344
+ const err = { file, line: lineNum, message: msg, severity: "error" };
141
345
  if (options?.suggest) {
142
346
  err.suggestion = getSuggestion(err);
143
347
  }
144
348
  errors.push(err);
145
349
  }
146
350
  }
351
+ for (const { pattern, msg, suggestion } of AST_PATTERNS) {
352
+ const re = new RegExp(pattern.source, pattern.flags);
353
+ let m;
354
+ while ((m = re.exec(content)) !== null) {
355
+ const lineNum = content.slice(0, m.index).split("\n").length;
356
+ const entry = {
357
+ file,
358
+ line: lineNum,
359
+ message: msg,
360
+ severity: msg.includes("risk") ? "warning" : "error"
361
+ };
362
+ if (options?.suggest && suggestion) {
363
+ entry.suggestion = suggestion;
364
+ }
365
+ if (entry.severity === "warning") {
366
+ warnings.push(entry);
367
+ } else {
368
+ errors.push(entry);
369
+ }
370
+ }
371
+ }
147
372
  const hasSdkImport = /from\s+['"]@keystone-os\/sdk['"]/.test(content);
148
373
  const hasForbidden = FORBIDDEN_PATTERNS.some((p) => new RegExp(p.pattern.source).test(content));
149
374
  if (!hasSdkImport && hasForbidden) {
150
375
  const err = {
151
376
  file,
152
377
  line: 1,
153
- message: "Use '@keystone-os/sdk' for fetch/vault/turnkey instead of raw APIs."
378
+ message: "Use '@keystone-os/sdk' for fetch/vault/turnkey instead of raw APIs.",
379
+ severity: "error"
154
380
  };
155
381
  if (options?.suggest) {
156
382
  err.suggestion = `Add: import { useFetch, useVault } from '@keystone-os/sdk';`;
@@ -158,8 +384,8 @@ function runValidate(dir = ".", options) {
158
384
  errors.push(err);
159
385
  }
160
386
  }
161
- const suggestions = options?.suggest ? errors.map((e) => e.suggestion).filter(Boolean) : void 0;
162
- return { ok: errors.length === 0, errors, suggestions };
387
+ const suggestions = options?.suggest ? [...errors, ...warnings].map((e) => e.suggestion).filter(Boolean) : void 0;
388
+ return { ok: errors.length === 0, errors, warnings, suggestions };
163
389
  }
164
390
 
165
391
  // src/commands/lockfile.ts
@@ -241,6 +467,904 @@ async function runBuild(options = {}) {
241
467
  };
242
468
  }
243
469
 
470
+ // src/commands/publish.ts
471
+ var fs6 = __toESM(require("fs"));
472
+ var path6 = __toESM(require("path"));
473
+ var crypto = __toESM(require("crypto"));
474
+
475
+ // src/commands/gatekeeper.ts
476
+ var fs5 = __toESM(require("fs"));
477
+ var path5 = __toESM(require("path"));
478
+ var EXTRA_FORBIDDEN = [
479
+ { pattern: /\binnerHTML\s*=/g, msg: "innerHTML assignment is blocked (XSS risk)." },
480
+ { pattern: /\bdangerouslySetInnerHTML\b/g, msg: "dangerouslySetInnerHTML requires explicit allowlist." },
481
+ { pattern: /\bwindow\.solana\b/g, msg: "Direct window.solana access is blocked. Use useTurnkey() from SDK." },
482
+ { pattern: /\bwindow\.ethereum\b/g, msg: "Direct window.ethereum access is blocked. Use SDK hooks." }
483
+ ];
484
+ function runGatekeeper(dir = ".") {
485
+ const targetDir = path5.resolve(process.cwd(), dir);
486
+ const validate = runValidate(dir);
487
+ const lockfile = validateLockfile(dir);
488
+ const errors = [
489
+ ...validate.errors.map((e) => ({ file: e.file, line: e.line, message: e.message })),
490
+ ...lockfile.errors.map((e) => ({ file: "keystone.lock.json", line: 1, message: `${e.package}: ${e.message}` }))
491
+ ];
492
+ const files = ["App.tsx", "app.tsx"];
493
+ for (const file of files) {
494
+ const filePath = path5.join(targetDir, file);
495
+ if (!fs5.existsSync(filePath)) continue;
496
+ const content = fs5.readFileSync(filePath, "utf-8");
497
+ for (const { pattern, msg } of EXTRA_FORBIDDEN) {
498
+ const re = new RegExp(pattern.source, pattern.flags);
499
+ let m;
500
+ while ((m = re.exec(content)) !== null) {
501
+ const lineNum = content.slice(0, m.index).split("\n").length;
502
+ errors.push({ file, line: lineNum, message: msg });
503
+ }
504
+ }
505
+ }
506
+ const totalErrors = errors.length;
507
+ const securityScore = Math.max(0, Math.min(100, 100 - totalErrors * 10));
508
+ return {
509
+ ok: totalErrors === 0,
510
+ securityScore,
511
+ validate,
512
+ lockfile,
513
+ errors
514
+ };
515
+ }
516
+
517
+ // src/commands/publish.ts
518
+ function sha256Hex(data) {
519
+ return crypto.createHash("sha256").update(data, "utf8").digest("hex");
520
+ }
521
+ function getClusterUrl(cluster) {
522
+ return cluster === "mainnet-beta" ? "https://api.mainnet-beta.solana.com" : "https://api.devnet.solana.com";
523
+ }
524
+ async function uploadToArweave(bundlePath, privateKey, cluster) {
525
+ try {
526
+ const Irys = (await import("@irys/sdk")).default;
527
+ const bs58 = (await import("bs58")).default;
528
+ const irysOpts = {
529
+ network: cluster === "mainnet-beta" ? "mainnet" : "devnet",
530
+ token: "solana"
531
+ };
532
+ if (privateKey) {
533
+ irysOpts.key = bs58.decode(privateKey);
534
+ irysOpts.config = { providerUrl: getClusterUrl(cluster) };
535
+ }
536
+ const irys = new Irys(irysOpts);
537
+ if (cluster !== "mainnet-beta") {
538
+ try {
539
+ await irys.fund(irys.utils.toAtomic(0.01));
540
+ } catch (e) {
541
+ console.warn("[publish] Irys fund skipped:", e.message);
542
+ }
543
+ }
544
+ const bundle = fs6.readFileSync(bundlePath, "utf-8");
545
+ const tags = [
546
+ { name: "Content-Type", value: "application/javascript" },
547
+ { name: "App-Name", value: "Keystone-OS" },
548
+ { name: "App-Version", value: "1.0" },
549
+ { name: "Type", value: "mini-app-bundle" }
550
+ ];
551
+ const tx = await irys.upload(bundle, { tags });
552
+ console.log(` Arweave TX: ${tx?.id}`);
553
+ return tx?.id ?? null;
554
+ } catch (err) {
555
+ console.warn("[publish] Arweave upload failed:", err instanceof Error ? err.message : err);
556
+ return null;
557
+ }
558
+ }
559
+ async function registerOnChain(opts) {
560
+ try {
561
+ const { Connection, Keypair, Transaction, TransactionInstruction, PublicKey } = await import("@solana/web3.js");
562
+ const bs58 = (await import("bs58")).default;
563
+ const connection = new Connection(getClusterUrl(opts.cluster), "confirmed");
564
+ const keypair = Keypair.fromSecretKey(bs58.decode(opts.privateKey));
565
+ const MEMO_PROGRAM_ID = new PublicKey("MemoSq4gqABAXKb96qnH8TysNcWxMyWCqXgDLGmfcHr");
566
+ const memoData = JSON.stringify({
567
+ protocol: "keystone-os",
568
+ version: "1.0",
569
+ action: "register_app",
570
+ app_id: opts.appId,
571
+ name: opts.name,
572
+ description: opts.description.slice(0, 200),
573
+ code_hash: opts.codeHash,
574
+ arweave_cid: opts.arweaveTxId || null,
575
+ creator: opts.creatorWallet,
576
+ price_usdc: opts.priceUsdc || 0,
577
+ timestamp: Date.now()
578
+ });
579
+ const memoInstruction = new TransactionInstruction({
580
+ keys: [{ pubkey: keypair.publicKey, isSigner: true, isWritable: true }],
581
+ programId: MEMO_PROGRAM_ID,
582
+ data: Buffer.from(memoData, "utf-8")
583
+ });
584
+ const tx = new Transaction().add(memoInstruction);
585
+ tx.feePayer = keypair.publicKey;
586
+ const { blockhash, lastValidBlockHeight } = await connection.getLatestBlockhash();
587
+ tx.recentBlockhash = blockhash;
588
+ tx.lastValidBlockHeight = lastValidBlockHeight;
589
+ tx.sign(keypair);
590
+ const txId = await connection.sendRawTransaction(tx.serialize(), {
591
+ skipPreflight: false,
592
+ preflightCommitment: "confirmed"
593
+ });
594
+ await connection.confirmTransaction({ signature: txId, blockhash, lastValidBlockHeight }, "confirmed");
595
+ return { txId };
596
+ } catch (err) {
597
+ console.warn("[publish] On-chain registration failed:", err instanceof Error ? err.message : err);
598
+ return null;
599
+ }
600
+ }
601
+ async function registerToRegistry(apiUrl, payload) {
602
+ try {
603
+ const res = await fetch(`${apiUrl.replace(/\/$/, "")}/api/studio/publish`, {
604
+ method: "POST",
605
+ headers: { "Content-Type": "application/json" },
606
+ body: JSON.stringify(payload)
607
+ });
608
+ if (!res.ok) {
609
+ const err = await res.text();
610
+ throw new Error(err || `Registry returned ${res.status}`);
611
+ }
612
+ const json = await res.json();
613
+ return json?.appId ? { appId: json.appId } : null;
614
+ } catch (err) {
615
+ console.warn("[publish] Registry sync skipped:", err instanceof Error ? err.message : err);
616
+ return null;
617
+ }
618
+ }
619
+ async function runPublish(options) {
620
+ const targetDir = path6.resolve(process.cwd(), options.dir ?? ".");
621
+ const appPath = path6.join(targetDir, "App.tsx");
622
+ const cluster = options.cluster ?? "devnet";
623
+ if (!fs6.existsSync(appPath)) {
624
+ return { ok: false, error: "App.tsx not found. Run keystone init first." };
625
+ }
626
+ const code = fs6.readFileSync(appPath, "utf-8");
627
+ const codeJson = JSON.stringify({ files: { "App.tsx": { content: code, language: "typescript" } } });
628
+ console.log(" [1/4] Running security gatekeeper...");
629
+ const gatekeeper = runGatekeeper(targetDir);
630
+ if (!gatekeeper.ok) {
631
+ return {
632
+ ok: false,
633
+ error: `Gatekeeper failed (score: ${gatekeeper.securityScore}). Fix errors:
634
+ ${gatekeeper.errors.map((e) => ` ${e.file}:${e.line} \u2014 ${e.message}`).join("\n")}`,
635
+ securityScore: gatekeeper.securityScore
636
+ };
637
+ }
638
+ console.log(` Security: ${gatekeeper.securityScore}/100`);
639
+ console.log(" [2/4] Building bundle...");
640
+ const build = await runBuild({ dir: targetDir, outDir: path6.join(targetDir, ".keystone", "dist") });
641
+ if (!build.ok) {
642
+ return { ok: false, error: build.error };
643
+ }
644
+ const bundlePath = build.outputPath;
645
+ const bundleContent = fs6.readFileSync(bundlePath, "utf-8");
646
+ const codeHash = sha256Hex(bundleContent);
647
+ console.log(` Code hash: ${codeHash.slice(0, 16)}...`);
648
+ let arweaveTxId = null;
649
+ if (!options.skipArweave && options.privateKey) {
650
+ console.log(" [3/4] Uploading to Arweave via Irys...");
651
+ arweaveTxId = await uploadToArweave(bundlePath, options.privateKey, cluster);
652
+ if (arweaveTxId) {
653
+ console.log(` Arweave: https://arweave.net/${arweaveTxId}`);
654
+ }
655
+ } else if (!options.skipArweave) {
656
+ console.log(" [3/4] Arweave skipped (no private key provided)");
657
+ } else {
658
+ console.log(" [3/4] Arweave skipped (--skip-arweave)");
659
+ }
660
+ const finalAppId = `app_${Date.now().toString(36)}`;
661
+ let solanaTxId;
662
+ let explorerUrl;
663
+ if (options.privateKey) {
664
+ console.log(" [4/4] Registering on Solana...");
665
+ const onChain = await registerOnChain({
666
+ privateKey: options.privateKey,
667
+ cluster,
668
+ appId: finalAppId,
669
+ name: options.name,
670
+ description: options.description,
671
+ codeHash,
672
+ arweaveTxId: arweaveTxId ?? void 0,
673
+ creatorWallet: options.creatorWallet,
674
+ priceUsdc: options.priceUsdc
675
+ });
676
+ if (onChain) {
677
+ solanaTxId = onChain.txId;
678
+ explorerUrl = `https://explorer.solana.com/tx/${solanaTxId}?cluster=${cluster}`;
679
+ console.log(` Solana TX: ${solanaTxId}`);
680
+ }
681
+ } else {
682
+ console.log(" [4/4] On-chain registration skipped (no private key)");
683
+ }
684
+ if (options.apiUrl) {
685
+ await registerToRegistry(options.apiUrl, {
686
+ name: options.name,
687
+ description: options.description,
688
+ code: codeJson,
689
+ creatorWallet: options.creatorWallet,
690
+ arweaveTxId: arweaveTxId ?? void 0,
691
+ codeHash,
692
+ securityScore: gatekeeper.securityScore,
693
+ category: options.category ?? "utility"
694
+ });
695
+ }
696
+ return {
697
+ ok: true,
698
+ appId: finalAppId,
699
+ arweaveTxId: arweaveTxId ?? void 0,
700
+ codeHash,
701
+ securityScore: gatekeeper.securityScore,
702
+ solanaTxId,
703
+ explorerUrl
704
+ };
705
+ }
706
+
707
+ // src/commands/dev.ts
708
+ var fs7 = __toESM(require("fs"));
709
+ var path7 = __toESM(require("path"));
710
+ var http = __toESM(require("http"));
711
+ var DEFAULT_PORT = 4200;
712
+ function buildSDKModule() {
713
+ return `
714
+ var React = window.React;
715
+ var useState = React.useState;
716
+ var useEffect = React.useEffect;
717
+ var useCallback = React.useCallback;
718
+
719
+ var useVault = function() {
720
+ var tokens = [
721
+ { symbol: 'SOL', name: 'Solana', balance: 124.5, price: 23.40, mint: 'So11111111111111111111111111111111111111112', logoURI: '' },
722
+ { symbol: 'USDC', name: 'USD Coin', balance: 5400.2, price: 1.00, mint: 'EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v', logoURI: '' },
723
+ { symbol: 'BONK', name: 'Bonk', balance: 15000000, price: 0.000024, mint: 'DezXAZ8z7PnrnRJjz3wXBoRgixCa6xjnB7YaB1pPB263', logoURI: '' },
724
+ { symbol: 'JUP', name: 'Jupiter', balance: 850, price: 1.12, mint: 'JUPyiwrYJFskUPiHa7hkeR8VUtAeFoSYbKedZNsDvCN', logoURI: '' },
725
+ ];
726
+ var balances = {};
727
+ tokens.forEach(function(t) { balances[t.symbol] = t.balance; });
728
+ return { activeVault: 'Main Portfolio', balances: balances, tokens: tokens };
729
+ };
730
+
731
+ var useTurnkey = function() {
732
+ return {
733
+ getPublicKey: function() { return Promise.resolve('7KeY...DeCv (Local Dev)'); },
734
+ signTransaction: function(tx, desc) {
735
+ console.log('[Turnkey] Sign request:', desc || 'Sign transaction');
736
+ return Promise.resolve({ signature: 'dev_sig_' + Math.random().toString(36).slice(2, 10) });
737
+ },
738
+ };
739
+ };
740
+
741
+ var useFetch = function(url, options) {
742
+ options = options || {};
743
+ var _s = useState(null), data = _s[0], setData = _s[1];
744
+ var _e = useState(null), error = _e[0], setError = _e[1];
745
+ var _l = useState(true), loading = _l[0], setLoading = _l[1];
746
+ var fetchData = useCallback(function() {
747
+ setLoading(true); setError(null);
748
+ return fetch(url, { method: options.method || 'GET', headers: options.headers || {}, body: options.body ? JSON.stringify(options.body) : undefined })
749
+ .then(function(r) { return r.json(); })
750
+ .then(function(d) { setData(d); })
751
+ .catch(function(err) { setError(err.message); })
752
+ .finally(function() { setLoading(false); });
753
+ }, [url]);
754
+ useEffect(function() { fetchData(); }, [fetchData]);
755
+ return { data: data, error: error, loading: loading, refetch: fetchData };
756
+ };
757
+
758
+ var AppEventBus = {
759
+ emit: function(type, payload) { console.log('[EventBus]', type, payload); },
760
+ };
761
+
762
+ var useEncryptedSecret = function() {
763
+ return { encrypt: function(p) { return Promise.resolve('enc_' + btoa(p)); }, decrypt: function(c) { return Promise.resolve(atob(c.replace('enc_', ''))); }, loading: false, error: null };
764
+ };
765
+ var useACEReport = function() { return { report: [], loading: false, error: null, refetch: function() { return Promise.resolve(); } }; };
766
+ var useAgentHandoff = function(from) { return { handoffTo: function(to, ctx) { console.log('[Agent] Handoff', from, '->', to); return Promise.resolve({ status: 'ok' }); } }; };
767
+ var useMCPClient = function(url) { return { call: function(tool, params) { console.log('[MCP]', tool, params); return Promise.resolve({ result: 'mock' }); }, loading: false, error: null }; };
768
+ var useMCPServer = function(tools, handlers) { return { registerTools: function() {}, handleCall: function(t, p) { return handlers[t] ? handlers[t](p) : Promise.reject(new Error('Unknown tool')); } }; };
769
+ var useSIWS = function() { return { signIn: function() { return Promise.resolve({ message: 'SIWS', signature: 'mock_sig' }); }, verify: function() { return Promise.resolve(true); }, session: null }; };
770
+ var useJupiterSwap = function() {
771
+ return {
772
+ swap: function(p) { return Promise.resolve({ swapTransaction: 'mock_tx', lastValidBlockHeight: 250000000, prioritizationFeeLamports: 50000 }); },
773
+ getQuote: function(p) { return Promise.resolve({ inputMint: p.inputMint, outputMint: p.outputMint, inAmount: p.amount, outAmount: String(parseFloat(p.amount) * 23.42), priceImpactPct: 0.12, routePlan: [] }); },
774
+ loading: false, error: null
775
+ };
776
+ };
777
+ var useImpactReport = function() {
778
+ return { simulate: function(tx) { return Promise.resolve({ before: { activeVault: 'Main', balances: {}, tokens: [] }, after: { activeVault: 'Main', balances: {}, tokens: [] }, diff: [{ symbol: 'SOL', delta: -1.0, percentChange: -0.8 }, { symbol: 'USDC', delta: 23.4, percentChange: 0.43 }] }); }, report: null, loading: false, error: null };
779
+ };
780
+ var useTaxForensics = function() { return { result: null, loading: false, error: null, refetch: function() { return Promise.resolve(); } }; };
781
+ var useYieldOptimizer = function() { return { paths: [], loading: false, error: null, refetch: function() { return Promise.resolve(); } }; };
782
+ var useGaslessTx = function() { return { submit: function(tx) { return Promise.resolve({ signature: 'gasless_dev_' + Math.random().toString(36).slice(2, 8) }); }, loading: false, error: null }; };
783
+
784
+ var usePortfolio = function() {
785
+ var vault = useVault();
786
+ return { data: { tokens: vault.tokens, totalValue: vault.tokens.reduce(function(s,t) { return s + t.balance * t.price; }, 0) }, isLoading: false, error: null, refetch: function() {} };
787
+ };
788
+
789
+ var useTheme = function() {
790
+ return { isDark: true, theme: 'dark', toggleTheme: function() { console.log('[Theme] Toggle'); } };
791
+ };
792
+
793
+ var useTokenPrice = function(mint) {
794
+ var prices = { 'So11111111111111111111111111111111111111112': 23.40, 'EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v': 1.00, 'DezXAZ8z7PnrnRJjz3wXBoRgixCa6xjnB7YaB1pPB263': 0.000024, 'JUPyiwrYJFskUPiHa7hkeR8VUtAeFoSYbKedZNsDvCN': 1.12 };
795
+ return { price: prices[mint] || 0, loading: false };
796
+ };
797
+
798
+ var useNotification = function() {
799
+ var _n = useState([]), notifications = _n[0], setNotifications = _n[1];
800
+ return {
801
+ notifications: notifications,
802
+ send: function(title, opts) { var id = 'notif_' + Date.now().toString(36); setNotifications(function(p) { return [{ id: id, type: (opts && opts.type) || 'info', title: title, message: opts && opts.message, timestamp: Date.now(), read: false }].concat(p); }); return id; },
803
+ dismiss: function(id) { setNotifications(function(p) { return p.filter(function(n) { return n.id !== id; }); }); },
804
+ markRead: function(id) { setNotifications(function(p) { return p.map(function(n) { return n.id === id ? Object.assign({}, n, { read: true }) : n; }); }); },
805
+ clearAll: function() { setNotifications([]); },
806
+ unreadCount: notifications.filter(function(n) { return !n.read; }).length
807
+ };
808
+ };
809
+
810
+ var useStorage = function(ns) {
811
+ var prefix = ns ? 'ks_app_' + ns + '_' : 'ks_app_';
812
+ return {
813
+ get: function(k) { try { return localStorage.getItem(prefix + k); } catch(e) { return null; } },
814
+ set: function(k, v) { try { localStorage.setItem(prefix + k, v); } catch(e) {} },
815
+ remove: function(k) { try { localStorage.removeItem(prefix + k); } catch(e) {} },
816
+ keys: function() { var r = []; try { for (var i = 0; i < localStorage.length; i++) { var k = localStorage.key(i); if (k && k.indexOf(prefix) === 0) r.push(k.slice(prefix.length)); } } catch(e) {} return r; },
817
+ clear: function() { try { var rm = []; for (var i = 0; i < localStorage.length; i++) { var k = localStorage.key(i); if (k && k.indexOf(prefix) === 0) rm.push(k); } rm.forEach(function(k) { localStorage.removeItem(k); }); } catch(e) {} }
818
+ };
819
+ };
820
+
821
+ var exportsObj = {
822
+ useVault: useVault, useTurnkey: useTurnkey, useFetch: useFetch, AppEventBus: AppEventBus,
823
+ useEncryptedSecret: useEncryptedSecret, useACEReport: useACEReport, useAgentHandoff: useAgentHandoff,
824
+ useMCPClient: useMCPClient, useMCPServer: useMCPServer, useSIWS: useSIWS, useJupiterSwap: useJupiterSwap,
825
+ useImpactReport: useImpactReport, useTaxForensics: useTaxForensics, useYieldOptimizer: useYieldOptimizer,
826
+ useGaslessTx: useGaslessTx, usePortfolio: usePortfolio, useTheme: useTheme, useTokenPrice: useTokenPrice,
827
+ useNotification: useNotification, useStorage: useStorage
828
+ };
829
+ window.__keystoneSDK = Object.assign({}, exportsObj, { default: exportsObj });
830
+ window.__keystoneSDK.__esModule = true;
831
+ `;
832
+ }
833
+ function buildHTML(appCode) {
834
+ const normalizedCode = appCode.replace(/from\s+['"]\.\/keystone['"]/g, 'from "@keystone-os/sdk"').replace(/from\s+['"]keystone-api['"]/g, 'from "@keystone-os/sdk"');
835
+ const escapedCode = JSON.stringify(normalizedCode);
836
+ return `<!DOCTYPE html>
837
+ <html>
838
+ <head>
839
+ <meta charset="UTF-8">
840
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
841
+ <title>Keystone Dev Server</title>
842
+ <script src="https://unpkg.com/react@18.2.0/umd/react.development.js"></script>
843
+ <script src="https://unpkg.com/react-dom@18.2.0/umd/react-dom.development.js"></script>
844
+ <script src="https://unpkg.com/@babel/standalone@7.26.2/babel.min.js"></script>
845
+ <script src="https://cdn.tailwindcss.com"></script>
846
+ <style>
847
+ body { margin: 0; background: #09090b; color: white; font-family: system-ui, -apple-system, sans-serif; min-height: 100vh; }
848
+ #root { min-height: 100vh; }
849
+ </style>
850
+ <script>
851
+ (function() { ${buildSDKModule()} })();
852
+ </script>
853
+ <script>
854
+ (function() {
855
+ var moduleMap = { 'react': window.React, 'react-dom': window.ReactDOM, 'react-dom/client': window.ReactDOM, '@keystone-os/sdk': window.__keystoneSDK };
856
+ window.__ks_require = function(name) { if (moduleMap[name]) return moduleMap[name]; throw new Error('[Keystone] Module not found: ' + name); };
857
+ })();
858
+ </script>
859
+ </head>
860
+ <body>
861
+ <div id="root"></div>
862
+ <script>
863
+ (function() {
864
+ try {
865
+ var rawCode = ${escapedCode};
866
+ var compiled = Babel.transform(rawCode, { presets: ['typescript', 'react'], plugins: ['transform-modules-commonjs'], filename: 'App.tsx', retainLines: true });
867
+ var _module = { exports: {} };
868
+ var fn = new Function('require', 'module', 'exports', compiled.code);
869
+ fn(window.__ks_require, _module, _module.exports);
870
+ var App = _module.exports.default || _module.exports;
871
+ if (typeof App !== 'function') throw new Error('App.tsx must export a default React component');
872
+ ReactDOM.createRoot(document.getElementById('root')).render(React.createElement(App));
873
+ } catch (err) {
874
+ document.getElementById('root').innerHTML = '<div style="color:#ef4444;padding:20px;font-family:monospace">' +
875
+ '<strong style="color:#f87171">Runtime Error</strong><br/>' +
876
+ '<span style="color:#fca5a5">' + (err.message || err) + '</span></div>';
877
+ console.error(err);
878
+ }
879
+ })();
880
+ </script>
881
+ <script>
882
+ // Auto-reload on file changes (poll-based)
883
+ (function() {
884
+ var lastHash = '';
885
+ setInterval(function() {
886
+ fetch('/__keystone_dev_hash')
887
+ .then(function(r) { return r.text(); })
888
+ .then(function(hash) {
889
+ if (lastHash && hash !== lastHash) { location.reload(); }
890
+ lastHash = hash;
891
+ })
892
+ .catch(function() {});
893
+ }, 1000);
894
+ })();
895
+ </script>
896
+ </body>
897
+ </html>`;
898
+ }
899
+ function runDev(options = {}) {
900
+ const dir = path7.resolve(process.cwd(), options.dir || ".");
901
+ const port = options.port || DEFAULT_PORT;
902
+ const appPath = path7.join(dir, "App.tsx");
903
+ if (!fs7.existsSync(appPath)) {
904
+ console.error(`
905
+ \u274C No App.tsx found in ${dir}`);
906
+ console.error(" Run 'keystone init' first to scaffold a Mini-App.\n");
907
+ process.exit(1);
908
+ }
909
+ let contentHash = "";
910
+ function getAppCode() {
911
+ return fs7.readFileSync(appPath, "utf-8");
912
+ }
913
+ function computeHash(content) {
914
+ let hash = 0;
915
+ for (let i = 0; i < content.length; i++) {
916
+ hash = (hash << 5) - hash + content.charCodeAt(i) | 0;
917
+ }
918
+ return hash.toString(36);
919
+ }
920
+ let watchTimer = null;
921
+ fs7.watch(dir, { recursive: true }, () => {
922
+ if (watchTimer) clearTimeout(watchTimer);
923
+ watchTimer = setTimeout(() => {
924
+ const code = getAppCode();
925
+ contentHash = computeHash(code);
926
+ }, 200);
927
+ });
928
+ contentHash = computeHash(getAppCode());
929
+ const server = http.createServer((req, res) => {
930
+ if (req.url === "/__keystone_dev_hash") {
931
+ res.writeHead(200, { "Content-Type": "text/plain", "Access-Control-Allow-Origin": "*" });
932
+ res.end(contentHash);
933
+ return;
934
+ }
935
+ const appCode = getAppCode();
936
+ contentHash = computeHash(appCode);
937
+ const html = buildHTML(appCode);
938
+ res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
939
+ res.end(html);
940
+ });
941
+ server.listen(port, () => {
942
+ console.log(`
943
+ \u{1F680} Keystone Dev Server running at http://localhost:${port}`);
944
+ console.log(` Watching: ${appPath}`);
945
+ console.log(` Auto-reload: enabled (1s poll)`);
946
+ console.log(`
947
+ Press Ctrl+C to stop.
948
+ `);
949
+ });
950
+ }
951
+
952
+ // src/commands/generate.ts
953
+ var fs8 = __toESM(require("fs"));
954
+ var path8 = __toESM(require("path"));
955
+ async function callLLMDirect(prompt, provider, apiKey, model) {
956
+ let endpoint;
957
+ let headers;
958
+ let body;
959
+ const systemPrompt = `You are "The Architect" \u2014 a Keystone OS Mini-App code generator.
960
+
961
+ RULES:
962
+ - Generate a SINGLE App.tsx file using React + TypeScript
963
+ - Import ONLY from '@keystone-os/sdk' and 'react'
964
+ - Available SDK hooks: useVault, useJupiterSwap, useImpactReport, useTurnkey,
965
+ useNotifications, useGovernance, useFetch, useTheme, useKeystoneEvent,
966
+ useYieldRouter, useTaxEngine, useAnalytics, useSovereignAuth,
967
+ useMultiSig, usePortfolio, useAlerts
968
+ - NO fetch(), axios, ethers, @solana/web3.js, localStorage, or Node.js APIs
969
+ - Use Tailwind CSS classes for styling (dark theme: bg-zinc-950, text-white)
970
+
971
+ OUTPUT: Return ONLY raw JSON (no markdown blocks) in this format:
972
+ {
973
+ "files": { "App.tsx": "...full code..." },
974
+ "explanation": "Brief description of what was built"
975
+ }`;
976
+ if (provider === "groq") {
977
+ endpoint = "https://api.groq.com/openai/v1/chat/completions";
978
+ headers = {
979
+ "Content-Type": "application/json",
980
+ Authorization: `Bearer ${apiKey}`
981
+ };
982
+ body = JSON.stringify({
983
+ model: model || "llama-3.3-70b-versatile",
984
+ messages: [
985
+ { role: "system", content: systemPrompt },
986
+ { role: "user", content: prompt }
987
+ ],
988
+ temperature: 0.7,
989
+ max_tokens: 4e3
990
+ });
991
+ } else if (provider === "cloudflare") {
992
+ const parts = apiKey.split(":");
993
+ const accountId = parts[0];
994
+ const cfToken = parts.slice(1).join(":");
995
+ endpoint = `https://api.cloudflare.com/client/v4/accounts/${accountId}/ai/v1/chat/completions`;
996
+ headers = {
997
+ "Content-Type": "application/json",
998
+ Authorization: `Bearer ${cfToken}`
999
+ };
1000
+ body = JSON.stringify({
1001
+ model: model || "@cf/qwen/qwen3-30b-a3b-fp8",
1002
+ messages: [
1003
+ { role: "system", content: systemPrompt },
1004
+ { role: "user", content: prompt }
1005
+ ],
1006
+ temperature: 0.7,
1007
+ max_tokens: 4e3
1008
+ });
1009
+ } else if (provider === "openai") {
1010
+ endpoint = "https://api.openai.com/v1/chat/completions";
1011
+ headers = {
1012
+ "Content-Type": "application/json",
1013
+ Authorization: `Bearer ${apiKey}`
1014
+ };
1015
+ body = JSON.stringify({
1016
+ model: model || "gpt-4o",
1017
+ messages: [
1018
+ { role: "system", content: systemPrompt },
1019
+ { role: "user", content: prompt }
1020
+ ],
1021
+ temperature: 0.7,
1022
+ max_tokens: 4e3,
1023
+ response_format: { type: "json_object" }
1024
+ });
1025
+ } else if (provider === "ollama") {
1026
+ const host = apiKey || "http://localhost:11434";
1027
+ endpoint = `${host}/v1/chat/completions`;
1028
+ headers = { "Content-Type": "application/json" };
1029
+ body = JSON.stringify({
1030
+ model: model || "qwen2.5-coder:7b",
1031
+ messages: [
1032
+ { role: "system", content: systemPrompt },
1033
+ { role: "user", content: prompt }
1034
+ ],
1035
+ temperature: 0.7
1036
+ });
1037
+ } else {
1038
+ throw new Error(`Unsupported provider: ${provider}`);
1039
+ }
1040
+ const res = await fetch(endpoint, { method: "POST", headers, body });
1041
+ if (!res.ok) {
1042
+ const err = await res.text().catch(() => "");
1043
+ throw new Error(`${provider} API error (${res.status}): ${err.slice(0, 200)}`);
1044
+ }
1045
+ const json = await res.json();
1046
+ let content = json.choices?.[0]?.message?.content || "{}";
1047
+ content = content.replace(/^```json?\s*/i, "").replace(/\s*```$/i, "").trim();
1048
+ const parsed = JSON.parse(content);
1049
+ return {
1050
+ files: parsed.files || {},
1051
+ explanation: parsed.explanation || ""
1052
+ };
1053
+ }
1054
+ async function callServerGenerate(prompt, apiUrl, aiConfig) {
1055
+ const res = await fetch(`${apiUrl.replace(/\/$/, "")}/api/studio/generate`, {
1056
+ method: "POST",
1057
+ headers: { "Content-Type": "application/json" },
1058
+ body: JSON.stringify({
1059
+ prompt,
1060
+ ...aiConfig && { aiConfig }
1061
+ })
1062
+ });
1063
+ if (!res.ok) {
1064
+ throw new Error(`Server returned ${res.status}: ${await res.text()}`);
1065
+ }
1066
+ const json = await res.json();
1067
+ if (json.error) {
1068
+ throw new Error(json.error === "no_api_key" ? json.details : json.error);
1069
+ }
1070
+ return { files: json.files || {}, explanation: json.explanation || "" };
1071
+ }
1072
+ async function runGenerate(options) {
1073
+ const targetDir = path8.resolve(process.cwd(), options.dir ?? ".");
1074
+ const appPath = path8.join(targetDir, "App.tsx");
1075
+ if (fs8.existsSync(appPath) && !options.force) {
1076
+ return {
1077
+ ok: false,
1078
+ error: "App.tsx already exists. Use --force to overwrite, or delete it first."
1079
+ };
1080
+ }
1081
+ let result;
1082
+ let usedProvider = "server";
1083
+ try {
1084
+ if (options.provider && options.apiKey) {
1085
+ usedProvider = options.provider;
1086
+ result = await callLLMDirect(
1087
+ options.prompt,
1088
+ options.provider,
1089
+ options.apiKey,
1090
+ options.model || ""
1091
+ );
1092
+ } else if (options.apiUrl) {
1093
+ result = await callServerGenerate(
1094
+ options.prompt,
1095
+ options.apiUrl,
1096
+ options.apiKey ? { provider: options.provider || "groq", apiKey: options.apiKey, model: options.model || "" } : void 0
1097
+ );
1098
+ } else if (process.env.GROQ_API_KEY) {
1099
+ usedProvider = "groq";
1100
+ result = await callLLMDirect(
1101
+ options.prompt,
1102
+ "groq",
1103
+ process.env.GROQ_API_KEY,
1104
+ options.model || "llama-3.3-70b-versatile"
1105
+ );
1106
+ } else if (process.env.CLOUDFLARE_ACCOUNT_ID && process.env.CLOUDFLARE_AI_TOKEN) {
1107
+ usedProvider = "cloudflare";
1108
+ result = await callLLMDirect(
1109
+ options.prompt,
1110
+ "cloudflare",
1111
+ `${process.env.CLOUDFLARE_ACCOUNT_ID}:${process.env.CLOUDFLARE_AI_TOKEN}`,
1112
+ options.model || "@cf/qwen/qwen3-30b-a3b-fp8"
1113
+ );
1114
+ } else {
1115
+ return {
1116
+ ok: false,
1117
+ error: "No AI provider configured. Options:\n --provider groq --api-key gsk_... (direct)\n --provider cloudflare --api-key id:token (direct)\n --api-url http://localhost:3000 (server mode)\n Set GROQ_API_KEY or CLOUDFLARE_ACCOUNT_ID + CLOUDFLARE_AI_TOKEN env vars"
1118
+ };
1119
+ }
1120
+ for (const [fileName, content] of Object.entries(result.files)) {
1121
+ const filePath = path8.join(targetDir, fileName);
1122
+ fs8.mkdirSync(path8.dirname(filePath), { recursive: true });
1123
+ fs8.writeFileSync(filePath, typeof content === "string" ? content : content.content || "", "utf-8");
1124
+ }
1125
+ return {
1126
+ ok: true,
1127
+ files: result.files,
1128
+ explanation: result.explanation,
1129
+ provider: usedProvider
1130
+ };
1131
+ } catch (err) {
1132
+ return {
1133
+ ok: false,
1134
+ error: err instanceof Error ? err.message : String(err),
1135
+ provider: usedProvider
1136
+ };
1137
+ }
1138
+ }
1139
+
1140
+ // src/commands/deploy.ts
1141
+ var fs9 = __toESM(require("fs"));
1142
+ var path9 = __toESM(require("path"));
1143
+ var import_node_child_process = require("child_process");
1144
+ var import_node_util = require("util");
1145
+ var execAsync = (0, import_node_util.promisify)(import_node_child_process.exec);
1146
+ async function runDeploy(options) {
1147
+ const targetDir = path9.resolve(process.cwd(), options.dir ?? ".");
1148
+ const cluster = options.cluster ?? "devnet";
1149
+ const programName = options.programName ?? "keystone_app";
1150
+ try {
1151
+ await execAsync("solana --version");
1152
+ } catch {
1153
+ return {
1154
+ ok: false,
1155
+ error: 'Solana CLI not found. Install it:\n sh -c "$(curl -sSfL https://release.anza.xyz/stable/install)"\n export PATH="~/.local/share/solana/install/active_release/bin:$PATH"'
1156
+ };
1157
+ }
1158
+ const soPath = options.soPath || path9.join(targetDir, ".keystone", "dist", `${programName}.so`) || path9.join(targetDir, "target", "deploy", `${programName}.so`);
1159
+ if (!fs9.existsSync(soPath)) {
1160
+ return {
1161
+ ok: false,
1162
+ error: `Compiled program not found at: ${soPath}
1163
+ Run 'keystone build' first, or provide --so-path.`
1164
+ };
1165
+ }
1166
+ try {
1167
+ await execAsync(`solana config set --url ${getClusterUrl2(cluster)}`);
1168
+ } catch (err) {
1169
+ return { ok: false, error: `Failed to set cluster: ${err.message}` };
1170
+ }
1171
+ try {
1172
+ const { stdout } = await execAsync("solana balance");
1173
+ const balance = parseFloat(stdout.trim().split(" ")[0]);
1174
+ if (balance < 0.5 && cluster !== "localnet") {
1175
+ console.warn(
1176
+ `Warning: Low deployer balance (${balance} SOL). Deploy may fail.
1177
+ ` + (cluster === "devnet" ? " Run: solana airdrop 2" : " Fund your wallet before deploying.")
1178
+ );
1179
+ }
1180
+ } catch {
1181
+ }
1182
+ try {
1183
+ let deployCmd = `solana program deploy "${soPath}"`;
1184
+ if (options.programKeypair) {
1185
+ deployCmd += ` --program-id "${options.programKeypair}"`;
1186
+ }
1187
+ if (options.keypair) {
1188
+ deployCmd += ` --keypair "${options.keypair}"`;
1189
+ }
1190
+ console.log(`Deploying to ${cluster}...`);
1191
+ const { stdout, stderr } = await execAsync(deployCmd, {
1192
+ cwd: targetDir,
1193
+ timeout: 18e4
1194
+ // 3 min timeout
1195
+ });
1196
+ const programIdMatch = stdout.match(/Program Id:\s*(\w+)/);
1197
+ const programId = programIdMatch?.[1];
1198
+ if (!programId) {
1199
+ return {
1200
+ ok: false,
1201
+ error: `Deploy succeeded but could not parse program ID:
1202
+ ${stdout}`
1203
+ };
1204
+ }
1205
+ let idlUploaded = false;
1206
+ if (!options.skipIdl) {
1207
+ const idlPath = path9.join(
1208
+ targetDir,
1209
+ "target",
1210
+ "idl",
1211
+ `${programName}.json`
1212
+ );
1213
+ if (fs9.existsSync(idlPath)) {
1214
+ try {
1215
+ await execAsync(
1216
+ `anchor idl init --filepath "${idlPath}" ${programId}`,
1217
+ { cwd: targetDir, timeout: 6e4 }
1218
+ );
1219
+ idlUploaded = true;
1220
+ } catch (idlErr) {
1221
+ console.warn(`IDL upload skipped: ${idlErr.message}`);
1222
+ }
1223
+ }
1224
+ }
1225
+ const txMatch = stdout.match(/Signature:\s*(\w+)/);
1226
+ return {
1227
+ ok: true,
1228
+ programId,
1229
+ cluster,
1230
+ txSignature: txMatch?.[1],
1231
+ idlUploaded
1232
+ };
1233
+ } catch (err) {
1234
+ return {
1235
+ ok: false,
1236
+ error: err.stderr || err.message || "Deployment failed",
1237
+ cluster
1238
+ };
1239
+ }
1240
+ }
1241
+ function getClusterUrl2(cluster) {
1242
+ switch (cluster) {
1243
+ case "mainnet-beta":
1244
+ return "https://api.mainnet-beta.solana.com";
1245
+ case "devnet":
1246
+ return "https://api.devnet.solana.com";
1247
+ case "testnet":
1248
+ return "https://api.testnet.solana.com";
1249
+ case "localnet":
1250
+ return "http://localhost:8899";
1251
+ default:
1252
+ return "https://api.devnet.solana.com";
1253
+ }
1254
+ }
1255
+
1256
+ // src/commands/ship.ts
1257
+ var path11 = __toESM(require("path"));
1258
+ var fs11 = __toESM(require("fs"));
1259
+
1260
+ // src/commands/config.ts
1261
+ var fs10 = __toESM(require("fs"));
1262
+ var path10 = __toESM(require("path"));
1263
+ var CONFIG_FILE = "keystone.config.json";
1264
+ function loadConfig(dir) {
1265
+ const searchDir = dir ? path10.resolve(dir) : process.cwd();
1266
+ const configPath = path10.join(searchDir, CONFIG_FILE);
1267
+ if (!fs10.existsSync(configPath)) {
1268
+ return {};
1269
+ }
1270
+ try {
1271
+ const raw = fs10.readFileSync(configPath, "utf-8");
1272
+ return JSON.parse(raw);
1273
+ } catch (e) {
1274
+ console.warn(`[config] Failed to parse ${CONFIG_FILE}:`, e.message);
1275
+ return {};
1276
+ }
1277
+ }
1278
+ function saveConfig(config, dir) {
1279
+ const searchDir = dir ? path10.resolve(dir) : process.cwd();
1280
+ const configPath = path10.join(searchDir, CONFIG_FILE);
1281
+ fs10.writeFileSync(configPath, JSON.stringify(config, null, 2) + "\n", "utf-8");
1282
+ }
1283
+ function mergeConfig(config, flags) {
1284
+ const merged = { ...config };
1285
+ for (const [key, value] of Object.entries(flags)) {
1286
+ if (value !== void 0 && value !== null && value !== "") {
1287
+ merged[key] = value;
1288
+ }
1289
+ }
1290
+ return merged;
1291
+ }
1292
+
1293
+ // src/commands/ship.ts
1294
+ async function runShip(options) {
1295
+ const dir = path11.resolve(process.cwd(), options.dir || ".");
1296
+ const appPath = path11.join(dir, "App.tsx");
1297
+ if (!fs11.existsSync(appPath)) {
1298
+ return { ok: false, error: "No App.tsx found. Run `keystone init` to get started." };
1299
+ }
1300
+ const config = loadConfig(dir);
1301
+ const merged = mergeConfig(config, {
1302
+ name: options.name,
1303
+ description: options.description,
1304
+ wallet: options.wallet,
1305
+ privateKey: options.privateKey,
1306
+ cluster: options.cluster
1307
+ });
1308
+ const appName = merged.name || path11.basename(dir);
1309
+ const description = merged.description || `Built with Keystone CLI`;
1310
+ const wallet = merged.wallet;
1311
+ if (!wallet) {
1312
+ return {
1313
+ ok: false,
1314
+ error: [
1315
+ "No wallet address found.",
1316
+ "",
1317
+ "Set it in keystone.config.json:",
1318
+ ' { "wallet": "YOUR_WALLET_ADDRESS" }',
1319
+ "",
1320
+ "Or pass it as a flag:",
1321
+ " keystone ship --wallet YOUR_WALLET_ADDRESS"
1322
+ ].join("\n")
1323
+ };
1324
+ }
1325
+ console.log(`
1326
+ Shipping "${appName}" to Keystone Marketplace
1327
+ `);
1328
+ console.log(" [1/4] Running security gatekeeper...");
1329
+ const gatekeeper = runGatekeeper(dir);
1330
+ if (!gatekeeper.ok) {
1331
+ return {
1332
+ ok: false,
1333
+ securityScore: gatekeeper.securityScore,
1334
+ error: `Security check failed (${gatekeeper.securityScore}/100):
1335
+ ${gatekeeper.errors.map((e) => ` ${e.file}:${e.line} \u2014 ${e.message}`).join("\n")}`
1336
+ };
1337
+ }
1338
+ console.log(` Score: ${gatekeeper.securityScore}/100`);
1339
+ console.log(" [2/4] Building bundle...");
1340
+ const build = await runBuild({ dir, outDir: path11.join(dir, ".keystone", "dist") });
1341
+ if (!build.ok) {
1342
+ return { ok: false, error: `Build failed: ${build.error}` };
1343
+ }
1344
+ console.log(` Output: ${path11.basename(build.outputPath)}`);
1345
+ const publishResult = await runPublish({
1346
+ dir,
1347
+ name: appName,
1348
+ description,
1349
+ creatorWallet: wallet,
1350
+ privateKey: merged.privateKey || options.privateKey,
1351
+ cluster: merged.cluster || options.cluster || "devnet",
1352
+ skipArweave: options.skipArweave
1353
+ });
1354
+ if (!publishResult.ok) {
1355
+ return { ok: false, error: publishResult.error };
1356
+ }
1357
+ return {
1358
+ ok: true,
1359
+ appId: publishResult.appId,
1360
+ arweaveTxId: publishResult.arweaveTxId,
1361
+ solanaTxId: publishResult.solanaTxId,
1362
+ explorerUrl: publishResult.explorerUrl,
1363
+ securityScore: gatekeeper.securityScore,
1364
+ codeHash: publishResult.codeHash
1365
+ };
1366
+ }
1367
+
244
1368
  // src/index.ts
245
1369
  var program = new import_commander.Command();
246
1370
  program.name("keystone").description("CLI for Keystone Studio Mini-Apps \u2014 Sovereign OS 2026").version("0.2.0");
@@ -252,6 +1376,51 @@ program.command("init [dir]").description("Scaffold a new Mini-App").action((dir
252
1376
  process.exit(1);
253
1377
  }
254
1378
  });
1379
+ program.command("dev [dir]").description("Start local dev server with Keystone SDK sandbox").option("-p, --port <port>", "Port number (default: 4200)", (v) => parseInt(v, 10)).action((dir = ".", opts) => {
1380
+ try {
1381
+ runDev({ dir, port: opts.port });
1382
+ } catch (err) {
1383
+ console.error(err instanceof Error ? err.message : err);
1384
+ process.exit(1);
1385
+ }
1386
+ });
1387
+ program.command("generate").description("AI-generate a Mini-App from a natural language prompt").requiredOption("-p, --prompt <prompt>", "What to build (e.g. 'Build a Jupiter swap widget')").option("--provider <provider>", "LLM provider: groq, cloudflare, openai, ollama").option("--api-key <key>", "API key (or accountId:token for Cloudflare)").option("--model <model>", "Model override (default: auto per provider)").option("--api-url <url>", "Use a running Keystone server instead of direct LLM").option("-f, --force", "Overwrite existing App.tsx").option("-d, --dir <dir>", "Target directory (default: current)", ".").action(async (opts) => {
1388
+ try {
1389
+ console.log("\nKeystone Architect -- Generating Mini-App...\n");
1390
+ const result = await runGenerate({
1391
+ prompt: opts.prompt,
1392
+ dir: opts.dir,
1393
+ provider: opts.provider,
1394
+ apiKey: opts.apiKey,
1395
+ model: opts.model,
1396
+ apiUrl: opts.apiUrl,
1397
+ force: opts.force
1398
+ });
1399
+ if (result.ok) {
1400
+ console.log("Generated files:");
1401
+ for (const name of Object.keys(result.files || {})) {
1402
+ console.log(` + ${name}`);
1403
+ }
1404
+ if (result.explanation) {
1405
+ console.log(`
1406
+ ${result.explanation}`);
1407
+ }
1408
+ console.log(`
1409
+ Provider: ${result.provider}`);
1410
+ console.log("\nNext steps:");
1411
+ console.log(" keystone validate # Check safety");
1412
+ console.log(" keystone dev # Preview locally");
1413
+ console.log(" keystone publish # Deploy to marketplace\n");
1414
+ } else {
1415
+ console.error(`
1416
+ Generation failed: ${result.error}`);
1417
+ process.exit(1);
1418
+ }
1419
+ } catch (err) {
1420
+ console.error(err instanceof Error ? err.message : err);
1421
+ process.exit(1);
1422
+ }
1423
+ });
255
1424
  program.command("validate [dir]").description("Validate Mini-App against Glass Safety Standard (Ouroboros Loop)").option("--suggest", "Output suggested fixes for each error").action((dir = ".", opts) => {
256
1425
  const result = runValidate(dir, { suggest: opts.suggest });
257
1426
  if (result.ok) {
@@ -304,4 +1473,132 @@ program.command("build [dir]").description("Build Mini-App (optional Arweave col
304
1473
  process.exit(1);
305
1474
  }
306
1475
  });
1476
+ program.command("publish [dir]").description("Publish Mini-App: Gatekeeper \u2192 Arweave \u2192 On-Chain Registry").requiredOption("-n, --name <name>", "App name").requiredOption("-d, --description <desc>", "App description").requiredOption("-w, --wallet <address>", "Creator wallet address").option("--private-key <key>", "Base58 private key for signing (Arweave + Solana)").option("--cluster <cluster>", "Solana cluster: devnet or mainnet-beta", "devnet").option("--api-url <url>", "Keystone OS API URL (e.g. https://keystone.example.com)").option("-c, --category <cat>", "Category (default: utility)", "utility").option("--skip-arweave", "Skip Arweave cold path upload").option("--register-marketplace", "Register on KeystoneMarket").option("--price-usdc <cents>", "Price in USDC cents", (v) => parseInt(v, 10)).action(async (dir = ".", opts) => {
1477
+ try {
1478
+ const result = await runPublish({
1479
+ dir,
1480
+ name: opts.name,
1481
+ description: opts.description,
1482
+ creatorWallet: opts.wallet,
1483
+ privateKey: opts.privateKey,
1484
+ cluster: opts.cluster,
1485
+ apiUrl: opts.apiUrl,
1486
+ category: opts.category,
1487
+ skipArweave: opts.skipArweave,
1488
+ registerMarketplace: opts.registerMarketplace,
1489
+ priceUsdc: opts.priceUsdc
1490
+ });
1491
+ if (result.ok) {
1492
+ console.log("\nPublished to Keystone OS!");
1493
+ console.log(`
1494
+ App ID: ${result.appId}`);
1495
+ if (result.arweaveTxId) console.log(` Arweave: https://arweave.net/${result.arweaveTxId}`);
1496
+ if (result.codeHash) console.log(` Hash: ${result.codeHash}`);
1497
+ if (result.securityScore !== void 0) console.log(` Security: ${result.securityScore}/100`);
1498
+ if (result.solanaTxId) console.log(` Solana TX: ${result.solanaTxId}`);
1499
+ if (result.explorerUrl) console.log(` Explorer: ${result.explorerUrl}`);
1500
+ console.log("\n");
1501
+ } else {
1502
+ console.error(`
1503
+ Publish Failed:`);
1504
+ console.error(result.error);
1505
+ process.exit(1);
1506
+ }
1507
+ } catch (err) {
1508
+ console.error(err instanceof Error ? err.message : err);
1509
+ process.exit(1);
1510
+ }
1511
+ });
1512
+ program.command("deploy [dir]").description("Deploy compiled Solana program to devnet/mainnet").option("--cluster <cluster>", "Target cluster: devnet, mainnet-beta, testnet, localnet", "devnet").option("--program-name <name>", "Program name (default: keystone_app)", "keystone_app").option("--keypair <path>", "Deployer keypair path").option("--program-keypair <path>", "Program keypair for deterministic address").option("--so-path <path>", "Path to compiled .so file").option("--skip-idl", "Skip IDL upload").action(async (dir = ".", opts) => {
1513
+ try {
1514
+ console.log(`
1515
+ Deploying to ${opts.cluster || "devnet"}...
1516
+ `);
1517
+ const result = await runDeploy({
1518
+ dir,
1519
+ cluster: opts.cluster,
1520
+ programName: opts.programName,
1521
+ keypair: opts.keypair,
1522
+ programKeypair: opts.programKeypair,
1523
+ soPath: opts.soPath,
1524
+ skipIdl: opts.skipIdl
1525
+ });
1526
+ if (result.ok) {
1527
+ console.log("\nDeployment successful!");
1528
+ console.log(` Program ID: ${result.programId}`);
1529
+ console.log(` Cluster: ${result.cluster}`);
1530
+ if (result.txSignature) console.log(` Tx: ${result.txSignature}`);
1531
+ if (result.idlUploaded) console.log(" IDL: Uploaded");
1532
+ console.log(`
1533
+ Explorer: https://explorer.solana.com/address/${result.programId}?cluster=${result.cluster}
1534
+ `);
1535
+ } else {
1536
+ console.error(`
1537
+ Deploy failed: ${result.error}`);
1538
+ process.exit(1);
1539
+ }
1540
+ } catch (err) {
1541
+ console.error(err instanceof Error ? err.message : err);
1542
+ process.exit(1);
1543
+ }
1544
+ });
1545
+ program.command("ship [dir]").description("Ship mini-app to marketplace (validate + build + publish in one step)").option("-n, --name <name>", "App name (overrides config)").option("-d, --description <desc>", "App description (overrides config)").option("-w, --wallet <address>", "Creator wallet (overrides config)").option("--private-key <key>", "Base58 private key for signing").option("--cluster <cluster>", "Solana cluster: devnet or mainnet-beta").option("--skip-arweave", "Skip Arweave upload").option("-y, --yes", "Skip confirmation prompts").action(async (dir = ".", opts) => {
1546
+ try {
1547
+ const result = await runShip({ dir, ...opts });
1548
+ if (result.ok) {
1549
+ console.log("\n Shipped to Keystone Marketplace!");
1550
+ console.log(`
1551
+ App ID: ${result.appId}`);
1552
+ if (result.arweaveTxId) console.log(` Arweave: https://arweave.net/${result.arweaveTxId}`);
1553
+ if (result.codeHash) console.log(` Hash: ${result.codeHash}`);
1554
+ if (result.securityScore !== void 0) console.log(` Security: ${result.securityScore}/100`);
1555
+ if (result.solanaTxId) console.log(` Solana: ${result.solanaTxId}`);
1556
+ if (result.explorerUrl) console.log(` Explorer: ${result.explorerUrl}`);
1557
+ console.log("\n");
1558
+ } else {
1559
+ console.error(`
1560
+ Ship failed:
1561
+ ${result.error}`);
1562
+ process.exit(1);
1563
+ }
1564
+ } catch (err) {
1565
+ console.error(err instanceof Error ? err.message : err);
1566
+ process.exit(1);
1567
+ }
1568
+ });
1569
+ program.command("config [dir]").description("Create or edit keystone.config.json").option("-n, --name <name>", "Set app name").option("-w, --wallet <address>", "Set wallet address").option("--cluster <cluster>", "Set cluster").option("--provider <provider>", "Set AI provider").option("--show", "Display current config").action((dir = ".", opts) => {
1570
+ const config = loadConfig(dir);
1571
+ if (opts.show) {
1572
+ console.log("\n keystone.config.json:\n");
1573
+ console.log(JSON.stringify(config, null, 2));
1574
+ console.log("");
1575
+ return;
1576
+ }
1577
+ if (opts.name) config.name = opts.name;
1578
+ if (opts.wallet) config.wallet = opts.wallet;
1579
+ if (opts.cluster) config.cluster = opts.cluster;
1580
+ if (opts.provider) config.provider = opts.provider;
1581
+ saveConfig(config, dir);
1582
+ console.log("\n Config saved to keystone.config.json\n");
1583
+ });
1584
+ program.command("status [dir]").description("Show project info and publish state").action((dir = ".") => {
1585
+ const config = loadConfig(dir);
1586
+ const path12 = require("path");
1587
+ const fs12 = require("fs");
1588
+ const targetDir = path12.resolve(process.cwd(), dir);
1589
+ const hasApp = fs12.existsSync(path12.join(targetDir, "App.tsx"));
1590
+ const hasBundle = fs12.existsSync(path12.join(targetDir, ".keystone", "dist", "app.bundle.js"));
1591
+ const hasLock = fs12.existsSync(path12.join(targetDir, "keystone.lock.json"));
1592
+ console.log("\n Keystone Project Status\n");
1593
+ console.log(` Name: ${config.name || "(not set)"}`);
1594
+ console.log(` Wallet: ${config.wallet || "(not set)"}`);
1595
+ console.log(` Cluster: ${config.cluster || "devnet"}`);
1596
+ console.log(` Category: ${config.category || "utility"}`);
1597
+ console.log(` Provider: ${config.provider || "(not set)"}`);
1598
+ console.log("");
1599
+ console.log(` App.tsx: ${hasApp ? "Found" : "Missing (run keystone init)"}`);
1600
+ console.log(` Bundle: ${hasBundle ? "Built" : "Not built (run keystone build)"}`);
1601
+ console.log(` Lockfile: ${hasLock ? "Valid" : "Missing"}`);
1602
+ console.log("");
1603
+ });
307
1604
  program.parse();