@overscore/cli 0.13.1 → 0.13.3
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 +175 -10
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -31,6 +31,28 @@ function parseEnv(content) {
|
|
|
31
31
|
}
|
|
32
32
|
return env;
|
|
33
33
|
}
|
|
34
|
+
/**
|
|
35
|
+
* Only let credentialed requests (API key + deploy/source bundle) target
|
|
36
|
+
* overscore.dev or localhost. Prevents a poisoned project .env
|
|
37
|
+
* (VITE_OVERSCORE_API_URL) from redirecting the key + uploaded bundle to an
|
|
38
|
+
* attacker host (audit L6). Falls back to the canonical Hub on anything else.
|
|
39
|
+
*/
|
|
40
|
+
function safeApiUrl(apiUrl) {
|
|
41
|
+
try {
|
|
42
|
+
const h = new URL(apiUrl).hostname;
|
|
43
|
+
if (h === "overscore.dev" ||
|
|
44
|
+
h.endsWith(".overscore.dev") ||
|
|
45
|
+
h === "localhost" ||
|
|
46
|
+
h === "127.0.0.1") {
|
|
47
|
+
return apiUrl;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
catch {
|
|
51
|
+
/* invalid URL */
|
|
52
|
+
}
|
|
53
|
+
console.warn(` Ignoring untrusted API URL "${apiUrl}"; using ${HUB_URL}/api`);
|
|
54
|
+
return `${HUB_URL}/api`;
|
|
55
|
+
}
|
|
34
56
|
async function loadEnv() {
|
|
35
57
|
// (1) Local .env (inside a dashboard project)
|
|
36
58
|
const envPath = path.resolve(process.cwd(), ".env");
|
|
@@ -41,7 +63,7 @@ async function loadEnv() {
|
|
|
41
63
|
const dashboardSlug = env.VITE_OVERSCORE_DASHBOARD_SLUG || path.basename(process.cwd());
|
|
42
64
|
const projectSlug = env.VITE_OVERSCORE_PROJECT_SLUG || "";
|
|
43
65
|
if (apiUrl && apiKey) {
|
|
44
|
-
return { apiUrl, apiKey, dashboardSlug, projectSlug };
|
|
66
|
+
return { apiUrl: safeApiUrl(apiUrl), apiKey, dashboardSlug, projectSlug };
|
|
45
67
|
}
|
|
46
68
|
// .env has project context but no API key — generate one via device token
|
|
47
69
|
if (projectSlug) {
|
|
@@ -58,7 +80,7 @@ async function loadEnv() {
|
|
|
58
80
|
: envContent + `VITE_OVERSCORE_API_KEY=${newKey}\n`;
|
|
59
81
|
fs.writeFileSync(envPath, updated);
|
|
60
82
|
return {
|
|
61
|
-
apiUrl: apiUrl || `${HUB_URL}/api
|
|
83
|
+
apiUrl: safeApiUrl(apiUrl || `${HUB_URL}/api`),
|
|
62
84
|
apiKey: newKey,
|
|
63
85
|
dashboardSlug,
|
|
64
86
|
projectSlug,
|
|
@@ -73,7 +95,7 @@ async function loadEnv() {
|
|
|
73
95
|
const cfg = parseEnv(fs.readFileSync(configPath, "utf-8"));
|
|
74
96
|
if (cfg.OVERSCORE_API_KEY) {
|
|
75
97
|
return {
|
|
76
|
-
apiUrl: cfg.OVERSCORE_API_URL || `${HUB_URL}/api
|
|
98
|
+
apiUrl: safeApiUrl(cfg.OVERSCORE_API_URL || `${HUB_URL}/api`),
|
|
77
99
|
apiKey: cfg.OVERSCORE_API_KEY,
|
|
78
100
|
dashboardSlug: path.basename(process.cwd()),
|
|
79
101
|
projectSlug: cfg.OVERSCORE_PROJECT_SLUG || "",
|
|
@@ -84,7 +106,7 @@ async function loadEnv() {
|
|
|
84
106
|
const newKey = await refreshApiKey(cfg.OVERSCORE_PROJECT_SLUG);
|
|
85
107
|
if (newKey) {
|
|
86
108
|
return {
|
|
87
|
-
apiUrl: cfg.OVERSCORE_API_URL || `${HUB_URL}/api
|
|
109
|
+
apiUrl: safeApiUrl(cfg.OVERSCORE_API_URL || `${HUB_URL}/api`),
|
|
88
110
|
apiKey: newKey,
|
|
89
111
|
dashboardSlug: path.basename(process.cwd()),
|
|
90
112
|
projectSlug: cfg.OVERSCORE_PROJECT_SLUG,
|
|
@@ -100,7 +122,7 @@ async function loadEnv() {
|
|
|
100
122
|
const cfg2 = parseEnv(fs.readFileSync(configPath2, "utf-8"));
|
|
101
123
|
if (cfg2.OVERSCORE_API_KEY) {
|
|
102
124
|
return {
|
|
103
|
-
apiUrl: cfg2.OVERSCORE_API_URL || `${HUB_URL}/api
|
|
125
|
+
apiUrl: safeApiUrl(cfg2.OVERSCORE_API_URL || `${HUB_URL}/api`),
|
|
104
126
|
apiKey: cfg2.OVERSCORE_API_KEY,
|
|
105
127
|
dashboardSlug: path.basename(process.cwd()),
|
|
106
128
|
projectSlug: cfg2.OVERSCORE_PROJECT_SLUG || "",
|
|
@@ -284,6 +306,23 @@ function isSecretFileName(name) {
|
|
|
284
306
|
return true;
|
|
285
307
|
return false;
|
|
286
308
|
}
|
|
309
|
+
/**
|
|
310
|
+
* Resolve `relPath` under `baseDir`, returning the absolute path only if it
|
|
311
|
+
* stays inside `baseDir`. Knowledge-file paths come from the Hub (tenant-
|
|
312
|
+
* controlled) and are written to disk on pull/scaffold, so they MUST be
|
|
313
|
+
* contained — otherwise `../rules/data-fetching.md` could overwrite an
|
|
314
|
+
* auto-loaded Claude Code rule on a teammate's machine (audit H6).
|
|
315
|
+
*/
|
|
316
|
+
function safeJoinWithin(baseDir, relPath) {
|
|
317
|
+
if (typeof relPath !== "string" || relPath.length === 0 || relPath.includes("\0")) {
|
|
318
|
+
return null;
|
|
319
|
+
}
|
|
320
|
+
const base = path.resolve(baseDir);
|
|
321
|
+
const resolved = path.resolve(base, relPath);
|
|
322
|
+
if (resolved !== base && !resolved.startsWith(base + path.sep))
|
|
323
|
+
return null;
|
|
324
|
+
return resolved;
|
|
325
|
+
}
|
|
287
326
|
/**
|
|
288
327
|
* Walk a directory collecting deployable source files. Excludes machine-state
|
|
289
328
|
* and secrets, NEVER follows symlinks (a symlink could point at ~/.ssh/id_rsa
|
|
@@ -1068,7 +1107,7 @@ async function loadAnalysisConfig() {
|
|
|
1068
1107
|
const apiKey = env.VITE_OVERSCORE_API_KEY;
|
|
1069
1108
|
const projectSlug = env.VITE_OVERSCORE_PROJECT_SLUG;
|
|
1070
1109
|
if (apiUrl && apiKey && projectSlug) {
|
|
1071
|
-
return { apiUrl, apiKey, projectSlug };
|
|
1110
|
+
return { apiUrl: safeApiUrl(apiUrl), apiKey, projectSlug };
|
|
1072
1111
|
}
|
|
1073
1112
|
}
|
|
1074
1113
|
// (2) ~/.overscore/config
|
|
@@ -1086,7 +1125,7 @@ async function loadAnalysisConfig() {
|
|
|
1086
1125
|
});
|
|
1087
1126
|
if (checkRes.ok) {
|
|
1088
1127
|
return {
|
|
1089
|
-
apiUrl: cfg.OVERSCORE_API_URL || `${HUB_URL}/api
|
|
1128
|
+
apiUrl: safeApiUrl(cfg.OVERSCORE_API_URL || `${HUB_URL}/api`),
|
|
1090
1129
|
apiKey: cfg.OVERSCORE_API_KEY,
|
|
1091
1130
|
projectSlug: cfg.OVERSCORE_PROJECT_SLUG,
|
|
1092
1131
|
};
|
|
@@ -1096,7 +1135,7 @@ async function loadAnalysisConfig() {
|
|
|
1096
1135
|
catch {
|
|
1097
1136
|
// Can't reach the hub — use saved config and hope for the best
|
|
1098
1137
|
return {
|
|
1099
|
-
apiUrl: cfg.OVERSCORE_API_URL || `${HUB_URL}/api
|
|
1138
|
+
apiUrl: safeApiUrl(cfg.OVERSCORE_API_URL || `${HUB_URL}/api`),
|
|
1100
1139
|
apiKey: cfg.OVERSCORE_API_KEY,
|
|
1101
1140
|
projectSlug: cfg.OVERSCORE_PROJECT_SLUG,
|
|
1102
1141
|
};
|
|
@@ -1264,9 +1303,15 @@ async function syncKnowledgeBase(targetDir, baseUrl, projectSlug, apiKey) {
|
|
|
1264
1303
|
if (!data.files || data.files.length === 0)
|
|
1265
1304
|
return false;
|
|
1266
1305
|
fs.mkdirSync(knowledgeDir, { recursive: true });
|
|
1267
|
-
// Write each knowledge file
|
|
1306
|
+
// Write each knowledge file — contained within knowledgeDir (audit H6).
|
|
1268
1307
|
for (const file of data.files) {
|
|
1269
|
-
|
|
1308
|
+
const dest = safeJoinWithin(knowledgeDir, file.path);
|
|
1309
|
+
if (!dest) {
|
|
1310
|
+
console.warn(` Skipped knowledge file with unsafe path: ${file.path}`);
|
|
1311
|
+
continue;
|
|
1312
|
+
}
|
|
1313
|
+
fs.mkdirSync(path.dirname(dest), { recursive: true });
|
|
1314
|
+
fs.writeFileSync(dest, file.content);
|
|
1270
1315
|
}
|
|
1271
1316
|
// Generate INDEX.md
|
|
1272
1317
|
const indexLines = [
|
|
@@ -1777,8 +1822,16 @@ function collectAnalysisFiles(dir, prefix, patterns) {
|
|
|
1777
1822
|
const userPatterns = patterns ?? loadOverscoreIgnore(dir);
|
|
1778
1823
|
const files = [];
|
|
1779
1824
|
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
1825
|
+
// Never follow symlinks or ship secrets — same guards as the deploy path's
|
|
1826
|
+
// collectSourceFiles (audit H5: this analysis-publish collector had neither,
|
|
1827
|
+
// so a poisoned agent could upload service-account.json / id_rsa, or a
|
|
1828
|
+
// file-target symlink to ~/.ssh/id_rsa).
|
|
1829
|
+
if (entry.isSymbolicLink())
|
|
1830
|
+
continue;
|
|
1780
1831
|
if (isBaselineExcluded(entry.name))
|
|
1781
1832
|
continue;
|
|
1833
|
+
if (isSecretFileName(entry.name))
|
|
1834
|
+
continue;
|
|
1782
1835
|
const relativePath = prefix ? `${prefix}/${entry.name}` : entry.name;
|
|
1783
1836
|
if (isIgnored(relativePath, entry.isDirectory(), userPatterns))
|
|
1784
1837
|
continue;
|
|
@@ -2332,6 +2385,17 @@ async function dev() {
|
|
|
2332
2385
|
await cleanup();
|
|
2333
2386
|
process.exit(0);
|
|
2334
2387
|
});
|
|
2388
|
+
// Also clean up on terminal close (SIGHUP) and on a crash, so the short-lived
|
|
2389
|
+
// dev key isn't stranded live when the tab closes or the process throws (M12).
|
|
2390
|
+
process.on("SIGHUP", async () => {
|
|
2391
|
+
await cleanup();
|
|
2392
|
+
process.exit(0);
|
|
2393
|
+
});
|
|
2394
|
+
process.on("uncaughtException", async (err) => {
|
|
2395
|
+
await cleanup();
|
|
2396
|
+
console.error(err);
|
|
2397
|
+
process.exit(1);
|
|
2398
|
+
});
|
|
2335
2399
|
}
|
|
2336
2400
|
// ── auth ────────────────────────────────────────────────────────────
|
|
2337
2401
|
async function authStatus() {
|
|
@@ -2520,6 +2584,90 @@ ${installed.map((s) => ` - ${s}`).join("\n")}
|
|
|
2520
2584
|
Re-run this command anytime to update.
|
|
2521
2585
|
`);
|
|
2522
2586
|
}
|
|
2587
|
+
// ── template pull ──────────────────────────────────────────────────
|
|
2588
|
+
async function templatePull(slug) {
|
|
2589
|
+
if (!slug) {
|
|
2590
|
+
console.error("\n Usage: npx @overscore/cli template pull <template-slug> [dir]\n");
|
|
2591
|
+
console.error(" Example:");
|
|
2592
|
+
console.error(" npx @overscore/cli template pull mrr-tracker\n");
|
|
2593
|
+
process.exit(1);
|
|
2594
|
+
}
|
|
2595
|
+
const targetDir = path.resolve(process.cwd(), process.argv[4] || slug);
|
|
2596
|
+
const url = `https://overscore.dev/api/templates/${slug}/source`;
|
|
2597
|
+
console.log(`\n Pulling template: ${slug}...`);
|
|
2598
|
+
let res;
|
|
2599
|
+
try {
|
|
2600
|
+
res = await fetch(url, {
|
|
2601
|
+
// Optional: attach device token if available, for attribution in install counting
|
|
2602
|
+
headers: (() => {
|
|
2603
|
+
const configPath = path.join(process.env.HOME || "", ".overscore", "config");
|
|
2604
|
+
if (fs.existsSync(configPath)) {
|
|
2605
|
+
const cfg = parseEnv(fs.readFileSync(configPath, "utf-8"));
|
|
2606
|
+
if (cfg.OVERSCORE_DEVICE_TOKEN) {
|
|
2607
|
+
return { Authorization: `Bearer ${cfg.OVERSCORE_DEVICE_TOKEN}` };
|
|
2608
|
+
}
|
|
2609
|
+
}
|
|
2610
|
+
return {};
|
|
2611
|
+
})(),
|
|
2612
|
+
});
|
|
2613
|
+
}
|
|
2614
|
+
catch {
|
|
2615
|
+
console.error(`\n Error: Could not reach overscore.dev\n`);
|
|
2616
|
+
process.exit(1);
|
|
2617
|
+
}
|
|
2618
|
+
if (!res.ok) {
|
|
2619
|
+
let errorMsg = `HTTP ${res.status}`;
|
|
2620
|
+
try {
|
|
2621
|
+
const body = await res.json();
|
|
2622
|
+
errorMsg = body.error || errorMsg;
|
|
2623
|
+
}
|
|
2624
|
+
catch {
|
|
2625
|
+
//
|
|
2626
|
+
}
|
|
2627
|
+
console.error(`\n Error: ${errorMsg}\n`);
|
|
2628
|
+
process.exit(1);
|
|
2629
|
+
}
|
|
2630
|
+
const templateSlug = res.headers.get("X-OS-Template-Slug") || slug;
|
|
2631
|
+
const templateVersion = res.headers.get("X-OS-Template-Version") || "0";
|
|
2632
|
+
const zipBuffer = Buffer.from(await res.arrayBuffer());
|
|
2633
|
+
// Confirm overwrite if dir exists
|
|
2634
|
+
if (fs.existsSync(targetDir)) {
|
|
2635
|
+
const yes = await confirm(`Directory "${path.basename(targetDir)}" already exists. Overwrite?`);
|
|
2636
|
+
if (!yes) {
|
|
2637
|
+
console.log(" Cancelled.\n");
|
|
2638
|
+
return;
|
|
2639
|
+
}
|
|
2640
|
+
for (const entry of fs.readdirSync(targetDir)) {
|
|
2641
|
+
if (entry === "node_modules")
|
|
2642
|
+
continue;
|
|
2643
|
+
fs.rmSync(path.join(targetDir, entry), { recursive: true, force: true });
|
|
2644
|
+
}
|
|
2645
|
+
}
|
|
2646
|
+
else {
|
|
2647
|
+
fs.mkdirSync(targetDir, { recursive: true });
|
|
2648
|
+
}
|
|
2649
|
+
// Extract zip
|
|
2650
|
+
await extractZip(zipBuffer, targetDir);
|
|
2651
|
+
// Write provenance: templates don't have .env (unbound to a project)
|
|
2652
|
+
// Instead, .overscore/template.json records the pull metadata
|
|
2653
|
+
const oscoreDir = path.join(targetDir, ".overscore");
|
|
2654
|
+
fs.mkdirSync(oscoreDir, { recursive: true });
|
|
2655
|
+
fs.writeFileSync(path.join(oscoreDir, "template.json"), JSON.stringify({
|
|
2656
|
+
template_slug: templateSlug,
|
|
2657
|
+
template_version: templateVersion,
|
|
2658
|
+
pulled_at: new Date().toISOString(),
|
|
2659
|
+
}, null, 2));
|
|
2660
|
+
console.log(`
|
|
2661
|
+
✓ Template extracted to: ${path.relative(process.cwd(), targetDir)}
|
|
2662
|
+
|
|
2663
|
+
Next steps:
|
|
2664
|
+
cd ${path.relative(process.cwd(), targetDir)}
|
|
2665
|
+
npm install
|
|
2666
|
+
npx @overscore/cli dev
|
|
2667
|
+
|
|
2668
|
+
This template runs on mock data — no warehouse connection or API key needed.
|
|
2669
|
+
`);
|
|
2670
|
+
}
|
|
2523
2671
|
// ── Routing ─────────────────────────────────────────────────────────
|
|
2524
2672
|
const command = process.argv[2];
|
|
2525
2673
|
const subcommand = process.argv[3];
|
|
@@ -2552,6 +2700,23 @@ else if (command === "auth") {
|
|
|
2552
2700
|
authStatus();
|
|
2553
2701
|
}
|
|
2554
2702
|
}
|
|
2703
|
+
else if (command === "template") {
|
|
2704
|
+
if (subcommand === "pull") {
|
|
2705
|
+
templatePull(process.argv[4]);
|
|
2706
|
+
}
|
|
2707
|
+
else {
|
|
2708
|
+
console.log(`
|
|
2709
|
+
overscore template — Manage marketplace templates
|
|
2710
|
+
|
|
2711
|
+
Commands:
|
|
2712
|
+
pull <slug> [dir] Download and extract a template
|
|
2713
|
+
|
|
2714
|
+
Examples:
|
|
2715
|
+
npx @overscore/cli template pull mrr-tracker
|
|
2716
|
+
npx @overscore/cli template pull mrr-tracker my-mrr-dash
|
|
2717
|
+
`);
|
|
2718
|
+
}
|
|
2719
|
+
}
|
|
2555
2720
|
else if (command === "analysis") {
|
|
2556
2721
|
if (subcommand === "new") {
|
|
2557
2722
|
analysisNew(process.argv[4]);
|