@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 +11 -0
- package/dist/template-push.js +306 -0
- package/package.json +1 -1
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
|
+
}
|