@keystone-os/cli 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +52 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +307 -0
- package/package.json +29 -0
package/README.md
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
# @keystone-os/cli
|
|
2
|
+
|
|
3
|
+
CLI for Keystone Studio Mini-Apps — **Sovereign OS 2026**. Scaffold, validate, and build with Ouroboros self-correction and Arweave cold path.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install -g @keystone-os/cli
|
|
9
|
+
# or
|
|
10
|
+
npx @keystone-os/cli init
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## Commands
|
|
14
|
+
|
|
15
|
+
### init [dir]
|
|
16
|
+
|
|
17
|
+
Scaffold a new Mini-App.
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
keystone init my-app
|
|
21
|
+
npx @keystone-os/cli init my-app
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
### validate [dir] (Ouroboros Loop)
|
|
25
|
+
|
|
26
|
+
Validate against Glass Safety Standard. Use `--suggest` for self-correction hints.
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
keystone validate --suggest
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
- Direct `fetch()` → use `useFetch()` from SDK
|
|
33
|
+
- `localStorage` / `eval` / etc. → blocked
|
|
34
|
+
- **Pinned Import Maps** — enforces `?external=react,react-dom` for esm.sh
|
|
35
|
+
|
|
36
|
+
### lockfile [dir]
|
|
37
|
+
|
|
38
|
+
Validate pinned import maps only.
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
keystone lockfile
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
### build [dir]
|
|
45
|
+
|
|
46
|
+
Build Mini-App. Optional Arweave cold path for atomic rollbacks.
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
keystone build
|
|
50
|
+
keystone build --anchor-arweave
|
|
51
|
+
keystone build -o ./dist
|
|
52
|
+
```
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,307 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
#!/usr/bin/env node
|
|
3
|
+
"use strict";
|
|
4
|
+
var __create = Object.create;
|
|
5
|
+
var __defProp = Object.defineProperty;
|
|
6
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
7
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
8
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
9
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
19
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
20
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
21
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
22
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
23
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
24
|
+
mod
|
|
25
|
+
));
|
|
26
|
+
|
|
27
|
+
// src/index.ts
|
|
28
|
+
var import_commander = require("commander");
|
|
29
|
+
|
|
30
|
+
// src/commands/init.ts
|
|
31
|
+
var fs = __toESM(require("fs"));
|
|
32
|
+
var path = __toESM(require("path"));
|
|
33
|
+
var STARTER_APP = `import { useVault, useFetch } from '@keystone-os/sdk';
|
|
34
|
+
|
|
35
|
+
export default function App() {
|
|
36
|
+
const { tokens, balances } = useVault();
|
|
37
|
+
|
|
38
|
+
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">
|
|
42
|
+
{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>
|
|
46
|
+
</div>
|
|
47
|
+
))}
|
|
48
|
+
</div>
|
|
49
|
+
</div>
|
|
50
|
+
);
|
|
51
|
+
}
|
|
52
|
+
`;
|
|
53
|
+
var LOCKFILE = {
|
|
54
|
+
version: "1.0.0",
|
|
55
|
+
packages: {
|
|
56
|
+
react: {
|
|
57
|
+
url: "https://esm.sh/react@19.0.0",
|
|
58
|
+
types: "https://esm.sh/v135/@types/react@19.0.0/index.d.ts",
|
|
59
|
+
external: true
|
|
60
|
+
},
|
|
61
|
+
"react-dom": {
|
|
62
|
+
url: "https://esm.sh/react-dom@19.0.0",
|
|
63
|
+
types: "https://esm.sh/v135/@types/react-dom@19.0.0/index.d.ts",
|
|
64
|
+
external: true
|
|
65
|
+
},
|
|
66
|
+
"@keystone-os/sdk": {
|
|
67
|
+
url: "https://esm.sh/@keystone-os/sdk",
|
|
68
|
+
types: "https://esm.sh/@keystone-os/sdk",
|
|
69
|
+
external: false
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
};
|
|
73
|
+
function runInit(dir) {
|
|
74
|
+
const targetDir = path.resolve(process.cwd(), dir || ".");
|
|
75
|
+
if (fs.existsSync(targetDir) && fs.readdirSync(targetDir).length > 0) {
|
|
76
|
+
throw new Error(`Directory ${targetDir} is not empty.`);
|
|
77
|
+
}
|
|
78
|
+
fs.mkdirSync(targetDir, { recursive: true });
|
|
79
|
+
fs.writeFileSync(path.join(targetDir, "App.tsx"), STARTER_APP);
|
|
80
|
+
fs.writeFileSync(
|
|
81
|
+
path.join(targetDir, "keystone.lock.json"),
|
|
82
|
+
JSON.stringify(LOCKFILE, null, 2)
|
|
83
|
+
);
|
|
84
|
+
fs.writeFileSync(
|
|
85
|
+
path.join(targetDir, "README.md"),
|
|
86
|
+
`# Keystone Mini-App
|
|
87
|
+
|
|
88
|
+
Built with \`@keystone-os/sdk\`. Open in Keystone Studio to run.
|
|
89
|
+
`
|
|
90
|
+
);
|
|
91
|
+
console.log(`Created Mini-App in ${targetDir}`);
|
|
92
|
+
console.log(" - App.tsx");
|
|
93
|
+
console.log(" - keystone.lock.json");
|
|
94
|
+
console.log(" - README.md");
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// src/commands/validate.ts
|
|
98
|
+
var fs2 = __toESM(require("fs"));
|
|
99
|
+
var path2 = __toESM(require("path"));
|
|
100
|
+
var FORBIDDEN_PATTERNS = [
|
|
101
|
+
{ pattern: /\bfetch\s*\(/g, msg: "Direct fetch() is blocked. Use useFetch() from '@keystone-os/sdk'." },
|
|
102
|
+
{ pattern: /\blocalStorage\b/g, msg: "localStorage is blocked in sandbox." },
|
|
103
|
+
{ pattern: /\bsessionStorage\b/g, msg: "sessionStorage is blocked in sandbox." },
|
|
104
|
+
{ pattern: /\bdocument\.cookie\b/g, msg: "document.cookie is blocked in sandbox." },
|
|
105
|
+
{ pattern: /\bwindow\.parent\.postMessage\b/g, msg: "window.parent.postMessage is reserved for SDK." },
|
|
106
|
+
{ pattern: /\beval\s*\(/g, msg: "eval() is blocked by CSP." },
|
|
107
|
+
{ pattern: /\bnew\s+Function\s*\(/g, msg: "new Function() is blocked by CSP." }
|
|
108
|
+
];
|
|
109
|
+
function getSuggestion(error) {
|
|
110
|
+
if (error.message.includes("fetch()")) {
|
|
111
|
+
return `Replace fetch(url) with: const { data } = useFetch(url);`;
|
|
112
|
+
}
|
|
113
|
+
if (error.message.includes("localStorage")) {
|
|
114
|
+
return `Use useEncryptedSecret() from '@keystone-os/sdk' for persistent storage.`;
|
|
115
|
+
}
|
|
116
|
+
if (error.message.includes("sessionStorage")) {
|
|
117
|
+
return `Use in-memory state or useEncryptedSecret() from '@keystone-os/sdk'.`;
|
|
118
|
+
}
|
|
119
|
+
if (error.message.includes("document.cookie")) {
|
|
120
|
+
return `Use useSIWS() from '@keystone-os/sdk' for session/auth.`;
|
|
121
|
+
}
|
|
122
|
+
if (error.message.includes("postMessage")) {
|
|
123
|
+
return `Use AppEventBus.emit() from '@keystone-os/sdk' for host communication.`;
|
|
124
|
+
}
|
|
125
|
+
return void 0;
|
|
126
|
+
}
|
|
127
|
+
function runValidate(dir = ".", options) {
|
|
128
|
+
const targetDir = path2.resolve(process.cwd(), dir);
|
|
129
|
+
const errors = [];
|
|
130
|
+
const files = ["App.tsx", "app.tsx"];
|
|
131
|
+
for (const file of files) {
|
|
132
|
+
const filePath = path2.join(targetDir, file);
|
|
133
|
+
if (!fs2.existsSync(filePath)) continue;
|
|
134
|
+
const content = fs2.readFileSync(filePath, "utf-8");
|
|
135
|
+
for (const { pattern, msg } of FORBIDDEN_PATTERNS) {
|
|
136
|
+
const re = new RegExp(pattern.source, pattern.flags);
|
|
137
|
+
let m;
|
|
138
|
+
while ((m = re.exec(content)) !== null) {
|
|
139
|
+
const lineNum = content.slice(0, m.index).split("\n").length;
|
|
140
|
+
const err = { file, line: lineNum, message: msg };
|
|
141
|
+
if (options?.suggest) {
|
|
142
|
+
err.suggestion = getSuggestion(err);
|
|
143
|
+
}
|
|
144
|
+
errors.push(err);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
const hasSdkImport = /from\s+['"]@keystone-os\/sdk['"]/.test(content);
|
|
148
|
+
const hasForbidden = FORBIDDEN_PATTERNS.some((p) => new RegExp(p.pattern.source).test(content));
|
|
149
|
+
if (!hasSdkImport && hasForbidden) {
|
|
150
|
+
const err = {
|
|
151
|
+
file,
|
|
152
|
+
line: 1,
|
|
153
|
+
message: "Use '@keystone-os/sdk' for fetch/vault/turnkey instead of raw APIs."
|
|
154
|
+
};
|
|
155
|
+
if (options?.suggest) {
|
|
156
|
+
err.suggestion = `Add: import { useFetch, useVault } from '@keystone-os/sdk';`;
|
|
157
|
+
}
|
|
158
|
+
errors.push(err);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
const suggestions = options?.suggest ? errors.map((e) => e.suggestion).filter(Boolean) : void 0;
|
|
162
|
+
return { ok: errors.length === 0, errors, suggestions };
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// src/commands/lockfile.ts
|
|
166
|
+
var fs3 = __toESM(require("fs"));
|
|
167
|
+
var path3 = __toESM(require("path"));
|
|
168
|
+
var ESM_SH_REGEX = /^https:\/\/esm\.sh\//;
|
|
169
|
+
var EXTERNAL_PARAM = "external=react,react-dom";
|
|
170
|
+
function validateLockfile(dir = ".") {
|
|
171
|
+
const targetDir = path3.resolve(process.cwd(), dir);
|
|
172
|
+
const lockPath = path3.join(targetDir, "keystone.lock.json");
|
|
173
|
+
const errors = [];
|
|
174
|
+
if (!fs3.existsSync(lockPath)) {
|
|
175
|
+
return { ok: true, errors: [] };
|
|
176
|
+
}
|
|
177
|
+
const raw = fs3.readFileSync(lockPath, "utf-8");
|
|
178
|
+
let data;
|
|
179
|
+
try {
|
|
180
|
+
data = JSON.parse(raw);
|
|
181
|
+
} catch {
|
|
182
|
+
return { ok: false, errors: [{ package: "lockfile", message: "Invalid JSON" }] };
|
|
183
|
+
}
|
|
184
|
+
const packages = data.packages ?? {};
|
|
185
|
+
const skipPackages = /* @__PURE__ */ new Set(["react", "react-dom", "react-dom/client", "@keystone-os/sdk"]);
|
|
186
|
+
for (const [name, pkg] of Object.entries(packages)) {
|
|
187
|
+
if (skipPackages.has(name)) continue;
|
|
188
|
+
const url = pkg.url;
|
|
189
|
+
if (!url || typeof url !== "string") continue;
|
|
190
|
+
if (url.startsWith("blob:") || url.startsWith("file:")) continue;
|
|
191
|
+
if (!ESM_SH_REGEX.test(url)) continue;
|
|
192
|
+
if (!url.includes("?")) {
|
|
193
|
+
errors.push({
|
|
194
|
+
package: name,
|
|
195
|
+
message: `esm.sh URL must include ?external=react,react-dom`,
|
|
196
|
+
fix: `${url}?external=react,react-dom`
|
|
197
|
+
});
|
|
198
|
+
} else if (!url.includes(EXTERNAL_PARAM) && !url.includes("external=")) {
|
|
199
|
+
const sep = url.includes("?") ? "&" : "?";
|
|
200
|
+
errors.push({
|
|
201
|
+
package: name,
|
|
202
|
+
message: `esm.sh URL must include external=react,react-dom`,
|
|
203
|
+
fix: `${url}${sep}external=react,react-dom`
|
|
204
|
+
});
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
return { ok: errors.length === 0, errors };
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// src/commands/build.ts
|
|
211
|
+
var fs4 = __toESM(require("fs"));
|
|
212
|
+
var path4 = __toESM(require("path"));
|
|
213
|
+
async function runBuild(options = {}) {
|
|
214
|
+
const targetDir = path4.resolve(process.cwd(), options.dir ?? ".");
|
|
215
|
+
const outDir = options.outDir ?? path4.join(targetDir, "dist");
|
|
216
|
+
const appPath = path4.join(targetDir, "App.tsx");
|
|
217
|
+
if (!fs4.existsSync(appPath)) {
|
|
218
|
+
return { ok: false, error: "App.tsx not found" };
|
|
219
|
+
}
|
|
220
|
+
const raw = fs4.readFileSync(appPath, "utf-8");
|
|
221
|
+
let bundle = raw;
|
|
222
|
+
fs4.mkdirSync(outDir, { recursive: true });
|
|
223
|
+
const outputPath = path4.join(outDir, "app.bundle.js");
|
|
224
|
+
fs4.writeFileSync(outputPath, bundle);
|
|
225
|
+
const manifest = {
|
|
226
|
+
version: "1.0.0",
|
|
227
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
228
|
+
files: ["app.bundle.js"],
|
|
229
|
+
coldPath: null
|
|
230
|
+
};
|
|
231
|
+
if (options.anchorArweave) {
|
|
232
|
+
manifest.coldPath = `arweave://pending`;
|
|
233
|
+
}
|
|
234
|
+
const manifestPath = path4.join(outDir, "manifest.json");
|
|
235
|
+
fs4.writeFileSync(manifestPath, JSON.stringify(manifest, null, 2));
|
|
236
|
+
return {
|
|
237
|
+
ok: true,
|
|
238
|
+
outputPath,
|
|
239
|
+
manifest: manifestPath,
|
|
240
|
+
arweaveTxId: options.anchorArweave ? void 0 : void 0
|
|
241
|
+
};
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
// src/index.ts
|
|
245
|
+
var program = new import_commander.Command();
|
|
246
|
+
program.name("keystone").description("CLI for Keystone Studio Mini-Apps \u2014 Sovereign OS 2026").version("0.2.0");
|
|
247
|
+
program.command("init [dir]").description("Scaffold a new Mini-App").action((dir) => {
|
|
248
|
+
try {
|
|
249
|
+
runInit(dir);
|
|
250
|
+
} catch (err) {
|
|
251
|
+
console.error(err instanceof Error ? err.message : err);
|
|
252
|
+
process.exit(1);
|
|
253
|
+
}
|
|
254
|
+
});
|
|
255
|
+
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
|
+
const result = runValidate(dir, { suggest: opts.suggest });
|
|
257
|
+
if (result.ok) {
|
|
258
|
+
const lockResult = validateLockfile(dir);
|
|
259
|
+
if (lockResult.ok) {
|
|
260
|
+
console.log("Validation passed.");
|
|
261
|
+
} else {
|
|
262
|
+
for (const e of lockResult.errors) {
|
|
263
|
+
console.error(`[lockfile] ${e.package}: ${e.message}`);
|
|
264
|
+
if (e.fix) console.error(` Fix: ${e.fix}`);
|
|
265
|
+
}
|
|
266
|
+
process.exit(1);
|
|
267
|
+
}
|
|
268
|
+
} else {
|
|
269
|
+
for (const e of result.errors) {
|
|
270
|
+
console.error(`${e.file}:${e.line} \u2014 ${e.message}`);
|
|
271
|
+
if (opts.suggest && e.suggestion) {
|
|
272
|
+
console.error(` Suggestion: ${e.suggestion}`);
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
process.exit(1);
|
|
276
|
+
}
|
|
277
|
+
});
|
|
278
|
+
program.command("lockfile [dir]").description("Validate pinned import maps (?external=react,react-dom)").action((dir = ".") => {
|
|
279
|
+
const result = validateLockfile(dir);
|
|
280
|
+
if (result.ok) {
|
|
281
|
+
console.log("Lockfile valid.");
|
|
282
|
+
} else {
|
|
283
|
+
for (const e of result.errors) {
|
|
284
|
+
console.error(`${e.package}: ${e.message}`);
|
|
285
|
+
if (e.fix) console.error(` Fix: ${e.fix}`);
|
|
286
|
+
}
|
|
287
|
+
process.exit(1);
|
|
288
|
+
}
|
|
289
|
+
});
|
|
290
|
+
program.command("build [dir]").description("Build Mini-App (optional Arweave cold path)").option("--anchor-arweave", "Anchor build to Arweave for atomic rollbacks").option("-o, --out-dir <dir>", "Output directory").action(async (dir = ".", opts) => {
|
|
291
|
+
try {
|
|
292
|
+
const result = await runBuild({ dir, anchorArweave: opts.anchorArweave, outDir: opts.outDir });
|
|
293
|
+
if (result.ok) {
|
|
294
|
+
console.log("Build complete:", result.outputPath);
|
|
295
|
+
if (opts.anchorArweave) {
|
|
296
|
+
console.log("Cold path: manifest.json (Arweave upload requires arweave CLI)");
|
|
297
|
+
}
|
|
298
|
+
} else {
|
|
299
|
+
console.error(result.error);
|
|
300
|
+
process.exit(1);
|
|
301
|
+
}
|
|
302
|
+
} catch (err) {
|
|
303
|
+
console.error(err instanceof Error ? err.message : err);
|
|
304
|
+
process.exit(1);
|
|
305
|
+
}
|
|
306
|
+
});
|
|
307
|
+
program.parse();
|
package/package.json
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@keystone-os/cli",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "CLI for Keystone Studio Mini-Apps — init, validate",
|
|
5
|
+
"main": "dist/index.js",
|
|
6
|
+
"bin": {
|
|
7
|
+
"keystone": "./dist/index.js"
|
|
8
|
+
},
|
|
9
|
+
"files": ["dist"],
|
|
10
|
+
"scripts": {
|
|
11
|
+
"build": "tsup",
|
|
12
|
+
"dev": "tsup src/index.ts --format cjs --dts --watch"
|
|
13
|
+
},
|
|
14
|
+
"dependencies": {
|
|
15
|
+
"commander": "^12.1.0"
|
|
16
|
+
},
|
|
17
|
+
"devDependencies": {
|
|
18
|
+
"@types/node": "^20.0.0",
|
|
19
|
+
"tsup": "^8.3.5",
|
|
20
|
+
"typescript": "^5.8.0"
|
|
21
|
+
},
|
|
22
|
+
"keywords": ["keystone", "web3", "treasury", "studio", "mini-app", "cli"],
|
|
23
|
+
"license": "MIT",
|
|
24
|
+
"repository": {
|
|
25
|
+
"type": "git",
|
|
26
|
+
"url": "https://github.com/stauniverse/keystone-treasury-os",
|
|
27
|
+
"directory": "packages/cli"
|
|
28
|
+
}
|
|
29
|
+
}
|