@overscore/cli 0.13.1 → 0.13.4
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 +181 -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 || "",
|
|
@@ -239,6 +261,12 @@ const SOURCE_EXCLUDE = new Set([
|
|
|
239
261
|
".docker",
|
|
240
262
|
".gnupg",
|
|
241
263
|
".overscore",
|
|
264
|
+
// Raw exploratory query results (`query run` writes warehouse rows here).
|
|
265
|
+
// NEVER ship these in the deployed source bundle — they're real warehouse
|
|
266
|
+
// data. The analysis publish path already excludes data/ via .overscoreignore;
|
|
267
|
+
// this baseline exclusion protects the dashboard deploy path too, regardless of
|
|
268
|
+
// whether a project has a .overscoreignore.
|
|
269
|
+
"data",
|
|
242
270
|
]);
|
|
243
271
|
const SECRET_FILE_EXTENSIONS = new Set([
|
|
244
272
|
".pem",
|
|
@@ -284,6 +312,23 @@ function isSecretFileName(name) {
|
|
|
284
312
|
return true;
|
|
285
313
|
return false;
|
|
286
314
|
}
|
|
315
|
+
/**
|
|
316
|
+
* Resolve `relPath` under `baseDir`, returning the absolute path only if it
|
|
317
|
+
* stays inside `baseDir`. Knowledge-file paths come from the Hub (tenant-
|
|
318
|
+
* controlled) and are written to disk on pull/scaffold, so they MUST be
|
|
319
|
+
* contained — otherwise `../rules/data-fetching.md` could overwrite an
|
|
320
|
+
* auto-loaded Claude Code rule on a teammate's machine (audit H6).
|
|
321
|
+
*/
|
|
322
|
+
function safeJoinWithin(baseDir, relPath) {
|
|
323
|
+
if (typeof relPath !== "string" || relPath.length === 0 || relPath.includes("\0")) {
|
|
324
|
+
return null;
|
|
325
|
+
}
|
|
326
|
+
const base = path.resolve(baseDir);
|
|
327
|
+
const resolved = path.resolve(base, relPath);
|
|
328
|
+
if (resolved !== base && !resolved.startsWith(base + path.sep))
|
|
329
|
+
return null;
|
|
330
|
+
return resolved;
|
|
331
|
+
}
|
|
287
332
|
/**
|
|
288
333
|
* Walk a directory collecting deployable source files. Excludes machine-state
|
|
289
334
|
* and secrets, NEVER follows symlinks (a symlink could point at ~/.ssh/id_rsa
|
|
@@ -1068,7 +1113,7 @@ async function loadAnalysisConfig() {
|
|
|
1068
1113
|
const apiKey = env.VITE_OVERSCORE_API_KEY;
|
|
1069
1114
|
const projectSlug = env.VITE_OVERSCORE_PROJECT_SLUG;
|
|
1070
1115
|
if (apiUrl && apiKey && projectSlug) {
|
|
1071
|
-
return { apiUrl, apiKey, projectSlug };
|
|
1116
|
+
return { apiUrl: safeApiUrl(apiUrl), apiKey, projectSlug };
|
|
1072
1117
|
}
|
|
1073
1118
|
}
|
|
1074
1119
|
// (2) ~/.overscore/config
|
|
@@ -1086,7 +1131,7 @@ async function loadAnalysisConfig() {
|
|
|
1086
1131
|
});
|
|
1087
1132
|
if (checkRes.ok) {
|
|
1088
1133
|
return {
|
|
1089
|
-
apiUrl: cfg.OVERSCORE_API_URL || `${HUB_URL}/api
|
|
1134
|
+
apiUrl: safeApiUrl(cfg.OVERSCORE_API_URL || `${HUB_URL}/api`),
|
|
1090
1135
|
apiKey: cfg.OVERSCORE_API_KEY,
|
|
1091
1136
|
projectSlug: cfg.OVERSCORE_PROJECT_SLUG,
|
|
1092
1137
|
};
|
|
@@ -1096,7 +1141,7 @@ async function loadAnalysisConfig() {
|
|
|
1096
1141
|
catch {
|
|
1097
1142
|
// Can't reach the hub — use saved config and hope for the best
|
|
1098
1143
|
return {
|
|
1099
|
-
apiUrl: cfg.OVERSCORE_API_URL || `${HUB_URL}/api
|
|
1144
|
+
apiUrl: safeApiUrl(cfg.OVERSCORE_API_URL || `${HUB_URL}/api`),
|
|
1100
1145
|
apiKey: cfg.OVERSCORE_API_KEY,
|
|
1101
1146
|
projectSlug: cfg.OVERSCORE_PROJECT_SLUG,
|
|
1102
1147
|
};
|
|
@@ -1264,9 +1309,15 @@ async function syncKnowledgeBase(targetDir, baseUrl, projectSlug, apiKey) {
|
|
|
1264
1309
|
if (!data.files || data.files.length === 0)
|
|
1265
1310
|
return false;
|
|
1266
1311
|
fs.mkdirSync(knowledgeDir, { recursive: true });
|
|
1267
|
-
// Write each knowledge file
|
|
1312
|
+
// Write each knowledge file — contained within knowledgeDir (audit H6).
|
|
1268
1313
|
for (const file of data.files) {
|
|
1269
|
-
|
|
1314
|
+
const dest = safeJoinWithin(knowledgeDir, file.path);
|
|
1315
|
+
if (!dest) {
|
|
1316
|
+
console.warn(` Skipped knowledge file with unsafe path: ${file.path}`);
|
|
1317
|
+
continue;
|
|
1318
|
+
}
|
|
1319
|
+
fs.mkdirSync(path.dirname(dest), { recursive: true });
|
|
1320
|
+
fs.writeFileSync(dest, file.content);
|
|
1270
1321
|
}
|
|
1271
1322
|
// Generate INDEX.md
|
|
1272
1323
|
const indexLines = [
|
|
@@ -1777,8 +1828,16 @@ function collectAnalysisFiles(dir, prefix, patterns) {
|
|
|
1777
1828
|
const userPatterns = patterns ?? loadOverscoreIgnore(dir);
|
|
1778
1829
|
const files = [];
|
|
1779
1830
|
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
1831
|
+
// Never follow symlinks or ship secrets — same guards as the deploy path's
|
|
1832
|
+
// collectSourceFiles (audit H5: this analysis-publish collector had neither,
|
|
1833
|
+
// so a poisoned agent could upload service-account.json / id_rsa, or a
|
|
1834
|
+
// file-target symlink to ~/.ssh/id_rsa).
|
|
1835
|
+
if (entry.isSymbolicLink())
|
|
1836
|
+
continue;
|
|
1780
1837
|
if (isBaselineExcluded(entry.name))
|
|
1781
1838
|
continue;
|
|
1839
|
+
if (isSecretFileName(entry.name))
|
|
1840
|
+
continue;
|
|
1782
1841
|
const relativePath = prefix ? `${prefix}/${entry.name}` : entry.name;
|
|
1783
1842
|
if (isIgnored(relativePath, entry.isDirectory(), userPatterns))
|
|
1784
1843
|
continue;
|
|
@@ -2332,6 +2391,17 @@ async function dev() {
|
|
|
2332
2391
|
await cleanup();
|
|
2333
2392
|
process.exit(0);
|
|
2334
2393
|
});
|
|
2394
|
+
// Also clean up on terminal close (SIGHUP) and on a crash, so the short-lived
|
|
2395
|
+
// dev key isn't stranded live when the tab closes or the process throws (M12).
|
|
2396
|
+
process.on("SIGHUP", async () => {
|
|
2397
|
+
await cleanup();
|
|
2398
|
+
process.exit(0);
|
|
2399
|
+
});
|
|
2400
|
+
process.on("uncaughtException", async (err) => {
|
|
2401
|
+
await cleanup();
|
|
2402
|
+
console.error(err);
|
|
2403
|
+
process.exit(1);
|
|
2404
|
+
});
|
|
2335
2405
|
}
|
|
2336
2406
|
// ── auth ────────────────────────────────────────────────────────────
|
|
2337
2407
|
async function authStatus() {
|
|
@@ -2520,6 +2590,90 @@ ${installed.map((s) => ` - ${s}`).join("\n")}
|
|
|
2520
2590
|
Re-run this command anytime to update.
|
|
2521
2591
|
`);
|
|
2522
2592
|
}
|
|
2593
|
+
// ── template pull ──────────────────────────────────────────────────
|
|
2594
|
+
async function templatePull(slug) {
|
|
2595
|
+
if (!slug) {
|
|
2596
|
+
console.error("\n Usage: npx @overscore/cli template pull <template-slug> [dir]\n");
|
|
2597
|
+
console.error(" Example:");
|
|
2598
|
+
console.error(" npx @overscore/cli template pull mrr-tracker\n");
|
|
2599
|
+
process.exit(1);
|
|
2600
|
+
}
|
|
2601
|
+
const targetDir = path.resolve(process.cwd(), process.argv[4] || slug);
|
|
2602
|
+
const url = `https://overscore.dev/api/templates/${slug}/source`;
|
|
2603
|
+
console.log(`\n Pulling template: ${slug}...`);
|
|
2604
|
+
let res;
|
|
2605
|
+
try {
|
|
2606
|
+
res = await fetch(url, {
|
|
2607
|
+
// Optional: attach device token if available, for attribution in install counting
|
|
2608
|
+
headers: (() => {
|
|
2609
|
+
const configPath = path.join(process.env.HOME || "", ".overscore", "config");
|
|
2610
|
+
if (fs.existsSync(configPath)) {
|
|
2611
|
+
const cfg = parseEnv(fs.readFileSync(configPath, "utf-8"));
|
|
2612
|
+
if (cfg.OVERSCORE_DEVICE_TOKEN) {
|
|
2613
|
+
return { Authorization: `Bearer ${cfg.OVERSCORE_DEVICE_TOKEN}` };
|
|
2614
|
+
}
|
|
2615
|
+
}
|
|
2616
|
+
return {};
|
|
2617
|
+
})(),
|
|
2618
|
+
});
|
|
2619
|
+
}
|
|
2620
|
+
catch {
|
|
2621
|
+
console.error(`\n Error: Could not reach overscore.dev\n`);
|
|
2622
|
+
process.exit(1);
|
|
2623
|
+
}
|
|
2624
|
+
if (!res.ok) {
|
|
2625
|
+
let errorMsg = `HTTP ${res.status}`;
|
|
2626
|
+
try {
|
|
2627
|
+
const body = await res.json();
|
|
2628
|
+
errorMsg = body.error || errorMsg;
|
|
2629
|
+
}
|
|
2630
|
+
catch {
|
|
2631
|
+
//
|
|
2632
|
+
}
|
|
2633
|
+
console.error(`\n Error: ${errorMsg}\n`);
|
|
2634
|
+
process.exit(1);
|
|
2635
|
+
}
|
|
2636
|
+
const templateSlug = res.headers.get("X-OS-Template-Slug") || slug;
|
|
2637
|
+
const templateVersion = res.headers.get("X-OS-Template-Version") || "0";
|
|
2638
|
+
const zipBuffer = Buffer.from(await res.arrayBuffer());
|
|
2639
|
+
// Confirm overwrite if dir exists
|
|
2640
|
+
if (fs.existsSync(targetDir)) {
|
|
2641
|
+
const yes = await confirm(`Directory "${path.basename(targetDir)}" already exists. Overwrite?`);
|
|
2642
|
+
if (!yes) {
|
|
2643
|
+
console.log(" Cancelled.\n");
|
|
2644
|
+
return;
|
|
2645
|
+
}
|
|
2646
|
+
for (const entry of fs.readdirSync(targetDir)) {
|
|
2647
|
+
if (entry === "node_modules")
|
|
2648
|
+
continue;
|
|
2649
|
+
fs.rmSync(path.join(targetDir, entry), { recursive: true, force: true });
|
|
2650
|
+
}
|
|
2651
|
+
}
|
|
2652
|
+
else {
|
|
2653
|
+
fs.mkdirSync(targetDir, { recursive: true });
|
|
2654
|
+
}
|
|
2655
|
+
// Extract zip
|
|
2656
|
+
await extractZip(zipBuffer, targetDir);
|
|
2657
|
+
// Write provenance: templates don't have .env (unbound to a project)
|
|
2658
|
+
// Instead, .overscore/template.json records the pull metadata
|
|
2659
|
+
const oscoreDir = path.join(targetDir, ".overscore");
|
|
2660
|
+
fs.mkdirSync(oscoreDir, { recursive: true });
|
|
2661
|
+
fs.writeFileSync(path.join(oscoreDir, "template.json"), JSON.stringify({
|
|
2662
|
+
template_slug: templateSlug,
|
|
2663
|
+
template_version: templateVersion,
|
|
2664
|
+
pulled_at: new Date().toISOString(),
|
|
2665
|
+
}, null, 2));
|
|
2666
|
+
console.log(`
|
|
2667
|
+
✓ Template extracted to: ${path.relative(process.cwd(), targetDir)}
|
|
2668
|
+
|
|
2669
|
+
Next steps:
|
|
2670
|
+
cd ${path.relative(process.cwd(), targetDir)}
|
|
2671
|
+
npm install
|
|
2672
|
+
npx @overscore/cli dev
|
|
2673
|
+
|
|
2674
|
+
This template runs on mock data — no warehouse connection or API key needed.
|
|
2675
|
+
`);
|
|
2676
|
+
}
|
|
2523
2677
|
// ── Routing ─────────────────────────────────────────────────────────
|
|
2524
2678
|
const command = process.argv[2];
|
|
2525
2679
|
const subcommand = process.argv[3];
|
|
@@ -2552,6 +2706,23 @@ else if (command === "auth") {
|
|
|
2552
2706
|
authStatus();
|
|
2553
2707
|
}
|
|
2554
2708
|
}
|
|
2709
|
+
else if (command === "template") {
|
|
2710
|
+
if (subcommand === "pull") {
|
|
2711
|
+
templatePull(process.argv[4]);
|
|
2712
|
+
}
|
|
2713
|
+
else {
|
|
2714
|
+
console.log(`
|
|
2715
|
+
overscore template — Manage marketplace templates
|
|
2716
|
+
|
|
2717
|
+
Commands:
|
|
2718
|
+
pull <slug> [dir] Download and extract a template
|
|
2719
|
+
|
|
2720
|
+
Examples:
|
|
2721
|
+
npx @overscore/cli template pull mrr-tracker
|
|
2722
|
+
npx @overscore/cli template pull mrr-tracker my-mrr-dash
|
|
2723
|
+
`);
|
|
2724
|
+
}
|
|
2725
|
+
}
|
|
2555
2726
|
else if (command === "analysis") {
|
|
2556
2727
|
if (subcommand === "new") {
|
|
2557
2728
|
analysisNew(process.argv[4]);
|