@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.
- package/README.md +15 -0
- package/dist/index.js +1316 -19
- 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 {
|
|
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,
|
|
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="
|
|
40
|
-
|
|
41
|
-
<div className="
|
|
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
|
|
44
|
-
|
|
45
|
-
|
|
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
|
-
`#
|
|
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
|
-
|
|
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(`
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
console.log("
|
|
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();
|