@overscore/cli 0.13.3 → 0.13.5

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 CHANGED
@@ -8,6 +8,7 @@ import { execSync } from "child_process";
8
8
  import { createInterface } from "readline";
9
9
  import { createHash, randomBytes } from "crypto";
10
10
  import yauzl from "yauzl";
11
+ import { templatePush } from "./template-push.js";
11
12
  // ── Shared helpers ──────────────────────────────────────────────────
12
13
  // Name CLI-minted API keys after the machine + date so stale keys are
13
14
  // recognizable and revocable in the Hub UI (mitigates key sprawl). Keys are
@@ -261,6 +262,12 @@ const SOURCE_EXCLUDE = new Set([
261
262
  ".docker",
262
263
  ".gnupg",
263
264
  ".overscore",
265
+ // Raw exploratory query results (`query run` writes warehouse rows here).
266
+ // NEVER ship these in the deployed source bundle — they're real warehouse
267
+ // data. The analysis publish path already excludes data/ via .overscoreignore;
268
+ // this baseline exclusion protects the dashboard deploy path too, regardless of
269
+ // whether a project has a .overscoreignore.
270
+ "data",
264
271
  ]);
265
272
  const SECRET_FILE_EXTENSIONS = new Set([
266
273
  ".pem",
@@ -2704,12 +2711,16 @@ else if (command === "template") {
2704
2711
  if (subcommand === "pull") {
2705
2712
  templatePull(process.argv[4]);
2706
2713
  }
2714
+ else if (subcommand === "push") {
2715
+ templatePush(process.argv[4]);
2716
+ }
2707
2717
  else {
2708
2718
  console.log(`
2709
2719
  overscore template — Manage marketplace templates
2710
2720
 
2711
2721
  Commands:
2712
2722
  pull <slug> [dir] Download and extract a template
2723
+ push [slug] Build the current directory and push it back to your template
2713
2724
 
2714
2725
  Examples:
2715
2726
  npx @overscore/cli template pull mrr-tracker
@@ -0,0 +1,306 @@
1
+ /**
2
+ * `overscore template push [slug]` — build the current directory and push it
3
+ * back to YOUR marketplace template (push update channel). The Hub replaces
4
+ * the template's frozen built files + pinned source.zip in place; no project,
5
+ * no dashboard, and no API key are involved — auth is the device token and
6
+ * ownership is checked server-side.
7
+ *
8
+ * Self-contained on purpose: index.ts is a single hand-rolled router and this
9
+ * module duplicates the few helpers it needs (parseEnv, file collectors)
10
+ * rather than entangling the deploy path.
11
+ *
12
+ * ── DEFERRED HOOK (staged — do NOT wire until the push backend ships) ──────
13
+ * packages/cli/src/index.ts needs exactly these 4 lines:
14
+ *
15
+ * 1. Import, with the other imports at the top of the file (after the
16
+ * `import yauzl from "yauzl";` line, index.ts ~11). The `.js` extension is
17
+ * required — tsc emits ESM (module: ESNext) and Node only resolves
18
+ * relative ESM imports with an extension:
19
+ *
20
+ * import { templatePush } from "./template-push.js";
21
+ *
22
+ * 2-3. Router case inside the `command === "template"` block, directly after
23
+ * the `if (subcommand === "pull") { templatePull(process.argv[4]); }`
24
+ * branch (index.ts ~3338-3339), before the help `else`:
25
+ *
26
+ * } else if (subcommand === "push") {
27
+ * templatePush(process.argv[4]);
28
+ *
29
+ * 4. Help line in the `overscore template` help text (index.ts ~3345, under
30
+ * the `pull <slug> [dir]` line):
31
+ *
32
+ * push [slug] Build the current directory and push it back to your template
33
+ */
34
+ import fs from "fs";
35
+ import path from "path";
36
+ import { execSync } from "child_process";
37
+ const HUB_URL = "https://overscore.dev";
38
+ // Mirrors the templates_slug_format CHECK in the marketplace migration.
39
+ const TEMPLATE_SLUG_RE = /^[a-z0-9]([a-z0-9-]*[a-z0-9])?$/;
40
+ function isValidTemplateSlug(slug) {
41
+ return (slug.length >= 1 &&
42
+ slug.length <= 100 &&
43
+ !slug.includes("--") &&
44
+ TEMPLATE_SLUG_RE.test(slug));
45
+ }
46
+ // Duplicated from index.ts — parses the KEY=VALUE lines of ~/.overscore/config.
47
+ function parseEnv(content) {
48
+ const env = {};
49
+ for (const line of content.split("\n")) {
50
+ const trimmed = line.trim();
51
+ if (!trimmed || trimmed.startsWith("#"))
52
+ continue;
53
+ const eqIndex = trimmed.indexOf("=");
54
+ if (eqIndex === -1)
55
+ continue;
56
+ env[trimmed.slice(0, eqIndex)] = trimmed.slice(eqIndex + 1);
57
+ }
58
+ return env;
59
+ }
60
+ function readDeviceToken() {
61
+ const configPath = path.join(process.env.HOME || "", ".overscore", "config");
62
+ if (!fs.existsSync(configPath))
63
+ return null;
64
+ const cfg = parseEnv(fs.readFileSync(configPath, "utf-8"));
65
+ return cfg.OVERSCORE_DEVICE_TOKEN || null;
66
+ }
67
+ /** Slug fallback: `template pull` records it in .overscore/template.json. */
68
+ function readPulledTemplateSlug() {
69
+ const metaPath = path.resolve(process.cwd(), ".overscore", "template.json");
70
+ if (!fs.existsSync(metaPath))
71
+ return null;
72
+ try {
73
+ const meta = JSON.parse(fs.readFileSync(metaPath, "utf-8"));
74
+ return typeof meta.template_slug === "string" ? meta.template_slug : null;
75
+ }
76
+ catch {
77
+ return null;
78
+ }
79
+ }
80
+ // Duplicated from index.ts — plain recursive walk of dist/ (built output is
81
+ // machine-generated; no secret filtering needed there).
82
+ function collectFiles(dir, prefix) {
83
+ const files = [];
84
+ for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
85
+ const relativePath = prefix ? `${prefix}/${entry.name}` : entry.name;
86
+ if (entry.isDirectory()) {
87
+ files.push(...collectFiles(path.join(dir, entry.name), relativePath));
88
+ }
89
+ else {
90
+ files.push(relativePath);
91
+ }
92
+ }
93
+ return files;
94
+ }
95
+ // ── Safe source collector (duplicated guards from the deploy path) ──────────
96
+ // Excludes machine state, build output, and anything secret-shaped, and NEVER
97
+ // follows symlinks (a symlink could point at ~/.ssh/id_rsa or outside the
98
+ // project). v1 limitation, on purpose: `.overscoreignore` is NOT honored here
99
+ // — templates ship mock data only, so there should be nothing project-
100
+ // specific to hide beyond these baseline exclusions.
101
+ const SOURCE_EXCLUDE = new Set([
102
+ "node_modules",
103
+ "dist",
104
+ ".git",
105
+ ".DS_Store",
106
+ ".aws",
107
+ ".ssh",
108
+ ".kube",
109
+ ".docker",
110
+ ".gnupg",
111
+ ".overscore",
112
+ // Raw exploratory query results — never ship real warehouse rows.
113
+ "data",
114
+ ]);
115
+ const SECRET_FILE_EXTENSIONS = new Set([
116
+ ".pem",
117
+ ".key",
118
+ ".p12",
119
+ ".pfx",
120
+ ".jks",
121
+ ".keystore",
122
+ ".p8",
123
+ ".ppk",
124
+ ".gpg",
125
+ ".asc",
126
+ ".tfstate",
127
+ ".tfvars",
128
+ ]);
129
+ const SECRET_FILE_NAMES = new Set([
130
+ "service-account.json",
131
+ "service_account.json",
132
+ "serviceaccount.json",
133
+ "gcp-credentials.json",
134
+ "credentials.json",
135
+ "id_rsa",
136
+ "id_dsa",
137
+ "id_ecdsa",
138
+ "id_ed25519",
139
+ ".netrc",
140
+ ".pgpass",
141
+ ".htpasswd",
142
+ "secrets.json",
143
+ "secrets.yaml",
144
+ "secrets.yml",
145
+ ]);
146
+ function isSecretFileName(name) {
147
+ const lower = name.toLowerCase();
148
+ if (SECRET_FILE_NAMES.has(lower))
149
+ return true;
150
+ const ext = lower.substring(lower.lastIndexOf("."));
151
+ if (SECRET_FILE_EXTENSIONS.has(ext))
152
+ return true;
153
+ if (lower.includes(".tfstate"))
154
+ return true;
155
+ return false;
156
+ }
157
+ function collectTemplateSourceFiles(dir, prefix, skipped) {
158
+ const s = skipped ?? [];
159
+ const files = [];
160
+ for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
161
+ if (entry.isSymbolicLink()) {
162
+ s.push((prefix ? `${prefix}/` : "") + entry.name + " (symlink)");
163
+ continue;
164
+ }
165
+ if (SOURCE_EXCLUDE.has(entry.name))
166
+ continue;
167
+ if (entry.name.startsWith(".env"))
168
+ continue; // .env, .env.local, …
169
+ if (entry.name.endsWith(".log"))
170
+ continue;
171
+ if (isSecretFileName(entry.name)) {
172
+ s.push((prefix ? `${prefix}/` : "") + entry.name + " (secret)");
173
+ continue;
174
+ }
175
+ const relativePath = prefix ? `${prefix}/${entry.name}` : entry.name;
176
+ if (entry.isDirectory()) {
177
+ files.push(...collectTemplateSourceFiles(path.join(dir, entry.name), relativePath, s)
178
+ .files);
179
+ }
180
+ else {
181
+ files.push(relativePath);
182
+ }
183
+ }
184
+ return { files, skipped: s };
185
+ }
186
+ // ── Command ──────────────────────────────────────────────────────────────────
187
+ export async function templatePush(slugArg) {
188
+ const slug = slugArg || readPulledTemplateSlug();
189
+ if (!slug) {
190
+ console.error(`
191
+ Usage: npx @overscore/cli template push [template-slug]
192
+
193
+ Run it from a pulled template directory (.overscore/template.json supplies
194
+ the slug automatically) or pass the slug explicitly.
195
+
196
+ Example:
197
+ npx @overscore/cli template push mrr-tracker
198
+ `);
199
+ process.exit(1);
200
+ }
201
+ if (!isValidTemplateSlug(slug)) {
202
+ console.error(`\n Error: "${slug}" is not a valid template slug.\n`);
203
+ process.exit(1);
204
+ }
205
+ const deviceToken = readDeviceToken();
206
+ if (!deviceToken) {
207
+ console.error(`
208
+ No Overscore credentials found. Sign in first:
209
+
210
+ npx @overscore/cli auth login
211
+ `);
212
+ process.exit(1);
213
+ }
214
+ console.log(`\n Pushing template: ${slug}\n`);
215
+ // Build — same guard as deploy: blank the API key env so a local dev key
216
+ // can never end up baked into the published bundle.
217
+ console.log(" Building...");
218
+ try {
219
+ execSync("npm run build", {
220
+ stdio: "inherit",
221
+ env: { ...process.env, VITE_OVERSCORE_API_KEY: "" },
222
+ });
223
+ }
224
+ catch {
225
+ console.error("\n Build failed. Fix errors and try again.");
226
+ process.exit(1);
227
+ }
228
+ const distDir = path.resolve(process.cwd(), "dist");
229
+ if (!fs.existsSync(distDir)) {
230
+ console.error(" Error: No dist/ directory found after build.");
231
+ process.exit(1);
232
+ }
233
+ const builtFiles = collectFiles(distDir, "");
234
+ console.log(` Found ${builtFiles.length} built files.`);
235
+ const { files: sourceFiles, skipped } = collectTemplateSourceFiles(process.cwd(), "");
236
+ console.log(` Found ${sourceFiles.length} source files.`);
237
+ if (skipped.length > 0) {
238
+ // Never silently lossy on secrets — same rule as deploy.
239
+ console.log(` Skipped ${skipped.length} sensitive/symlinked file(s): ${skipped
240
+ .slice(0, 10)
241
+ .join(", ")}${skipped.length > 10 ? ", …" : ""}`);
242
+ }
243
+ console.log(" Uploading...");
244
+ const formData = new FormData();
245
+ for (const file of builtFiles) {
246
+ const content = fs.readFileSync(path.join(distDir, file));
247
+ formData.append(`file:${file}`, new Blob([content]), file);
248
+ }
249
+ for (const file of sourceFiles) {
250
+ const content = fs.readFileSync(path.join(process.cwd(), file));
251
+ formData.append(`source:${file}`, new Blob([content]), file);
252
+ }
253
+ let res;
254
+ try {
255
+ res = await fetch(`${HUB_URL}/api/templates/${slug}/push`, {
256
+ method: "POST",
257
+ headers: { Authorization: `Bearer ${deviceToken}` },
258
+ body: formData,
259
+ });
260
+ }
261
+ catch {
262
+ console.error(`\n Error: Could not reach overscore.dev\n`);
263
+ process.exit(1);
264
+ }
265
+ const data = (await res.json().catch(() => ({})));
266
+ if (!res.ok) {
267
+ const serverMsg = data.error || data.message;
268
+ if (res.status === 401) {
269
+ console.error(`
270
+ Authentication failed${serverMsg ? `: ${serverMsg}` : ""}.
271
+
272
+ Your device token may have expired — sign in again:
273
+ npx @overscore/cli auth login
274
+ `);
275
+ }
276
+ else if (res.status === 403) {
277
+ console.error(`
278
+ You don't own the template "${slug}"${serverMsg ? ` — ${serverMsg}` : "."}
279
+
280
+ Push only works on templates you published. To build on someone else's
281
+ template, pull it and publish your own listing.
282
+ `);
283
+ }
284
+ else if (res.status === 422) {
285
+ // Channel/validation errors (e.g. a dashboard-channel template that
286
+ // updates via "Refresh to the latest deploy") — the server explains
287
+ // itself; pass it through verbatim.
288
+ console.error(`\n Error: ${serverMsg || "This template can't be pushed."}\n`);
289
+ }
290
+ else {
291
+ console.error(`\n Push failed: ${serverMsg || `HTTP ${res.status}`}\n`);
292
+ }
293
+ process.exit(1);
294
+ }
295
+ const builtCount = data.built_file_count ?? builtFiles.length;
296
+ const sourceCount = sourceFiles.length;
297
+ const listingUrl = data.detail_url
298
+ ? `${HUB_URL}${data.detail_url}`
299
+ : `${HUB_URL}/explore/${slug}`;
300
+ console.log(`
301
+ Pushed successfully!
302
+
303
+ ${builtCount} built files + ${sourceCount} source files uploaded
304
+ Listing: ${listingUrl}${data.preview_url ? `\n Preview: ${data.preview_url}` : ""}
305
+ `);
306
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@overscore/cli",
3
- "version": "0.13.3",
3
+ "version": "0.13.5",
4
4
  "description": "CLI for deploying Overscore dashboards and publishing analyses",
5
5
  "bin": {
6
6
  "overscore": "dist/index.js"