@tokiui/cli 0.1.1 → 0.2.1
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/dist/index.js +120 -15
- package/package.json +10 -7
package/dist/index.js
CHANGED
|
@@ -233,7 +233,7 @@ var initCommand = new import_commander.Command("init").description("Initialize t
|
|
|
233
233
|
console.log(import_kleur.default.green("\n \u2713 tokiui initialized\n"));
|
|
234
234
|
console.log(` ${import_kleur.default.bold("Add your first component:")}
|
|
235
235
|
`);
|
|
236
|
-
console.log(` ${import_kleur.default.cyan("npx tokiui add button")}
|
|
236
|
+
console.log(` ${import_kleur.default.cyan("npx @tokiui/cli add button")}
|
|
237
237
|
`);
|
|
238
238
|
if (!isNext) {
|
|
239
239
|
console.log(
|
|
@@ -254,21 +254,107 @@ var import_ora2 = __toESM(require("ora"));
|
|
|
254
254
|
var import_execa2 = require("execa");
|
|
255
255
|
|
|
256
256
|
// src/utils/registry.ts
|
|
257
|
-
var
|
|
257
|
+
var ALLOWED_FETCH_ORIGIN = "https://raw.githubusercontent.com/TopherGacad/tokiui/";
|
|
258
|
+
var SAFE_REF_RE = /^[a-zA-Z0-9._/@-]+$/;
|
|
259
|
+
function resolveRegistryRef() {
|
|
260
|
+
const envRef = process.env.TOKIUI_REGISTRY_REF;
|
|
261
|
+
if (envRef !== void 0) {
|
|
262
|
+
if (!SAFE_REF_RE.test(envRef) || envRef.includes("..")) {
|
|
263
|
+
throw new Error(
|
|
264
|
+
`TOKIUI_REGISTRY_REF contains invalid characters: "${envRef}". Only alphanumeric characters, hyphens, dots, slashes, and @ are allowed.`
|
|
265
|
+
);
|
|
266
|
+
}
|
|
267
|
+
return envRef;
|
|
268
|
+
}
|
|
269
|
+
return `cli-v${"0.2.1"}`;
|
|
270
|
+
}
|
|
271
|
+
var REGISTRY_REF = resolveRegistryRef();
|
|
272
|
+
var REGISTRY_BASE = `${ALLOWED_FETCH_ORIGIN}${REGISTRY_REF}/packages/registry`;
|
|
273
|
+
var SOURCE_BASE = `${ALLOWED_FETCH_ORIGIN}${REGISTRY_REF}/packages/ui/src`;
|
|
274
|
+
async function safeFetch(url) {
|
|
275
|
+
if (!url.startsWith(ALLOWED_FETCH_ORIGIN)) {
|
|
276
|
+
throw new Error(
|
|
277
|
+
`Security: refusing to fetch from unexpected origin.
|
|
278
|
+
Expected: ${ALLOWED_FETCH_ORIGIN}...
|
|
279
|
+
Got: ${url}`
|
|
280
|
+
);
|
|
281
|
+
}
|
|
282
|
+
return fetch(url);
|
|
283
|
+
}
|
|
284
|
+
var ALLOWED_SCOPES = /* @__PURE__ */ new Set(["@radix-ui"]);
|
|
285
|
+
var ALLOWED_PACKAGES = /* @__PURE__ */ new Set([
|
|
286
|
+
"class-variance-authority",
|
|
287
|
+
"clsx",
|
|
288
|
+
"tailwind-merge",
|
|
289
|
+
"sonner",
|
|
290
|
+
"lucide-react"
|
|
291
|
+
]);
|
|
292
|
+
var PACKAGE_NAME_RE = /^(@[a-z0-9][a-z0-9-._]*\/[a-z0-9][a-z0-9-._]*)(@[^\s]+)?$|^([a-z0-9][a-z0-9-._]*)(@[^\s]+)?$/;
|
|
293
|
+
function assertSafeDependency(dep) {
|
|
294
|
+
if (!PACKAGE_NAME_RE.test(dep)) {
|
|
295
|
+
throw new Error(
|
|
296
|
+
`Registry returned a malformed dependency name: "${dep}". Aborting install.`
|
|
297
|
+
);
|
|
298
|
+
}
|
|
299
|
+
if (dep.startsWith("@")) {
|
|
300
|
+
const scope = dep.slice(0, dep.indexOf("/"));
|
|
301
|
+
if (!ALLOWED_SCOPES.has(scope)) {
|
|
302
|
+
throw new Error(
|
|
303
|
+
`Dependency scope "${scope}" is not in the approved list. Refusing to install "${dep}". If this is legitimate, open an issue on GitHub.`
|
|
304
|
+
);
|
|
305
|
+
}
|
|
306
|
+
} else {
|
|
307
|
+
const name = dep.split("@")[0];
|
|
308
|
+
if (!ALLOWED_PACKAGES.has(name)) {
|
|
309
|
+
throw new Error(
|
|
310
|
+
`Package "${name}" is not in the approved dependency list. Refusing to install. If this is legitimate, open an issue on GitHub.`
|
|
311
|
+
);
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
function assertSafeFilePath(filePath) {
|
|
316
|
+
if (filePath.includes("..") || filePath.startsWith("/") || /^[A-Za-z]:/.test(filePath) || filePath.includes("\0") || filePath.includes("\r") || filePath.includes("\n")) {
|
|
317
|
+
throw new Error(`Registry returned an unsafe file path: "${filePath}". Aborting.`);
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
function assertSafeComponentName(name) {
|
|
321
|
+
if (!/^[a-z][a-z0-9-]*$/.test(name)) {
|
|
322
|
+
throw new Error(`Invalid component name in registry: "${name}". Aborting.`);
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
function assertRegistryIndex(data) {
|
|
326
|
+
if (typeof data !== "object" || data === null || !Array.isArray(data.components)) {
|
|
327
|
+
throw new Error("Received an invalid registry index from the server.");
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
function assertRegistryComponent(data) {
|
|
331
|
+
const obj = data;
|
|
332
|
+
const valid = typeof data === "object" && data !== null && typeof obj.name === "string" && Array.isArray(obj.files) && Array.isArray(obj.dependencies) && Array.isArray(obj.devDependencies) && Array.isArray(obj.registryDependencies) && obj.files.every((f) => typeof f === "string") && obj.dependencies.every((d) => typeof d === "string") && obj.registryDependencies.every((d) => typeof d === "string");
|
|
333
|
+
if (!valid) {
|
|
334
|
+
throw new Error(
|
|
335
|
+
`Received an invalid component definition for "${String(obj.name ?? "unknown")}" from the registry.`
|
|
336
|
+
);
|
|
337
|
+
}
|
|
338
|
+
}
|
|
258
339
|
async function fetchRegistryIndex() {
|
|
259
|
-
const res = await
|
|
340
|
+
const res = await safeFetch(`${REGISTRY_BASE}/index.json`);
|
|
260
341
|
if (!res.ok) throw new Error(`Failed to fetch registry index: ${res.statusText}`);
|
|
261
|
-
|
|
342
|
+
const data = await res.json();
|
|
343
|
+
assertRegistryIndex(data);
|
|
344
|
+
return data;
|
|
262
345
|
}
|
|
263
346
|
async function fetchComponent(name) {
|
|
264
|
-
|
|
347
|
+
assertSafeComponentName(name);
|
|
348
|
+
const res = await safeFetch(`${REGISTRY_BASE}/components/${name}.json`);
|
|
265
349
|
if (!res.ok) throw new Error(`Component "${name}" not found in registry`);
|
|
266
|
-
|
|
350
|
+
const data = await res.json();
|
|
351
|
+
assertRegistryComponent(data);
|
|
352
|
+
return data;
|
|
267
353
|
}
|
|
268
354
|
async function fetchComponentSource(name, file) {
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
);
|
|
355
|
+
assertSafeComponentName(name);
|
|
356
|
+
assertSafeFilePath(file);
|
|
357
|
+
const res = await safeFetch(`${SOURCE_BASE}/${file}`);
|
|
272
358
|
if (!res.ok) throw new Error(`Failed to fetch source for ${name}/${file}`);
|
|
273
359
|
return res.text();
|
|
274
360
|
}
|
|
@@ -308,23 +394,33 @@ var addCommand = new import_commander2.Command("add").description("Add a compone
|
|
|
308
394
|
console.log(import_kleur2.default.red("Aborted."));
|
|
309
395
|
process.exit(0);
|
|
310
396
|
}
|
|
397
|
+
const visited = /* @__PURE__ */ new Set();
|
|
311
398
|
for (const name of answer.components) {
|
|
312
|
-
await installComponent(name, cwd, config.componentsDir, libAlias);
|
|
399
|
+
await installComponent(name, cwd, config.componentsDir, libAlias, visited);
|
|
313
400
|
}
|
|
314
401
|
return;
|
|
315
402
|
}
|
|
316
|
-
await installComponent(componentName, cwd, config.componentsDir, libAlias);
|
|
403
|
+
await installComponent(componentName, cwd, config.componentsDir, libAlias, /* @__PURE__ */ new Set());
|
|
317
404
|
});
|
|
318
|
-
async function installComponent(name, cwd, componentsDir, libAlias) {
|
|
405
|
+
async function installComponent(name, cwd, componentsDir, libAlias, visited) {
|
|
406
|
+
if (visited.has(name)) return;
|
|
407
|
+
visited.add(name);
|
|
408
|
+
assertSafeComponentName(name);
|
|
319
409
|
const spinner = (0, import_ora2.default)(`Adding ${name}...`).start();
|
|
320
|
-
const meta = await fetchComponent(name).catch(() => {
|
|
321
|
-
spinner.fail(
|
|
410
|
+
const meta = await fetchComponent(name).catch((err) => {
|
|
411
|
+
spinner.fail(`${err instanceof Error ? err.message : String(err)}`);
|
|
322
412
|
process.exit(1);
|
|
323
413
|
});
|
|
324
414
|
for (const dep of meta.registryDependencies) {
|
|
325
|
-
await installComponent(dep, cwd, componentsDir, libAlias);
|
|
415
|
+
await installComponent(dep, cwd, componentsDir, libAlias, visited);
|
|
326
416
|
}
|
|
327
417
|
for (const file of meta.files) {
|
|
418
|
+
try {
|
|
419
|
+
assertSafeFilePath(file);
|
|
420
|
+
} catch (err) {
|
|
421
|
+
spinner.fail(`Security: ${err instanceof Error ? err.message : String(err)}`);
|
|
422
|
+
process.exit(1);
|
|
423
|
+
}
|
|
328
424
|
const source = await fetchComponentSource(name, file);
|
|
329
425
|
const transformed = transformImports(source, libAlias);
|
|
330
426
|
const destPath = import_path3.default.join(cwd, componentsDir, import_path3.default.basename(file));
|
|
@@ -346,6 +442,15 @@ async function installComponent(name, cwd, componentsDir, libAlias) {
|
|
|
346
442
|
await import_fs_extra3.default.writeFile(destPath, transformed);
|
|
347
443
|
}
|
|
348
444
|
if (meta.dependencies.length > 0) {
|
|
445
|
+
for (const dep of meta.dependencies) {
|
|
446
|
+
try {
|
|
447
|
+
assertSafeDependency(dep);
|
|
448
|
+
} catch (err) {
|
|
449
|
+
spinner.fail(`Security: ${err instanceof Error ? err.message : String(err)}`);
|
|
450
|
+
process.exit(1);
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
console.log(import_kleur2.default.dim(` Installing: ${meta.dependencies.join(", ")}`));
|
|
349
454
|
const pm = detectPackageManager(cwd);
|
|
350
455
|
const installCmd = pm === "npm" ? "install" : "add";
|
|
351
456
|
await (0, import_execa2.execa)(pm, [installCmd, ...meta.dependencies], { cwd });
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tokiui/cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.1",
|
|
4
4
|
"description": "CLI for adding tokiui components to your project",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Topher Gacad",
|
|
@@ -21,14 +21,12 @@
|
|
|
21
21
|
"bin": {
|
|
22
22
|
"tokiui": "./dist/index.js"
|
|
23
23
|
},
|
|
24
|
+
"publishConfig": {
|
|
25
|
+
"access": "public"
|
|
26
|
+
},
|
|
24
27
|
"files": [
|
|
25
28
|
"dist"
|
|
26
29
|
],
|
|
27
|
-
"scripts": {
|
|
28
|
-
"build": "tsup",
|
|
29
|
-
"dev": "tsup --watch",
|
|
30
|
-
"lint": "tsc --noEmit"
|
|
31
|
-
},
|
|
32
30
|
"dependencies": {
|
|
33
31
|
"commander": "^12.1.0",
|
|
34
32
|
"execa": "^9.3.0",
|
|
@@ -43,5 +41,10 @@
|
|
|
43
41
|
"@types/prompts": "^2.4.9",
|
|
44
42
|
"tsup": "^8.2.4",
|
|
45
43
|
"typescript": "^5.5.4"
|
|
44
|
+
},
|
|
45
|
+
"scripts": {
|
|
46
|
+
"build": "tsup",
|
|
47
|
+
"dev": "tsup --watch",
|
|
48
|
+
"lint": "tsc --noEmit"
|
|
46
49
|
}
|
|
47
|
-
}
|
|
50
|
+
}
|