@zveltio/cli 3.0.0-beta.5 → 3.0.0-beta.70
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 +1226 -258
- package/package.json +3 -3
package/dist/index.js
CHANGED
|
@@ -861,7 +861,7 @@ Expecting one of '${allowedValues.join("', '")}'`);
|
|
|
861
861
|
this._exitCallback = (err) => {
|
|
862
862
|
if (err.code !== "commander.executeSubCommandAsync") {
|
|
863
863
|
throw err;
|
|
864
|
-
}
|
|
864
|
+
}
|
|
865
865
|
};
|
|
866
866
|
}
|
|
867
867
|
return this;
|
|
@@ -1888,7 +1888,7 @@ var require_commander = __commonJS((exports) => {
|
|
|
1888
1888
|
|
|
1889
1889
|
// ../sdk/dist/build/index.js
|
|
1890
1890
|
import { createRequire } from "module";
|
|
1891
|
-
import { existsSync as
|
|
1891
|
+
import { existsSync as existsSync6, readFileSync } from "fs";
|
|
1892
1892
|
import { dirname, join as join5 } from "path";
|
|
1893
1893
|
function escapeRegExp(s) {
|
|
1894
1894
|
return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
@@ -1913,7 +1913,7 @@ function resolveSearchPaths(resolveFromDir) {
|
|
|
1913
1913
|
for (let i = 0;i < 6; i++) {
|
|
1914
1914
|
cur = dirname(cur);
|
|
1915
1915
|
paths.push(cur);
|
|
1916
|
-
if (
|
|
1916
|
+
if (existsSync6(join5(cur, "node_modules")))
|
|
1917
1917
|
break;
|
|
1918
1918
|
}
|
|
1919
1919
|
return [...new Set(paths)];
|
|
@@ -1927,7 +1927,7 @@ function resolvePackageRoot(pkg, searchPaths) {
|
|
|
1927
1927
|
}
|
|
1928
1928
|
for (;; ) {
|
|
1929
1929
|
const pkgJsonPath = join5(candidate, "package.json");
|
|
1930
|
-
if (
|
|
1930
|
+
if (existsSync6(pkgJsonPath)) {
|
|
1931
1931
|
const manifest = JSON.parse(readFileSync(pkgJsonPath, "utf8"));
|
|
1932
1932
|
if (manifest.name === pkg)
|
|
1933
1933
|
return candidate;
|
|
@@ -1951,7 +1951,7 @@ function resolvePackageEsmEntry(pkg, resolveFromDir) {
|
|
|
1951
1951
|
throw new Error(`Cannot determine ESM entry for package "${pkg}"`);
|
|
1952
1952
|
}
|
|
1953
1953
|
const abs = join5(root, rel.replace(/^\.\//, ""));
|
|
1954
|
-
if (!
|
|
1954
|
+
if (!existsSync6(abs)) {
|
|
1955
1955
|
throw new Error(`ESM entry missing for "${pkg}": ${abs}`);
|
|
1956
1956
|
}
|
|
1957
1957
|
if (abs.endsWith(".d.ts")) {
|
|
@@ -1997,7 +1997,7 @@ var init_build = __esm(() => {
|
|
|
1997
1997
|
});
|
|
1998
1998
|
|
|
1999
1999
|
// src/lib/extension-bundle.ts
|
|
2000
|
-
import { existsSync as
|
|
2000
|
+
import { existsSync as existsSync7, renameSync } from "fs";
|
|
2001
2001
|
import { basename, dirname as dirname2, join as join6 } from "path";
|
|
2002
2002
|
async function bundleExtensionEngine(opts) {
|
|
2003
2003
|
const outdir = dirname2(opts.outfile);
|
|
@@ -2018,7 +2018,7 @@ async function bundleExtensionEngine(opts) {
|
|
|
2018
2018
|
`);
|
|
2019
2019
|
throw new Error(messages || "Bun.build failed");
|
|
2020
2020
|
}
|
|
2021
|
-
if (!
|
|
2021
|
+
if (!existsSync7(builtPath)) {
|
|
2022
2022
|
throw new Error(`Bun.build succeeded but ${builtPath} was not produced`);
|
|
2023
2023
|
}
|
|
2024
2024
|
if (builtPath !== opts.outfile) {
|
|
@@ -2031,6 +2031,18 @@ var init_extension_bundle = __esm(() => {
|
|
|
2031
2031
|
EXTENSION_BUNDLE_CORE_DEPS2 = EXTENSION_BUNDLE_CORE_DEPS;
|
|
2032
2032
|
});
|
|
2033
2033
|
|
|
2034
|
+
// src/lib/pack-isolation.ts
|
|
2035
|
+
function resolvePackIsolation(opts) {
|
|
2036
|
+
let resolved = opts.current;
|
|
2037
|
+
if (opts.firstParty && resolved === "worker" && !opts.keepIsolation) {
|
|
2038
|
+
resolved = undefined;
|
|
2039
|
+
}
|
|
2040
|
+
if (!resolved && opts.communityInject && !opts.firstParty) {
|
|
2041
|
+
resolved = "worker";
|
|
2042
|
+
}
|
|
2043
|
+
return resolved;
|
|
2044
|
+
}
|
|
2045
|
+
|
|
2034
2046
|
// src/lib/publisher-tier.ts
|
|
2035
2047
|
function tierAllowsInline(tier) {
|
|
2036
2048
|
return tier === "first-party" || tier === "verified";
|
|
@@ -2069,14 +2081,108 @@ function normalizeTier(v) {
|
|
|
2069
2081
|
// src/commands/extension-pack.ts
|
|
2070
2082
|
var exports_extension_pack = {};
|
|
2071
2083
|
__export(exports_extension_pack, {
|
|
2084
|
+
recordBundledVersions: () => recordBundledVersions,
|
|
2085
|
+
hashEngineSources: () => hashEngineSources,
|
|
2072
2086
|
extensionPackCommand: () => extensionPackCommand
|
|
2073
2087
|
});
|
|
2074
2088
|
import { createHash } from "crypto";
|
|
2075
|
-
import { existsSync as
|
|
2076
|
-
import {
|
|
2089
|
+
import { existsSync as existsSync8, readdirSync, readFileSync as readFileSync2, writeFileSync } from "fs";
|
|
2090
|
+
import { homedir } from "os";
|
|
2091
|
+
import { join as join7, relative, resolve } from "path";
|
|
2092
|
+
function scrubBuildPaths(outfile, dir) {
|
|
2093
|
+
const original = readFileSync2(outfile, "utf8");
|
|
2094
|
+
let count = 0;
|
|
2095
|
+
let text = original.replace(/(__dirname|__filename)\s*=\s*"(\/[^"]*)"/g, (_match, name, abs) => {
|
|
2096
|
+
count += 1;
|
|
2097
|
+
const at = abs.lastIndexOf("node_modules/");
|
|
2098
|
+
const tail = at === -1 ? abs.slice(abs.lastIndexOf("/") + 1) : abs.slice(at);
|
|
2099
|
+
return `${name} = "/zveltio-extension/${tail}"`;
|
|
2100
|
+
});
|
|
2101
|
+
text = text.replace(/\.{1,2}\/[^\s"'`]*(?:node_modules|packages)\/[^\s"'`]*/g, (match) => {
|
|
2102
|
+
const store = match.indexOf(".bun/");
|
|
2103
|
+
const at = store === -1 ? Math.min(...[match.lastIndexOf("node_modules/"), match.indexOf("packages/")].filter((i) => i >= 0)) : store;
|
|
2104
|
+
count += 1;
|
|
2105
|
+
return `/zveltio-extension/${match.slice(at)}`;
|
|
2106
|
+
});
|
|
2107
|
+
text = text.replace(/(?<=^\/\/\s*)node_modules\//gm, "/zveltio-extension/node_modules/");
|
|
2108
|
+
const prefix = relative(process.cwd(), dir).replace(/\\/g, "/");
|
|
2109
|
+
if (prefix && !prefix.startsWith("..")) {
|
|
2110
|
+
const re = new RegExp(`(?<=^//\\s*)${prefix.replace(/[.*+?^${}()|[\\]]/g, "\\$&")}/`, "gm");
|
|
2111
|
+
const before = text;
|
|
2112
|
+
text = text.replace(re, "");
|
|
2113
|
+
if (text !== before)
|
|
2114
|
+
count += 1;
|
|
2115
|
+
}
|
|
2116
|
+
const home = homedir();
|
|
2117
|
+
if (home && home.startsWith("/") && home.length > 1 && text.includes(home)) {
|
|
2118
|
+
count += text.split(home).length - 1;
|
|
2119
|
+
text = text.split(home).join("/zveltio-extension");
|
|
2120
|
+
}
|
|
2121
|
+
if (count > 0)
|
|
2122
|
+
writeFileSync(outfile, text, "utf8");
|
|
2123
|
+
return count;
|
|
2124
|
+
}
|
|
2125
|
+
function recordBundledVersions(outfile, dir) {
|
|
2126
|
+
let text = readFileSync2(outfile, "utf8");
|
|
2127
|
+
const recorded = [];
|
|
2128
|
+
for (const dep of CORE_DEPS) {
|
|
2129
|
+
if (!text.includes(`node_modules/${dep}/`) || text.includes(`.bun/${dep}@`))
|
|
2130
|
+
continue;
|
|
2131
|
+
for (let at = resolve(dir);; at = resolve(at, "..")) {
|
|
2132
|
+
const pkg = join7(at, "node_modules", dep, "package.json");
|
|
2133
|
+
if (existsSync8(pkg)) {
|
|
2134
|
+
const { version } = JSON.parse(readFileSync2(pkg, "utf8"));
|
|
2135
|
+
if (version)
|
|
2136
|
+
recorded.push(`${dep}@${version}`);
|
|
2137
|
+
break;
|
|
2138
|
+
}
|
|
2139
|
+
if (at === resolve(at, ".."))
|
|
2140
|
+
break;
|
|
2141
|
+
}
|
|
2142
|
+
}
|
|
2143
|
+
if (recorded.length === 0)
|
|
2144
|
+
return recorded;
|
|
2145
|
+
text = text.replace(/\n*$/, `
|
|
2146
|
+
`);
|
|
2147
|
+
text += recorded.map((r) => `// @zveltio-bundled ${r}
|
|
2148
|
+
`).join("");
|
|
2149
|
+
writeFileSync(outfile, text, "utf8");
|
|
2150
|
+
return recorded;
|
|
2151
|
+
}
|
|
2152
|
+
function hashEngineSources(dir) {
|
|
2153
|
+
const engineDir = join7(dir, "engine");
|
|
2154
|
+
const files = [];
|
|
2155
|
+
const walk = (d) => {
|
|
2156
|
+
for (const entry of readdirSync(d, { withFileTypes: true })) {
|
|
2157
|
+
const full = join7(d, entry.name);
|
|
2158
|
+
if (entry.isDirectory()) {
|
|
2159
|
+
if (entry.name === "migrations" || entry.name === "node_modules")
|
|
2160
|
+
continue;
|
|
2161
|
+
walk(full);
|
|
2162
|
+
continue;
|
|
2163
|
+
}
|
|
2164
|
+
if (!/\.(ts|tsx|js|mjs|json)$/.test(entry.name))
|
|
2165
|
+
continue;
|
|
2166
|
+
if (/\.(test|spec)\.[a-z]+$/.test(entry.name))
|
|
2167
|
+
continue;
|
|
2168
|
+
if (entry.name === "index.js")
|
|
2169
|
+
continue;
|
|
2170
|
+
files.push(full);
|
|
2171
|
+
}
|
|
2172
|
+
};
|
|
2173
|
+
walk(engineDir);
|
|
2174
|
+
const h = createHash("sha256");
|
|
2175
|
+
for (const f of files.sort()) {
|
|
2176
|
+
h.update(f.slice(engineDir.length).replace(/\\/g, "/"));
|
|
2177
|
+
h.update("\x00");
|
|
2178
|
+
h.update(readFileSync2(f));
|
|
2179
|
+
h.update("\x00");
|
|
2180
|
+
}
|
|
2181
|
+
return h.digest("hex");
|
|
2182
|
+
}
|
|
2077
2183
|
function readManifest(dir) {
|
|
2078
2184
|
const path = join7(dir, "manifest.json");
|
|
2079
|
-
if (!
|
|
2185
|
+
if (!existsSync8(path)) {
|
|
2080
2186
|
throw new Error(`No manifest.json at ${path}.`);
|
|
2081
2187
|
}
|
|
2082
2188
|
return JSON.parse(readFileSync2(path, "utf8"));
|
|
@@ -2099,7 +2205,7 @@ async function extensionPackCommand(opts) {
|
|
|
2099
2205
|
const dir = resolve(opts.dir ?? process.cwd());
|
|
2100
2206
|
const entry = join7(dir, "engine", "index.ts");
|
|
2101
2207
|
const outfile = join7(dir, "engine", "index.js");
|
|
2102
|
-
if (!
|
|
2208
|
+
if (!existsSync8(entry)) {
|
|
2103
2209
|
throw new Error(`No engine/index.ts at ${entry}. ` + `Run from the extension root or pass --dir. Extensions without an engine entry don't need pack.`);
|
|
2104
2210
|
}
|
|
2105
2211
|
const manifest = readManifest(dir);
|
|
@@ -2125,6 +2231,14 @@ async function extensionPackCommand(opts) {
|
|
|
2125
2231
|
} catch (err) {
|
|
2126
2232
|
throw new Error(`Bun bundle failed: ${err.message}`);
|
|
2127
2233
|
}
|
|
2234
|
+
const scrubbed = scrubBuildPaths(outfile, dir);
|
|
2235
|
+
if (scrubbed > 0) {
|
|
2236
|
+
console.log(` ${c5.green("\u2713")} ${c5.dim(`${scrubbed} build path(s) neutralised`)}`);
|
|
2237
|
+
}
|
|
2238
|
+
const recorded = recordBundledVersions(outfile, dir);
|
|
2239
|
+
if (recorded.length > 0) {
|
|
2240
|
+
console.log(` ${c5.green("\u2713")} ${c5.dim(`recorded bundled ${recorded.join(", ")}`)}`);
|
|
2241
|
+
}
|
|
2128
2242
|
const bundleBytes = readFileSync2(outfile);
|
|
2129
2243
|
const engineSha256 = createHash("sha256").update(bundleBytes).digest("hex");
|
|
2130
2244
|
const bundleText = bundleBytes.toString("utf8").replace(/\/\*[\s\S]*?\*\//g, "").replace(/(^|\s)\/\/[^\n]*/g, "$1");
|
|
@@ -2136,21 +2250,29 @@ async function extensionPackCommand(opts) {
|
|
|
2136
2250
|
}
|
|
2137
2251
|
const sizeKb = Math.round(bundleBytes.byteLength / 1024);
|
|
2138
2252
|
console.log(` ${c5.green("\u2713")} ${c5.bold(`engine/index.js`)} ${c5.dim(`(${sizeKb} KB, sha256=${engineSha256.slice(0, 12)}\u2026)`)}`);
|
|
2139
|
-
let
|
|
2140
|
-
if (!
|
|
2253
|
+
let communityInject = false;
|
|
2254
|
+
if (!manifest.engine?.isolation) {
|
|
2141
2255
|
const resolved = await resolvePublisherTier({
|
|
2142
2256
|
firstParty: opts.firstParty,
|
|
2143
2257
|
token: opts.token,
|
|
2144
2258
|
registryUrl: opts.registryUrl
|
|
2145
2259
|
});
|
|
2146
2260
|
if (!resolved.allowsInline) {
|
|
2147
|
-
|
|
2261
|
+
communityInject = true;
|
|
2148
2262
|
console.log(` ${c5.yellow("\u26A0")} ${resolved.tier} publisher \u2014 auto-set ${c5.bold('engine.isolation: "worker"')} ` + `(community extensions can't run inline).`);
|
|
2149
2263
|
if (resolved.source === "default") {
|
|
2150
2264
|
console.log(c5.dim(" Tier defaulted to community (no --first-party flag, no registry token). " + "If you are verified/first-party, pass --first-party or set ZVELTIO_REGISTRY_TOKEN."));
|
|
2151
2265
|
}
|
|
2152
2266
|
}
|
|
2267
|
+
} else if (opts.firstParty && manifest.engine.isolation === "worker" && !opts.keepIsolation) {
|
|
2268
|
+
console.log(` ${c5.yellow("\u26A0")} first-party pack \u2014 clearing sticky ${c5.bold('engine.isolation: "worker"')} ` + `(inline is the first-party default). Pass ${c5.bold("--keep-isolation")} to retain worker.`);
|
|
2153
2269
|
}
|
|
2270
|
+
const resolvedIsolation = resolvePackIsolation({
|
|
2271
|
+
current: manifest.engine?.isolation,
|
|
2272
|
+
firstParty: opts.firstParty,
|
|
2273
|
+
keepIsolation: opts.keepIsolation,
|
|
2274
|
+
communityInject
|
|
2275
|
+
});
|
|
2154
2276
|
if (!opts.noManifestUpdate) {
|
|
2155
2277
|
manifest.engine = {
|
|
2156
2278
|
entry: "engine/index.js",
|
|
@@ -2162,6 +2284,7 @@ async function extensionPackCommand(opts) {
|
|
|
2162
2284
|
};
|
|
2163
2285
|
manifest.integrity = {
|
|
2164
2286
|
engineSha256,
|
|
2287
|
+
sourceSha256: hashEngineSources(dir),
|
|
2165
2288
|
...manifest.integrity?.archiveSha256 ? { archiveSha256: manifest.integrity.archiveSha256 } : {}
|
|
2166
2289
|
};
|
|
2167
2290
|
writeManifest(dir, manifest);
|
|
@@ -2197,6 +2320,7 @@ ${c14.bold("Extension status")}
|
|
|
2197
2320
|
console.log(` Registry: ${c14.dim(registryUrl)}
|
|
2198
2321
|
`);
|
|
2199
2322
|
const listRes = await fetch(`${registryUrl}/api/extensions/list`).catch(() => null);
|
|
2323
|
+
unreachable(listRes);
|
|
2200
2324
|
if (listRes?.ok) {
|
|
2201
2325
|
const body = await listRes.json();
|
|
2202
2326
|
const found = body.extensions.find((e) => e.name === name);
|
|
@@ -2208,6 +2332,7 @@ ${c14.bold("Extension status")}
|
|
|
2208
2332
|
}
|
|
2209
2333
|
}
|
|
2210
2334
|
const detailRes = await fetch(`${registryUrl}/api/dev/extensions/by-name/${encodeURIComponent(name)}`).catch(() => null);
|
|
2335
|
+
unreachable(detailRes);
|
|
2211
2336
|
if (detailRes?.ok) {
|
|
2212
2337
|
const body = await detailRes.json();
|
|
2213
2338
|
const statusColor = body.status === "published" ? c14.green : body.status === "pending" ? c14.yellow : c14.red;
|
|
@@ -2242,6 +2367,12 @@ ${c14.bold("Extension status")}
|
|
|
2242
2367
|
` + " - First-party / bundled extensions are not surfaced by this command \u2014 they ship with the engine binary directly."));
|
|
2243
2368
|
console.log("");
|
|
2244
2369
|
}
|
|
2370
|
+
function unreachable(res) {
|
|
2371
|
+
if (res && res.status < 500)
|
|
2372
|
+
return;
|
|
2373
|
+
console.error(c14.red(` \u2717 Registry unreachable (${res ? `HTTP ${res.status}` : "network error"}).`));
|
|
2374
|
+
process.exit(1);
|
|
2375
|
+
}
|
|
2245
2376
|
var c14;
|
|
2246
2377
|
var init_extension_status = __esm(() => {
|
|
2247
2378
|
c14 = {
|
|
@@ -3104,10 +3235,13 @@ function findDockerfile(cwd) {
|
|
|
3104
3235
|
return null;
|
|
3105
3236
|
}
|
|
3106
3237
|
async function generateDockerfile(cwd, projectName) {
|
|
3107
|
-
|
|
3108
|
-
|
|
3109
|
-
|
|
3110
|
-
|
|
3238
|
+
const candidates = [
|
|
3239
|
+
"packages/engine/src/binary-entry.ts",
|
|
3240
|
+
"packages/engine/src/index.ts",
|
|
3241
|
+
"src/binary-entry.ts",
|
|
3242
|
+
"src/index.ts"
|
|
3243
|
+
];
|
|
3244
|
+
const entryPoint = candidates.find((f) => existsSync4(join4(cwd, f))) ?? "index.ts";
|
|
3111
3245
|
await Bun.write(join4(cwd, "Dockerfile"), `# Auto-generated by zveltio deploy
|
|
3112
3246
|
# Edit this file to customise your production image.
|
|
3113
3247
|
FROM oven/bun:1-alpine AS base
|
|
@@ -3140,6 +3274,40 @@ CMD ["./dist/${projectName}"]
|
|
|
3140
3274
|
`);
|
|
3141
3275
|
}
|
|
3142
3276
|
|
|
3277
|
+
// src/lib/engine-modules.ts
|
|
3278
|
+
import { existsSync as existsSync5 } from "fs";
|
|
3279
|
+
import { fileURLToPath } from "url";
|
|
3280
|
+
var CANDIDATES = [
|
|
3281
|
+
"../../../engine/src/db/",
|
|
3282
|
+
"../../engine/src/db/"
|
|
3283
|
+
];
|
|
3284
|
+
|
|
3285
|
+
class EngineModulesUnavailable extends Error {
|
|
3286
|
+
constructor() {
|
|
3287
|
+
super(`this CLI cannot reach the engine\u2019s migration runner.
|
|
3288
|
+
|
|
3289
|
+
` + ` The published @zveltio/cli package does not ship the engine, so the
|
|
3290
|
+
` + ` direct-to-database path only works from a repository checkout.
|
|
3291
|
+
|
|
3292
|
+
` + ` Use one of these instead:
|
|
3293
|
+
` + ` zveltio migrate # the release binary, with DATABASE_URL set
|
|
3294
|
+
` + " zveltio migrate --url http://host # against a running engine");
|
|
3295
|
+
this.name = "EngineModulesUnavailable";
|
|
3296
|
+
}
|
|
3297
|
+
}
|
|
3298
|
+
async function loadEngineMigrationModules() {
|
|
3299
|
+
for (const base of CANDIDATES) {
|
|
3300
|
+
const dbUrl = new URL(`${base}index.js`, import.meta.url);
|
|
3301
|
+
const path = fileURLToPath(dbUrl);
|
|
3302
|
+
if (!existsSync5(path) && !existsSync5(path.replace(/\.js$/, ".ts")))
|
|
3303
|
+
continue;
|
|
3304
|
+
const db = await import(dbUrl.href);
|
|
3305
|
+
const migrations = await import(new URL(`${base}migrations/index.js`, import.meta.url).href);
|
|
3306
|
+
return { db, migrations };
|
|
3307
|
+
}
|
|
3308
|
+
throw new EngineModulesUnavailable;
|
|
3309
|
+
}
|
|
3310
|
+
|
|
3143
3311
|
// src/commands/migrate.ts
|
|
3144
3312
|
var migrateCommand = new Command("migrate").description("Run pending database migrations").option("--url <url>", "Engine URL", "http://localhost:3000").option("--database-url <url>", "Direct database URL (skip engine)").option("--dry-run", "Show pending migrations without applying").action(async (opts) => {
|
|
3145
3313
|
console.log(`
|
|
@@ -3155,10 +3323,9 @@ var migrateCommand = new Command("migrate").description("Run pending database mi
|
|
|
3155
3323
|
async function runMigrationsDirectly(opts, databaseUrl) {
|
|
3156
3324
|
try {
|
|
3157
3325
|
process.env.DATABASE_URL = databaseUrl;
|
|
3158
|
-
const
|
|
3159
|
-
const
|
|
3160
|
-
const {
|
|
3161
|
-
const { runMigrations, getAppliedMigrations, getLastAppliedMigration } = await import(migrationsPath);
|
|
3326
|
+
const { db: dbModule, migrations } = await loadEngineMigrationModules();
|
|
3327
|
+
const { initDatabase } = dbModule;
|
|
3328
|
+
const { runMigrations, getAppliedMigrations, getLastAppliedMigration } = migrations;
|
|
3162
3329
|
const db = await initDatabase();
|
|
3163
3330
|
if (opts.dryRun) {
|
|
3164
3331
|
const applied = await getAppliedMigrations(db);
|
|
@@ -3229,37 +3396,41 @@ async function runMigrationsViaAPI(opts) {
|
|
|
3229
3396
|
}
|
|
3230
3397
|
|
|
3231
3398
|
// src/commands/extension.ts
|
|
3232
|
-
import { existsSync as
|
|
3399
|
+
import { existsSync as existsSync9 } from "fs";
|
|
3233
3400
|
import { mkdir, writeFile } from "fs/promises";
|
|
3234
3401
|
import { join as join8 } from "path";
|
|
3235
3402
|
async function extensionCommand(action, name, opts) {
|
|
3236
3403
|
switch (action) {
|
|
3237
3404
|
case "create":
|
|
3238
|
-
await createExtension(name, opts.category || "custom");
|
|
3405
|
+
await createExtension(name, opts.category || "custom", opts.codePage === true);
|
|
3239
3406
|
break;
|
|
3240
3407
|
case "build":
|
|
3241
3408
|
await buildExtension(opts);
|
|
3242
3409
|
break;
|
|
3243
|
-
case "dev":
|
|
3244
|
-
await devExtension();
|
|
3245
|
-
break;
|
|
3246
3410
|
}
|
|
3247
3411
|
}
|
|
3248
|
-
async function createExtension(name, category) {
|
|
3412
|
+
async function createExtension(name, category, codePage) {
|
|
3249
3413
|
const safeName = name.toLowerCase().replace(/[^a-z0-9-]/g, "-");
|
|
3414
|
+
const extName = `${category}/${safeName}`;
|
|
3415
|
+
const table = `zv_${safeName.replace(/-/g, "_")}_items`;
|
|
3250
3416
|
const targetDir = join8(process.cwd(), "extensions", category, safeName);
|
|
3251
|
-
if (
|
|
3417
|
+
if (existsSync9(targetDir)) {
|
|
3252
3418
|
console.error(`Extension already exists: ${targetDir}`);
|
|
3253
3419
|
process.exit(1);
|
|
3254
3420
|
}
|
|
3255
3421
|
console.log(`
|
|
3256
|
-
Creating extension: ${
|
|
3422
|
+
Creating extension: ${extName}
|
|
3257
3423
|
`);
|
|
3258
|
-
await mkdir(join8(targetDir, "engine"), { recursive: true });
|
|
3259
|
-
await mkdir(join8(targetDir, "studio", "src", "pages"), { recursive: true });
|
|
3260
3424
|
await mkdir(join8(targetDir, "engine", "migrations"), { recursive: true });
|
|
3425
|
+
await mkdir(join8(targetDir, "studio", "src", "components"), { recursive: true });
|
|
3426
|
+
if (codePage) {
|
|
3427
|
+
await mkdir(join8(targetDir, "studio", "pages"), { recursive: true });
|
|
3428
|
+
} else {
|
|
3429
|
+
await mkdir(join8(targetDir, "studio", "schemas"), { recursive: true });
|
|
3430
|
+
await mkdir(join8(targetDir, "studio", "messages"), { recursive: true });
|
|
3431
|
+
}
|
|
3261
3432
|
await writeFile(join8(targetDir, "manifest.json"), JSON.stringify({
|
|
3262
|
-
name:
|
|
3433
|
+
name: extName,
|
|
3263
3434
|
package: `@zveltio/ext-${safeName}`,
|
|
3264
3435
|
category,
|
|
3265
3436
|
displayName: name,
|
|
@@ -3268,13 +3439,29 @@ Creating extension: ${category}/${safeName}
|
|
|
3268
3439
|
zveltioMinVersion: "1.0.0",
|
|
3269
3440
|
zveltioMaxVersion: "4.0.0",
|
|
3270
3441
|
permissions: ["database"],
|
|
3271
|
-
|
|
3442
|
+
studio: {
|
|
3443
|
+
pages: [
|
|
3444
|
+
{
|
|
3445
|
+
path: `/admin/${safeName}`,
|
|
3446
|
+
label: name,
|
|
3447
|
+
icon: "Puzzle",
|
|
3448
|
+
...codePage ? {} : { schema: `schemas/${safeName}.json` }
|
|
3449
|
+
}
|
|
3450
|
+
],
|
|
3451
|
+
navGroup: "developer"
|
|
3452
|
+
},
|
|
3453
|
+
contributes: {
|
|
3454
|
+
engine: true,
|
|
3455
|
+
studio: true,
|
|
3456
|
+
fieldTypes: [],
|
|
3457
|
+
slots: []
|
|
3458
|
+
}
|
|
3272
3459
|
}, null, 2));
|
|
3273
3460
|
await writeFile(join8(targetDir, "engine", "index.ts"), `import type { ZveltioExtension } from '@zveltio/sdk/extension';
|
|
3274
3461
|
import { join } from 'path';
|
|
3275
3462
|
|
|
3276
3463
|
const extension: ZveltioExtension = {
|
|
3277
|
-
name: '${
|
|
3464
|
+
name: '${extName}',
|
|
3278
3465
|
category: '${category}',
|
|
3279
3466
|
|
|
3280
3467
|
getMigrations() {
|
|
@@ -3284,88 +3471,120 @@ const extension: ZveltioExtension = {
|
|
|
3284
3471
|
},
|
|
3285
3472
|
|
|
3286
3473
|
async register(app, ctx) {
|
|
3287
|
-
//
|
|
3288
|
-
|
|
3474
|
+
// The route the generated page reads. ctx.db resolves the caller's tenant
|
|
3475
|
+
// transaction, so this returns only that tenant's rows.
|
|
3476
|
+
app.get('/items', async (c) => {
|
|
3477
|
+
const data = await ctx.db
|
|
3478
|
+
.selectFrom('${table}')
|
|
3479
|
+
.selectAll()
|
|
3480
|
+
.orderBy('created_at', 'desc')
|
|
3481
|
+
.limit(100)
|
|
3482
|
+
.execute();
|
|
3483
|
+
return c.json({ data });
|
|
3484
|
+
});
|
|
3289
3485
|
},
|
|
3290
3486
|
};
|
|
3291
3487
|
|
|
3292
3488
|
export default extension;
|
|
3293
3489
|
`);
|
|
3294
|
-
await writeFile(join8(targetDir, "engine", "migrations", "001_init.sql"), `-- ${name}
|
|
3295
|
-
--
|
|
3490
|
+
await writeFile(join8(targetDir, "engine", "migrations", "001_init.sql"), `-- ${name} \u2014 initial schema.
|
|
3491
|
+
--
|
|
3492
|
+
-- Tenant isolation is not optional and not something to add later: a table
|
|
3493
|
+
-- without it is readable by every tenant on the instance. The three parts below
|
|
3494
|
+
-- are the whole pattern.
|
|
3495
|
+
|
|
3496
|
+
CREATE TABLE IF NOT EXISTS ${table} (
|
|
3497
|
+
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
3498
|
+
name TEXT NOT NULL,
|
|
3499
|
+
-- 1. The column, defaulting to the tenant of the transaction doing the write,
|
|
3500
|
+
-- so application code never has to name it (and cannot forge it).
|
|
3501
|
+
tenant_id UUID NOT NULL DEFAULT COALESCE(
|
|
3502
|
+
NULLIF(current_setting('zveltio.current_tenant', true), '')::uuid,
|
|
3503
|
+
'00000000-0000-0000-0000-000000000001'::uuid
|
|
3504
|
+
),
|
|
3505
|
+
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
|
3506
|
+
);
|
|
3296
3507
|
|
|
3297
|
-
|
|
3298
|
-
|
|
3299
|
-
--
|
|
3300
|
-
--
|
|
3301
|
-
|
|
3302
|
-
|
|
3508
|
+
CREATE INDEX IF NOT EXISTS idx_${table}_tenant ON ${table}(tenant_id);
|
|
3509
|
+
|
|
3510
|
+
-- 2. FORCE matters. Without it Postgres lets the table owner bypass the policy,
|
|
3511
|
+
-- and the engine connects as owner on a stock install \u2014 RLS becomes advisory.
|
|
3512
|
+
ALTER TABLE ${table} ENABLE ROW LEVEL SECURITY;
|
|
3513
|
+
ALTER TABLE ${table} FORCE ROW LEVEL SECURITY;
|
|
3514
|
+
|
|
3515
|
+
-- 3. The host's own predicate, not a hand-rolled copy: reads and writes agree
|
|
3516
|
+
-- with the column DEFAULT above, and the engine reconciles every policy
|
|
3517
|
+
-- named \`tenant_isolation_*\` onto it at boot.
|
|
3518
|
+
DROP POLICY IF EXISTS tenant_isolation_${table} ON ${table};
|
|
3519
|
+
CREATE POLICY tenant_isolation_${table} ON ${table}
|
|
3520
|
+
USING (tenant_id = ANY (zveltio_visible_tenants()))
|
|
3521
|
+
WITH CHECK (zveltio_tenant_write_ok(tenant_id));
|
|
3303
3522
|
`);
|
|
3304
|
-
|
|
3523
|
+
if (codePage) {
|
|
3524
|
+
await writeFile(join8(targetDir, "studio", "pages", "+page.svelte"), `<script lang="ts">
|
|
3525
|
+
import { api } from '$lib/api.js';
|
|
3305
3526
|
|
|
3306
|
-
|
|
3307
|
-
|
|
3308
|
-
|
|
3309
|
-
|
|
3310
|
-
return;
|
|
3311
|
-
}
|
|
3527
|
+
// A code page owns its fetching, loading and error states, and its strings
|
|
3528
|
+
// have to reach i18n by hand. A schema gets all three from the host \u2014 worth
|
|
3529
|
+
// re-checking that this page really needs to be code.
|
|
3530
|
+
let items = $state<Array<{ id: string; name: string }>>([]);
|
|
3312
3531
|
|
|
3313
|
-
|
|
3314
|
-
|
|
3315
|
-
|
|
3316
|
-
|
|
3317
|
-
|
|
3318
|
-
|
|
3532
|
+
$effect(() => {
|
|
3533
|
+
api
|
|
3534
|
+
.get<{ data: Array<{ id: string; name: string }> }>('/ext/${extName}/items')
|
|
3535
|
+
.then((r) => {
|
|
3536
|
+
items = r.data;
|
|
3537
|
+
});
|
|
3319
3538
|
});
|
|
3320
|
-
}
|
|
3321
|
-
`);
|
|
3322
|
-
await writeFile(join8(targetDir, "studio", "src", "pages", "MainPage.svelte"), `<script lang="ts">
|
|
3323
|
-
const engineUrl = (window as any).__ZVELTIO_ENGINE_URL__;
|
|
3324
3539
|
</script>
|
|
3325
3540
|
|
|
3326
3541
|
<div class="space-y-6">
|
|
3327
3542
|
<h1 class="text-2xl font-bold">${name}</h1>
|
|
3328
|
-
|
|
3543
|
+
{#each items as item (item.id)}
|
|
3544
|
+
<div>{item.name}</div>
|
|
3545
|
+
{/each}
|
|
3329
3546
|
</div>
|
|
3330
3547
|
`);
|
|
3331
|
-
|
|
3332
|
-
|
|
3333
|
-
|
|
3334
|
-
|
|
3335
|
-
|
|
3336
|
-
|
|
3337
|
-
|
|
3338
|
-
|
|
3339
|
-
|
|
3340
|
-
|
|
3341
|
-
|
|
3342
|
-
|
|
3343
|
-
|
|
3344
|
-
|
|
3345
|
-
|
|
3346
|
-
|
|
3347
|
-
|
|
3348
|
-
|
|
3349
|
-
|
|
3350
|
-
|
|
3351
|
-
|
|
3352
|
-
|
|
3353
|
-
})
|
|
3548
|
+
} else {
|
|
3549
|
+
await writeFile(join8(targetDir, "studio", "schemas", `${safeName}.json`), `${JSON.stringify({
|
|
3550
|
+
sduiSchema: 1,
|
|
3551
|
+
title: `${safeName}.title`,
|
|
3552
|
+
subtitle: `${safeName}.subtitle`,
|
|
3553
|
+
resources: [
|
|
3554
|
+
{
|
|
3555
|
+
id: "items",
|
|
3556
|
+
dataSource: `/ext/${extName}/items`,
|
|
3557
|
+
dataPath: "data",
|
|
3558
|
+
search: { fields: ["name"], placeholder: "common.search" },
|
|
3559
|
+
columns: [
|
|
3560
|
+
{ key: "name", label: "common.col.name" },
|
|
3561
|
+
{ key: "created_at", label: "common.col.created", type: "date" }
|
|
3562
|
+
]
|
|
3563
|
+
}
|
|
3564
|
+
]
|
|
3565
|
+
}, null, 2)}
|
|
3566
|
+
`);
|
|
3567
|
+
await writeFile(join8(targetDir, "studio", "messages", "en.json"), `${JSON.stringify({
|
|
3568
|
+
[`${safeName}.title`]: name,
|
|
3569
|
+
[`${safeName}.subtitle`]: `Manage ${name.toLowerCase()} records`
|
|
3570
|
+
}, null, 2)}
|
|
3571
|
+
`);
|
|
3572
|
+
}
|
|
3573
|
+
await writeFile(join8(targetDir, "studio", "src", "contribute.ts.example"), `/**
|
|
3574
|
+
* Rename to contribute.ts to register dashboard/settings slot widgets.
|
|
3575
|
+
* Synced by packages/studio/scripts/sync-extensions.ts \u2014 no studio/dist/ build.
|
|
3576
|
+
*
|
|
3577
|
+
* import { registerContributionSlot } from '$lib/extension-api.svelte.js';
|
|
3578
|
+
* import MyWidget from './components/MyWidget.svelte';
|
|
3579
|
+
*
|
|
3580
|
+
* export function activate(): void {
|
|
3581
|
+
* registerContributionSlot('${extName}', 'dashboard.widgets', {
|
|
3582
|
+
* component: MyWidget,
|
|
3583
|
+
* priority: 100,
|
|
3584
|
+
* });
|
|
3585
|
+
* }
|
|
3586
|
+
*/
|
|
3354
3587
|
`);
|
|
3355
|
-
await writeFile(join8(targetDir, "studio", "package.json"), JSON.stringify({
|
|
3356
|
-
name: `@zveltio/ext-${safeName}-studio`,
|
|
3357
|
-
version: "1.0.0",
|
|
3358
|
-
type: "module",
|
|
3359
|
-
scripts: {
|
|
3360
|
-
build: "vite build",
|
|
3361
|
-
dev: "vite build --watch"
|
|
3362
|
-
},
|
|
3363
|
-
dependencies: { svelte: "^5.0.0" },
|
|
3364
|
-
devDependencies: {
|
|
3365
|
-
"@sveltejs/vite-plugin-svelte": "^4.0.0",
|
|
3366
|
-
vite: "^6.0.0"
|
|
3367
|
-
}
|
|
3368
|
-
}, null, 2));
|
|
3369
3588
|
await writeFile(join8(targetDir, ".gitattributes"), `* text=auto eol=lf
|
|
3370
3589
|
|
|
3371
3590
|
# Packed engine bundle: byte-identical across OSes. Manifest
|
|
@@ -3392,57 +3611,60 @@ jobs:
|
|
|
3392
3611
|
- name: Install dependencies
|
|
3393
3612
|
run: bun install
|
|
3394
3613
|
|
|
3614
|
+
# Validate FIRST, on the COMMITTED bundle. It checks the bundle against
|
|
3615
|
+
# manifest integrity.engineSha256 and against the source it was built
|
|
3616
|
+
# from, so a bundle nobody repacked fails here. Running pack before this
|
|
3617
|
+
# would rewrite both hashes and the check could never fail.
|
|
3618
|
+
- name: Validate committed bundle, manifest + structure
|
|
3619
|
+
run: bunx @zveltio/cli extension validate
|
|
3620
|
+
|
|
3395
3621
|
- name: Pack extension (engine/index.ts \u2192 engine/index.js + manifest hash)
|
|
3396
3622
|
run: bunx @zveltio/cli extension pack
|
|
3397
|
-
|
|
3398
|
-
- name: Verify committed bundle matches manifest engineSha256
|
|
3399
|
-
# If pack changed engine/index.js, the committed bytes drift
|
|
3400
|
-
# from manifest.integrity.engineSha256. The publishing pipeline
|
|
3401
|
-
# would reject this \u2014 fail PR before merging.
|
|
3402
|
-
run: |
|
|
3403
|
-
actual=$(sha256sum engine/index.js | awk '{print $1}')
|
|
3404
|
-
declared=$(node -e "console.log(require('./manifest.json').integrity?.engineSha256 ?? '')")
|
|
3405
|
-
if [ "$actual" != "$declared" ]; then
|
|
3406
|
-
echo "::error::Bundle hash $actual \u2260 manifest engineSha256 $declared"
|
|
3407
|
-
echo "::error::Run \\\`bunx @zveltio/cli extension pack\\\` locally and commit the result."
|
|
3408
|
-
exit 1
|
|
3409
|
-
fi
|
|
3410
|
-
|
|
3411
|
-
- name: Validate manifest + structure
|
|
3412
|
-
run: bunx @zveltio/cli extension validate
|
|
3413
3623
|
`);
|
|
3624
|
+
const pageLines = codePage ? ` pages/
|
|
3625
|
+
+page.svelte <- code page (synced into Studio at release)` : ` schemas/
|
|
3626
|
+
${safeName}.json <- THE PAGE (rendered by the host; no build step)
|
|
3627
|
+
messages/
|
|
3628
|
+
en.json <- keys this schema owns; other locales filled from en`;
|
|
3414
3629
|
console.log(`Extension scaffolded at extensions/${category}/${safeName}/
|
|
3415
3630
|
|
|
3416
|
-
Structure:
|
|
3417
3631
|
engine/
|
|
3418
|
-
index.ts <- API routes
|
|
3419
|
-
migrations/ <- SQL
|
|
3632
|
+
index.ts <- API routes (mounted at /ext/${extName}/*)
|
|
3633
|
+
migrations/ <- SQL, with tenant isolation already wired
|
|
3420
3634
|
studio/
|
|
3635
|
+
${pageLines}
|
|
3421
3636
|
src/
|
|
3422
|
-
|
|
3423
|
-
|
|
3424
|
-
vite.config.ts
|
|
3637
|
+
components/ <- shared Svelte (optional)
|
|
3638
|
+
contribute.ts.example <- rename to contribute.ts for a dashboard widget
|
|
3425
3639
|
manifest.json
|
|
3426
|
-
.gitattributes
|
|
3427
|
-
.github/workflows/
|
|
3428
|
-
ci.yml <- pack + hash verify + validate on every PR
|
|
3640
|
+
.gitattributes
|
|
3641
|
+
.github/workflows/ci.yml
|
|
3429
3642
|
|
|
3430
3643
|
Next steps:
|
|
3431
|
-
1.
|
|
3432
|
-
2.
|
|
3433
|
-
3.
|
|
3434
|
-
|
|
3644
|
+
1. Put your logic in engine/index.ts \u2014 that is where the truth lives
|
|
3645
|
+
2. ${codePage ? "Write the page in studio/pages/+page.svelte" : `Shape the page in studio/schemas/${safeName}.json`}
|
|
3646
|
+
3. \`bunx @zveltio/cli extension pack\` \u2014 produces engine/index.js + integrity
|
|
3647
|
+
(validate needs this: it rejects a manifest with no engine block)
|
|
3648
|
+
4. \`bunx @zveltio/cli extension validate\`${codePage ? " \u2014 manifest, migrations and bundle" : ` \u2014 checks the schema against the
|
|
3649
|
+
routes you actually serve, and the message keys against what you ship`}
|
|
3650
|
+
5. Local preview: \`cd packages/studio && bun scripts/sync-extensions.ts\`,
|
|
3651
|
+
then \`bun run dev\` \u2014 see developer-guide.md \xA72
|
|
3435
3652
|
|
|
3436
|
-
|
|
3437
|
-
|
|
3438
|
-
|
|
3439
|
-
|
|
3440
|
-
registry token). Worker mode = Bun.Worker with crash isolation and no
|
|
3441
|
-
direct DB credentials.
|
|
3653
|
+
Where UI goes:
|
|
3654
|
+
a page -> studio/schemas/*.json (almost always)
|
|
3655
|
+
a widget on the dashboard -> studio/src/contribute.ts (occasionally)
|
|
3656
|
+
canvas / chat / map / IDE -> studio/pages/+page.svelte (rarely)
|
|
3442
3657
|
|
|
3443
|
-
|
|
3444
|
-
--first-party. See https://github.com/zveltio-devs/zveltio/blob/master/docs/EXTENSION-DEVELOPER-GUIDE.md#135-isolation-tiers-be-honest-about-what-you-ship
|
|
3658
|
+
Do NOT add studio/vite.config.ts, studio/package.json, or studio/dist/ \u2014 removed in alpha.94/beta.15.
|
|
3445
3659
|
`);
|
|
3660
|
+
if (codePage) {
|
|
3661
|
+
console.warn(`\x1B[33mScaffolded a CODE page. Across the 56 shipped extensions, 61 pages are
|
|
3662
|
+
schemas and seven are code \u2014 and each of those seven sits beside a schema, for
|
|
3663
|
+
the part a schema could not express. On this road you own fetching,
|
|
3664
|
+
loading and error states, i18n by hand, and a Studio release to ship a change.
|
|
3665
|
+
Worth it for a canvas, a chat, a map or an inbox \u2014 for a list, a form or
|
|
3666
|
+
settings, delete studio/pages/ and add \`schema\` back to manifest.studio.pages[0].\x1B[0m`);
|
|
3667
|
+
}
|
|
3446
3668
|
}
|
|
3447
3669
|
async function buildExtension(opts) {
|
|
3448
3670
|
console.warn("\x1B[33m`zveltio extension build` is deprecated \u2014 use `zveltio extension pack`.\x1B[0m");
|
|
@@ -3450,26 +3672,10 @@ async function buildExtension(opts) {
|
|
|
3450
3672
|
const { extensionPackCommand: extensionPackCommand2 } = await Promise.resolve().then(() => (init_extension_pack(), exports_extension_pack));
|
|
3451
3673
|
await extensionPackCommand2({ dir: opts.dir });
|
|
3452
3674
|
}
|
|
3453
|
-
async function devExtension() {
|
|
3454
|
-
console.log(`
|
|
3455
|
-
Starting extension dev mode...
|
|
3456
|
-
`);
|
|
3457
|
-
const studioProc = Bun.spawn(["bun", "run", "dev"], {
|
|
3458
|
-
cwd: "studio",
|
|
3459
|
-
stdout: "inherit",
|
|
3460
|
-
stderr: "inherit"
|
|
3461
|
-
});
|
|
3462
|
-
console.log(" Studio: watching for changes...");
|
|
3463
|
-
process.on("SIGINT", () => {
|
|
3464
|
-
studioProc.kill();
|
|
3465
|
-
process.exit(0);
|
|
3466
|
-
});
|
|
3467
|
-
await studioProc.exited;
|
|
3468
|
-
}
|
|
3469
3675
|
|
|
3470
3676
|
// src/commands/extension-types.ts
|
|
3471
|
-
import { existsSync as
|
|
3472
|
-
import { join as join9, relative } from "path";
|
|
3677
|
+
import { existsSync as existsSync10, readdirSync as readdirSync2, readFileSync as readFileSync3, mkdirSync as mkdirSync2, writeFileSync as writeFileSync2 } from "fs";
|
|
3678
|
+
import { join as join9, relative as relative2 } from "path";
|
|
3473
3679
|
|
|
3474
3680
|
// ../sdk/dist/schema-codegen.js
|
|
3475
3681
|
var NUMERIC_TYPES = new Set([
|
|
@@ -3539,6 +3745,10 @@ function mapPgTypeToTs(pgType) {
|
|
|
3539
3745
|
tsBase = "unknown";
|
|
3540
3746
|
return isArray ? `${tsBase}[]` : tsBase;
|
|
3541
3747
|
}
|
|
3748
|
+
function upSection(sql) {
|
|
3749
|
+
const i = sql.search(/^--\s*DOWN\s*$/im);
|
|
3750
|
+
return i < 0 ? sql : sql.slice(0, i);
|
|
3751
|
+
}
|
|
3542
3752
|
function stripComments(sql) {
|
|
3543
3753
|
let out = "";
|
|
3544
3754
|
let i = 0;
|
|
@@ -3716,13 +3926,12 @@ function parseSchema(sqlChunks) {
|
|
|
3716
3926
|
}
|
|
3717
3927
|
return tables.get(name);
|
|
3718
3928
|
};
|
|
3719
|
-
const allSql = sqlChunks.map(stripComments).join(`
|
|
3929
|
+
const allSql = sqlChunks.map(upSection).map(stripComments).join(`
|
|
3720
3930
|
`);
|
|
3721
3931
|
const createRe = /CREATE\s+TABLE\s+(?:IF\s+NOT\s+EXISTS\s+)?(?:"([^"]+)"|([a-zA-Z_][a-zA-Z0-9_]*))\s*\(/gi;
|
|
3722
|
-
|
|
3723
|
-
while ((m = createRe.exec(allSql)) !== null) {
|
|
3932
|
+
for (const m of allSql.matchAll(createRe)) {
|
|
3724
3933
|
const tableName = m[1] ?? m[2];
|
|
3725
|
-
const openIdx =
|
|
3934
|
+
const openIdx = m.index + m[0].length - 1;
|
|
3726
3935
|
const closeIdx = findMatchingParen(allSql, openIdx);
|
|
3727
3936
|
if (closeIdx < 0)
|
|
3728
3937
|
continue;
|
|
@@ -3731,10 +3940,9 @@ function parseSchema(sqlChunks) {
|
|
|
3731
3940
|
const tableCols = ensureTable(tableName);
|
|
3732
3941
|
for (const c6 of columns)
|
|
3733
3942
|
tableCols.set(c6.name, c6);
|
|
3734
|
-
createRe.lastIndex = closeIdx + 1;
|
|
3735
3943
|
}
|
|
3736
3944
|
const alterRe = /ALTER\s+TABLE\s+(?:"([^"]+)"|([a-zA-Z_][a-zA-Z0-9_]*))\s+ADD\s+COLUMN\s+(?:IF\s+NOT\s+EXISTS\s+)?(?:"([^"]+)"|([a-zA-Z_][a-zA-Z0-9_]*))\s+([^;]+?)(?=;|$)/gi;
|
|
3737
|
-
|
|
3945
|
+
for (const m of allSql.matchAll(alterRe)) {
|
|
3738
3946
|
const tableName = m[1] ?? m[2];
|
|
3739
3947
|
const colName = m[3] ?? m[4];
|
|
3740
3948
|
const tail = m[5];
|
|
@@ -3744,6 +3952,10 @@ function parseSchema(sqlChunks) {
|
|
|
3744
3952
|
tableCols.set(col.name, col);
|
|
3745
3953
|
}
|
|
3746
3954
|
}
|
|
3955
|
+
const dropRe = /ALTER\s+TABLE\s+(?:IF\s+EXISTS\s+)?(?:"([^"]+)"|([a-zA-Z_][a-zA-Z0-9_]*))\s+DROP\s+COLUMN\s+(?:IF\s+EXISTS\s+)?(?:"([^"]+)"|([a-zA-Z_][a-zA-Z0-9_]*))/gi;
|
|
3956
|
+
for (const m of allSql.matchAll(dropRe)) {
|
|
3957
|
+
tables.get(m[1] ?? m[2])?.delete(m[3] ?? m[4]);
|
|
3958
|
+
}
|
|
3747
3959
|
return {
|
|
3748
3960
|
tables: order.map((name) => ({ name, columns: [...tables.get(name).values()] }))
|
|
3749
3961
|
};
|
|
@@ -3793,18 +4005,18 @@ ${c6.bold("Extension types")}
|
|
|
3793
4005
|
`);
|
|
3794
4006
|
console.log(` Extension dir: ${c6.dim(dir)}`);
|
|
3795
4007
|
const manifestPath = join9(dir, "manifest.json");
|
|
3796
|
-
if (!
|
|
4008
|
+
if (!existsSync10(manifestPath)) {
|
|
3797
4009
|
console.error(c6.red(`No manifest.json found in ${dir}`));
|
|
3798
4010
|
console.error(c6.dim(" Run this command from your extension's root folder, or pass --dir."));
|
|
3799
4011
|
process.exit(1);
|
|
3800
4012
|
}
|
|
3801
4013
|
const migrationsDir = join9(dir, "engine", "migrations");
|
|
3802
|
-
if (!
|
|
4014
|
+
if (!existsSync10(migrationsDir)) {
|
|
3803
4015
|
console.error(c6.yellow(`No engine/migrations directory found at ${migrationsDir}`));
|
|
3804
4016
|
console.error(c6.dim(" Nothing to generate. Add SQL migrations first."));
|
|
3805
4017
|
process.exit(0);
|
|
3806
4018
|
}
|
|
3807
|
-
const sqlFiles =
|
|
4019
|
+
const sqlFiles = readdirSync2(migrationsDir).filter((f) => f.endsWith(".sql")).sort();
|
|
3808
4020
|
if (sqlFiles.length === 0) {
|
|
3809
4021
|
console.error(c6.yellow(`No .sql files in ${migrationsDir}`));
|
|
3810
4022
|
process.exit(0);
|
|
@@ -3832,13 +4044,14 @@ ${c6.bold("Extension types")}
|
|
|
3832
4044
|
mkdirSync2(join9(outputPath, ".."), { recursive: true });
|
|
3833
4045
|
writeFileSync2(outputPath, tsBody, "utf8");
|
|
3834
4046
|
console.log(`
|
|
3835
|
-
${c6.green("Written:")} ${
|
|
4047
|
+
${c6.green("Written:")} ${relative2(process.cwd(), outputPath) || outputPath}`);
|
|
3836
4048
|
console.log(c6.dim(" Add `.zveltio/` to your .gitignore. Regenerate with this command after every migration."));
|
|
3837
4049
|
console.log("");
|
|
3838
4050
|
}
|
|
3839
4051
|
|
|
3840
4052
|
// src/commands/extension-validate.ts
|
|
3841
|
-
import {
|
|
4053
|
+
import { createHash as createHash2 } from "crypto";
|
|
4054
|
+
import { existsSync as existsSync11, readdirSync as readdirSync3, readFileSync as readFileSync4, statSync } from "fs";
|
|
3842
4055
|
import { join as join10 } from "path";
|
|
3843
4056
|
|
|
3844
4057
|
// ../sdk/dist/validate/migration-parse.js
|
|
@@ -3855,6 +4068,221 @@ function parseMigrationSql(raw) {
|
|
|
3855
4068
|
return { up, down: downBody.length > 0 ? downBody : null };
|
|
3856
4069
|
}
|
|
3857
4070
|
|
|
4071
|
+
// ../sdk/dist/validate/shared-message-keys.js
|
|
4072
|
+
var SHARED_MESSAGE_KEYS = new Set([
|
|
4073
|
+
"common.accessRestricted",
|
|
4074
|
+
"common.actions",
|
|
4075
|
+
"common.add",
|
|
4076
|
+
"common.added",
|
|
4077
|
+
"common.aiStudio",
|
|
4078
|
+
"common.approve",
|
|
4079
|
+
"common.assign",
|
|
4080
|
+
"common.bulkActions",
|
|
4081
|
+
"common.cancel",
|
|
4082
|
+
"common.cancelEdit",
|
|
4083
|
+
"common.clear",
|
|
4084
|
+
"common.clearSelection",
|
|
4085
|
+
"common.close",
|
|
4086
|
+
"common.col.account",
|
|
4087
|
+
"common.col.active",
|
|
4088
|
+
"common.col.address",
|
|
4089
|
+
"common.col.amount",
|
|
4090
|
+
"common.col.balance",
|
|
4091
|
+
"common.col.bank",
|
|
4092
|
+
"common.col.bcc",
|
|
4093
|
+
"common.col.category",
|
|
4094
|
+
"common.col.cc",
|
|
4095
|
+
"common.col.channel",
|
|
4096
|
+
"common.col.client",
|
|
4097
|
+
"common.col.code",
|
|
4098
|
+
"common.col.collection",
|
|
4099
|
+
"common.col.company",
|
|
4100
|
+
"common.col.created",
|
|
4101
|
+
"common.col.credit",
|
|
4102
|
+
"common.col.currency",
|
|
4103
|
+
"common.col.date",
|
|
4104
|
+
"common.col.days",
|
|
4105
|
+
"common.col.debit",
|
|
4106
|
+
"common.col.department",
|
|
4107
|
+
"common.col.description",
|
|
4108
|
+
"common.col.docNumber",
|
|
4109
|
+
"common.col.dueDate",
|
|
4110
|
+
"common.col.duration",
|
|
4111
|
+
"common.col.email",
|
|
4112
|
+
"common.col.employee",
|
|
4113
|
+
"common.col.enabled",
|
|
4114
|
+
"common.col.end",
|
|
4115
|
+
"common.col.event",
|
|
4116
|
+
"common.col.field",
|
|
4117
|
+
"common.col.file",
|
|
4118
|
+
"common.col.from",
|
|
4119
|
+
"common.col.hireDate",
|
|
4120
|
+
"common.col.host",
|
|
4121
|
+
"common.col.iban",
|
|
4122
|
+
"common.col.index",
|
|
4123
|
+
"common.col.key",
|
|
4124
|
+
"common.col.label",
|
|
4125
|
+
"common.col.leaveType",
|
|
4126
|
+
"common.col.locale",
|
|
4127
|
+
"common.col.message",
|
|
4128
|
+
"common.col.method",
|
|
4129
|
+
"common.col.model",
|
|
4130
|
+
"common.col.name",
|
|
4131
|
+
"common.col.notes",
|
|
4132
|
+
"common.col.number",
|
|
4133
|
+
"common.col.paid",
|
|
4134
|
+
"common.col.parent",
|
|
4135
|
+
"common.col.password",
|
|
4136
|
+
"common.col.path",
|
|
4137
|
+
"common.col.phone",
|
|
4138
|
+
"common.col.port",
|
|
4139
|
+
"common.col.position",
|
|
4140
|
+
"common.col.price",
|
|
4141
|
+
"common.col.priority",
|
|
4142
|
+
"common.col.product",
|
|
4143
|
+
"common.col.project",
|
|
4144
|
+
"common.col.provider",
|
|
4145
|
+
"common.col.quantity",
|
|
4146
|
+
"common.col.reason",
|
|
4147
|
+
"common.col.recipient",
|
|
4148
|
+
"common.col.records",
|
|
4149
|
+
"common.col.required",
|
|
4150
|
+
"common.col.resource",
|
|
4151
|
+
"common.col.role",
|
|
4152
|
+
"common.col.sku",
|
|
4153
|
+
"common.col.slug",
|
|
4154
|
+
"common.col.start",
|
|
4155
|
+
"common.col.status",
|
|
4156
|
+
"common.col.subject",
|
|
4157
|
+
"common.col.taxId",
|
|
4158
|
+
"common.col.template",
|
|
4159
|
+
"common.col.ticket",
|
|
4160
|
+
"common.col.time",
|
|
4161
|
+
"common.col.title",
|
|
4162
|
+
"common.col.to",
|
|
4163
|
+
"common.col.total",
|
|
4164
|
+
"common.col.type",
|
|
4165
|
+
"common.col.unique",
|
|
4166
|
+
"common.col.unit",
|
|
4167
|
+
"common.col.user",
|
|
4168
|
+
"common.col.username",
|
|
4169
|
+
"common.col.validUntil",
|
|
4170
|
+
"common.col.value",
|
|
4171
|
+
"common.col.vat",
|
|
4172
|
+
"common.col.vendor",
|
|
4173
|
+
"common.col.version",
|
|
4174
|
+
"common.col.warehouse",
|
|
4175
|
+
"common.col.year",
|
|
4176
|
+
"common.collectionRequired",
|
|
4177
|
+
"common.confirm",
|
|
4178
|
+
"common.copied",
|
|
4179
|
+
"common.copy",
|
|
4180
|
+
"common.copyShort",
|
|
4181
|
+
"common.create",
|
|
4182
|
+
"common.delete",
|
|
4183
|
+
"common.deleteCount",
|
|
4184
|
+
"common.deleteFailed",
|
|
4185
|
+
"common.discard",
|
|
4186
|
+
"common.dismiss",
|
|
4187
|
+
"common.dismissNotification",
|
|
4188
|
+
"common.done",
|
|
4189
|
+
"common.download",
|
|
4190
|
+
"common.edit",
|
|
4191
|
+
"common.emptyDash",
|
|
4192
|
+
"common.failed",
|
|
4193
|
+
"common.fields",
|
|
4194
|
+
"common.filter.admin",
|
|
4195
|
+
"common.filter.all",
|
|
4196
|
+
"common.filter.allStatuses",
|
|
4197
|
+
"common.filter.auth",
|
|
4198
|
+
"common.filter.data",
|
|
4199
|
+
"common.format",
|
|
4200
|
+
"common.generate",
|
|
4201
|
+
"common.loadFailed",
|
|
4202
|
+
"common.loading",
|
|
4203
|
+
"common.logout",
|
|
4204
|
+
"common.manage",
|
|
4205
|
+
"common.mergeFailed",
|
|
4206
|
+
"common.nameRequired",
|
|
4207
|
+
"common.new",
|
|
4208
|
+
"common.next",
|
|
4209
|
+
"common.no",
|
|
4210
|
+
"common.noResults",
|
|
4211
|
+
"common.notFound",
|
|
4212
|
+
"common.op.delete",
|
|
4213
|
+
"common.op.insert",
|
|
4214
|
+
"common.op.update",
|
|
4215
|
+
"common.open",
|
|
4216
|
+
"common.optional",
|
|
4217
|
+
"common.optionalParen",
|
|
4218
|
+
"common.other",
|
|
4219
|
+
"common.pageOf",
|
|
4220
|
+
"common.post",
|
|
4221
|
+
"common.prev",
|
|
4222
|
+
"common.publish",
|
|
4223
|
+
"common.rangeOf",
|
|
4224
|
+
"common.refresh",
|
|
4225
|
+
"common.reject",
|
|
4226
|
+
"common.remove",
|
|
4227
|
+
"common.rename",
|
|
4228
|
+
"common.reorder",
|
|
4229
|
+
"common.required",
|
|
4230
|
+
"common.resetFailed",
|
|
4231
|
+
"common.retry",
|
|
4232
|
+
"common.revoke",
|
|
4233
|
+
"common.save",
|
|
4234
|
+
"common.saveFailed",
|
|
4235
|
+
"common.saved",
|
|
4236
|
+
"common.saving",
|
|
4237
|
+
"common.search",
|
|
4238
|
+
"common.selectCollection",
|
|
4239
|
+
"common.selectPlaceholder",
|
|
4240
|
+
"common.selected",
|
|
4241
|
+
"common.send",
|
|
4242
|
+
"common.somethingWrong",
|
|
4243
|
+
"common.status.approved",
|
|
4244
|
+
"common.status.cancelled",
|
|
4245
|
+
"common.status.draft",
|
|
4246
|
+
"common.status.pending",
|
|
4247
|
+
"common.status.reimbursed",
|
|
4248
|
+
"common.status.rejected",
|
|
4249
|
+
"common.status.submitted",
|
|
4250
|
+
"common.submit",
|
|
4251
|
+
"common.sync",
|
|
4252
|
+
"common.testConnection",
|
|
4253
|
+
"common.typeToSearch",
|
|
4254
|
+
"common.undo",
|
|
4255
|
+
"common.unexpectedError",
|
|
4256
|
+
"common.unknownError",
|
|
4257
|
+
"common.upload",
|
|
4258
|
+
"common.uploadFailed",
|
|
4259
|
+
"common.uploading",
|
|
4260
|
+
"common.verifyingPermissions",
|
|
4261
|
+
"common.view",
|
|
4262
|
+
"common.viewAll",
|
|
4263
|
+
"common.yes",
|
|
4264
|
+
"ext.approved",
|
|
4265
|
+
"ext.confirm.deleteForm",
|
|
4266
|
+
"ext.confirm.deleteNamed",
|
|
4267
|
+
"ext.confirm.removeSearchIndex",
|
|
4268
|
+
"ext.confirmDelete",
|
|
4269
|
+
"ext.copied",
|
|
4270
|
+
"ext.copyFailed",
|
|
4271
|
+
"ext.created",
|
|
4272
|
+
"ext.deleted",
|
|
4273
|
+
"ext.errorPrefix",
|
|
4274
|
+
"ext.linkCopied",
|
|
4275
|
+
"ext.loadFailed",
|
|
4276
|
+
"ext.rejected",
|
|
4277
|
+
"ext.reveal.copy",
|
|
4278
|
+
"ext.reveal.note",
|
|
4279
|
+
"ext.reveal.title",
|
|
4280
|
+
"ext.saveFailed",
|
|
4281
|
+
"ext.saved",
|
|
4282
|
+
"ext.sent",
|
|
4283
|
+
"ext.submitted"
|
|
4284
|
+
]);
|
|
4285
|
+
|
|
3858
4286
|
// ../sdk/dist/validate/index.js
|
|
3859
4287
|
var SEMVER_RE = /^\d+\.\d+\.\d+(?:-[A-Za-z0-9.-]+)?$/;
|
|
3860
4288
|
var KNOWN_CATEGORIES = new Set([
|
|
@@ -3890,6 +4318,9 @@ var SAFE_VERSION_RANGE = /^[\d.*^~>=<| -]+$/;
|
|
|
3890
4318
|
function err(code, message, file) {
|
|
3891
4319
|
return file ? { code, message, file } : { code, message };
|
|
3892
4320
|
}
|
|
4321
|
+
function warn(code, message, file) {
|
|
4322
|
+
return { ...err(code, message, file), severity: "warning" };
|
|
4323
|
+
}
|
|
3893
4324
|
function validateManifest(input) {
|
|
3894
4325
|
const out = [];
|
|
3895
4326
|
const m = input.manifest;
|
|
@@ -3916,8 +4347,16 @@ function validateManifest(input) {
|
|
|
3916
4347
|
if (typeof obj.zveltioMaxVersion === "string" && !SEMVER_RE.test(obj.zveltioMaxVersion)) {
|
|
3917
4348
|
out.push(err("MANIFEST_BAD_MAX_VERSION", `zveltioMaxVersion "${obj.zveltioMaxVersion}" is not valid semver`));
|
|
3918
4349
|
}
|
|
4350
|
+
if (obj.apiKeyRoutes !== undefined) {
|
|
4351
|
+
const routes = Array.isArray(obj.apiKeyRoutes) ? obj.apiKeyRoutes : [obj.apiKeyRoutes];
|
|
4352
|
+
for (const r of routes) {
|
|
4353
|
+
if (typeof r !== "string" || !/^(GET|POST|PUT|PATCH|DELETE) \S+$/.test(r)) {
|
|
4354
|
+
out.push(err("MANIFEST_BAD_API_KEY_ROUTE", `apiKeyRoutes entry ${JSON.stringify(r)} must be "<GET|POST|PUT|PATCH|DELETE> <pattern>"`));
|
|
4355
|
+
}
|
|
4356
|
+
}
|
|
4357
|
+
}
|
|
3919
4358
|
if (typeof obj.category === "string" && !KNOWN_CATEGORIES.has(obj.category)) {
|
|
3920
|
-
out.push(
|
|
4359
|
+
out.push(warn("MANIFEST_UNKNOWN_CATEGORY", `category "${obj.category}" isn't in the known set \u2014 typo? Known: ${[...KNOWN_CATEGORIES].sort().join(", ")}`));
|
|
3921
4360
|
}
|
|
3922
4361
|
return out;
|
|
3923
4362
|
}
|
|
@@ -3949,12 +4388,111 @@ function hasDestructiveDdl(upSql) {
|
|
|
3949
4388
|
return true;
|
|
3950
4389
|
if (/\bDROP\s+COLUMN\b/i.test(s))
|
|
3951
4390
|
return true;
|
|
3952
|
-
if (/\bALTER\s+COLUMN\b[\s\S]*?\bDROP\s+(NOT\s+NULL|DEFAULT)\b/i.test(s))
|
|
3953
|
-
return true;
|
|
3954
4391
|
return false;
|
|
3955
4392
|
}
|
|
4393
|
+
function userRefUuidColumns(upSql) {
|
|
4394
|
+
const s = upSql.replace(/--[^\n]*/g, "").replace(/\/\*[\s\S]*?\*\//g, "");
|
|
4395
|
+
const found = new Set;
|
|
4396
|
+
for (const m of s.matchAll(/(\b[a-z_][a-z0-9_]*_by)\s+UUID\b/gi)) {
|
|
4397
|
+
found.add(m[1].toLowerCase());
|
|
4398
|
+
}
|
|
4399
|
+
for (const m of s.matchAll(/ALTER\s+COLUMN\s+(\b[a-z_][a-z0-9_]*_by)\s+TYPE\s+UUID\b/gi)) {
|
|
4400
|
+
found.add(m[1].toLowerCase());
|
|
4401
|
+
}
|
|
4402
|
+
return [...found];
|
|
4403
|
+
}
|
|
4404
|
+
function userRefFixedColumns(upSql) {
|
|
4405
|
+
const s = upSql.replace(/--[^\n]*/g, "").replace(/\/\*[\s\S]*?\*\//g, "");
|
|
4406
|
+
return [
|
|
4407
|
+
...new Set([...s.matchAll(/ALTER\s+COLUMN\s+(\b[a-z_][a-z0-9_]*_by)\s+TYPE\s+TEXT\b/gi)].map((m) => m[1].toLowerCase()))
|
|
4408
|
+
];
|
|
4409
|
+
}
|
|
4410
|
+
function createTableColumnDefs(sql) {
|
|
4411
|
+
const out = [];
|
|
4412
|
+
const re = /CREATE\s+TABLE\s+(?:IF\s+NOT\s+EXISTS\s+)?[^(]*\(/gi;
|
|
4413
|
+
let m;
|
|
4414
|
+
while ((m = re.exec(sql)) !== null) {
|
|
4415
|
+
let depth = 1;
|
|
4416
|
+
let item = "";
|
|
4417
|
+
let i = m.index + m[0].length;
|
|
4418
|
+
for (;i < sql.length && depth > 0; i++) {
|
|
4419
|
+
const ch = sql[i];
|
|
4420
|
+
if (ch === "(")
|
|
4421
|
+
depth++;
|
|
4422
|
+
else if (ch === ")") {
|
|
4423
|
+
depth--;
|
|
4424
|
+
if (depth === 0)
|
|
4425
|
+
break;
|
|
4426
|
+
}
|
|
4427
|
+
if (depth === 1 && ch === ",") {
|
|
4428
|
+
out.push(item);
|
|
4429
|
+
item = "";
|
|
4430
|
+
continue;
|
|
4431
|
+
}
|
|
4432
|
+
item += ch;
|
|
4433
|
+
}
|
|
4434
|
+
out.push(item);
|
|
4435
|
+
re.lastIndex = i;
|
|
4436
|
+
}
|
|
4437
|
+
return out;
|
|
4438
|
+
}
|
|
4439
|
+
function columnLevelUniqueColumns(sql) {
|
|
4440
|
+
const out = [];
|
|
4441
|
+
for (const def of createTableColumnDefs(sql)) {
|
|
4442
|
+
const t = def.trim();
|
|
4443
|
+
const bare = t.replace(/'(?:[^']|'')*'/g, "''");
|
|
4444
|
+
if (!/\bUNIQUE\b(?!\s*\()/i.test(bare))
|
|
4445
|
+
continue;
|
|
4446
|
+
const name = /^([a-z_][a-z0-9_]*)\s+\S/i.exec(t)?.[1];
|
|
4447
|
+
if (!name)
|
|
4448
|
+
continue;
|
|
4449
|
+
if (/^(?:primary|unique|constraint|foreign|check|exclude|like)$/i.test(name))
|
|
4450
|
+
continue;
|
|
4451
|
+
out.push(name);
|
|
4452
|
+
}
|
|
4453
|
+
return out;
|
|
4454
|
+
}
|
|
4455
|
+
function keyColumnSets(upSql) {
|
|
4456
|
+
const s = upSql.replace(/--[^\n]*/g, "").replace(/\/\*[\s\S]*?\*\//g, "");
|
|
4457
|
+
const sets = [];
|
|
4458
|
+
const add = (cols) => sets.push(cols.split(",").map((c7) => c7.trim().replace(/["`]/g, "").toLowerCase()).filter(Boolean));
|
|
4459
|
+
for (const m of s.matchAll(/\bUNIQUE\s*\(([^)]+)\)/gi))
|
|
4460
|
+
add(m[1]);
|
|
4461
|
+
for (const m of s.matchAll(/\bPRIMARY\s+KEY\s*\(([^)]+)\)/gi))
|
|
4462
|
+
add(m[1]);
|
|
4463
|
+
for (const m of s.matchAll(/CREATE\s+UNIQUE\s+INDEX[^(]*\(([^)]+)\)/gi))
|
|
4464
|
+
add(m[1]);
|
|
4465
|
+
for (const col of columnLevelUniqueColumns(s))
|
|
4466
|
+
add(col);
|
|
4467
|
+
for (const m of s.matchAll(/\bADD\s+COLUMN\s+(?:IF\s+NOT\s+EXISTS\s+)?([a-z_][a-z0-9_]*)[^,;\n]*?\bUNIQUE\b(?!\s*\()/gi))
|
|
4468
|
+
add(m[1]);
|
|
4469
|
+
return sets;
|
|
4470
|
+
}
|
|
4471
|
+
function narrowKey(parts) {
|
|
4472
|
+
if (parts.length === 0)
|
|
4473
|
+
return null;
|
|
4474
|
+
if (parts.includes("tenant_id"))
|
|
4475
|
+
return null;
|
|
4476
|
+
if (parts.length === 1 && parts[0] === "id")
|
|
4477
|
+
return null;
|
|
4478
|
+
if (parts.some((p) => /token|hash|secret/.test(p)))
|
|
4479
|
+
return null;
|
|
4480
|
+
if (parts.some((p) => /_id$/.test(p)))
|
|
4481
|
+
return null;
|
|
4482
|
+
return [...parts].sort().join(", ");
|
|
4483
|
+
}
|
|
4484
|
+
function widenedKey(parts) {
|
|
4485
|
+
if (!parts.includes("tenant_id"))
|
|
4486
|
+
return null;
|
|
4487
|
+
const rest = parts.filter((p) => p !== "tenant_id").sort();
|
|
4488
|
+
return rest.length > 0 ? rest.join(", ") : null;
|
|
4489
|
+
}
|
|
3956
4490
|
function validateMigrations(input) {
|
|
3957
4491
|
const out = [];
|
|
4492
|
+
const narrowKeys = [];
|
|
4493
|
+
const widened = new Set;
|
|
4494
|
+
const uuidRefs = [];
|
|
4495
|
+
const fixedRefs = new Set;
|
|
3958
4496
|
for (const file of input.files) {
|
|
3959
4497
|
if (file.sql.trim() === "") {
|
|
3960
4498
|
out.push(err("MIGRATION_EMPTY", "migration file is empty", file.filename));
|
|
@@ -3974,6 +4512,25 @@ function validateMigrations(input) {
|
|
|
3974
4512
|
if (input.requireDownForDestructive && hasDestructiveDdl(parsed.up) && !parsed.down) {
|
|
3975
4513
|
out.push(err("MIGRATION_DESTRUCTIVE_NO_DOWN", 'migration contains destructive DDL (DROP TABLE/COLUMN or ALTER ... DROP) but has no "-- DOWN" section. Add one so uninstall --purgeData can roll back.', file.filename));
|
|
3976
4514
|
}
|
|
4515
|
+
narrowKeys.push(...keyColumnSets(parsed.up).map(narrowKey).filter((k) => k !== null).map((cols) => ({ cols, filename: file.filename })));
|
|
4516
|
+
for (const parts of keyColumnSets(parsed.up)) {
|
|
4517
|
+
const wide = widenedKey(parts);
|
|
4518
|
+
if (wide)
|
|
4519
|
+
widened.add(wide);
|
|
4520
|
+
}
|
|
4521
|
+
uuidRefs.push(...userRefUuidColumns(parsed.up).map((column) => ({ column, filename: file.filename })));
|
|
4522
|
+
for (const column of userRefFixedColumns(parsed.up))
|
|
4523
|
+
fixedRefs.add(column);
|
|
4524
|
+
}
|
|
4525
|
+
for (const { column, filename } of uuidRefs) {
|
|
4526
|
+
if (fixedRefs.has(column))
|
|
4527
|
+
continue;
|
|
4528
|
+
out.push(err("SCHEMA_USER_REF_UUID", `"${column}" is declared UUID and its name says it records a user. ` + '`"user".id` is a 32-character nanoid, so Postgres rejects every write with ' + '22P02 "invalid input syntax for type uuid" \u2014 the route then fails for ' + "everybody, always, with an error naming the type rather than the cause. " + "Declare it TEXT.", filename));
|
|
4529
|
+
}
|
|
4530
|
+
for (const { cols, filename } of narrowKeys) {
|
|
4531
|
+
if (widened.has(cols))
|
|
4532
|
+
continue;
|
|
4533
|
+
out.push(err("SCHEMA_UNIQUE_WITHOUT_TENANT", `UNIQUE (${cols}) leaves out tenant_id. Several companies share one ` + "instance, so the second one to use a value is refused it \u2014 its own invoice " + "number, its own product code, its own fiscal year \u2014 and row-level security " + "hides the conflicting row, so it is shown a database error about something " + `it cannot see and cannot resolve. Use UNIQUE (tenant_id, ${cols}) unless ` + "the value must be unique across the whole instance, as a token or a hash is.", filename));
|
|
3977
4534
|
}
|
|
3978
4535
|
return out;
|
|
3979
4536
|
}
|
|
@@ -3997,6 +4554,215 @@ function validateBundleSize(input) {
|
|
|
3997
4554
|
}
|
|
3998
4555
|
return [];
|
|
3999
4556
|
}
|
|
4557
|
+
var SDUI_SHARED_READ_ALLOWLIST = new Set([
|
|
4558
|
+
"/api/collections",
|
|
4559
|
+
"/api/views"
|
|
4560
|
+
]);
|
|
4561
|
+
function normalizeRoute(p) {
|
|
4562
|
+
return p.split("?")[0].replace(/\{[^}]+\}/g, "*").replace(/:[^/]+/g, "*").replace(/\/+$/, "") || "/";
|
|
4563
|
+
}
|
|
4564
|
+
var SDUI_I18N_SLOTS = [
|
|
4565
|
+
"title",
|
|
4566
|
+
"subtitle",
|
|
4567
|
+
"label",
|
|
4568
|
+
"placeholder",
|
|
4569
|
+
"note",
|
|
4570
|
+
"confirm",
|
|
4571
|
+
"description",
|
|
4572
|
+
"emptyText",
|
|
4573
|
+
"emptyTitle",
|
|
4574
|
+
"hint"
|
|
4575
|
+
];
|
|
4576
|
+
var I18N_UNIVERSAL = new Set([
|
|
4577
|
+
"API",
|
|
4578
|
+
"JSON",
|
|
4579
|
+
"NDJSON",
|
|
4580
|
+
"CSV",
|
|
4581
|
+
"SQL",
|
|
4582
|
+
"URL",
|
|
4583
|
+
"UUID",
|
|
4584
|
+
"ID",
|
|
4585
|
+
"HTTP",
|
|
4586
|
+
"HTTPS",
|
|
4587
|
+
"GET",
|
|
4588
|
+
"POST",
|
|
4589
|
+
"PUT",
|
|
4590
|
+
"PATCH",
|
|
4591
|
+
"DELETE",
|
|
4592
|
+
"Zveltio",
|
|
4593
|
+
"OK",
|
|
4594
|
+
"UTC",
|
|
4595
|
+
"AI",
|
|
4596
|
+
"SDK",
|
|
4597
|
+
"CLI",
|
|
4598
|
+
"RLS",
|
|
4599
|
+
"DB",
|
|
4600
|
+
"PDF",
|
|
4601
|
+
"XML",
|
|
4602
|
+
"YAML"
|
|
4603
|
+
]);
|
|
4604
|
+
var I18N_PRODUCT_NAMES = new Set([
|
|
4605
|
+
"HubSpot",
|
|
4606
|
+
"Notion",
|
|
4607
|
+
"Airtable",
|
|
4608
|
+
"Okta",
|
|
4609
|
+
"Entra ID",
|
|
4610
|
+
"Slack",
|
|
4611
|
+
"Stripe",
|
|
4612
|
+
"Salesforce",
|
|
4613
|
+
"ANAF"
|
|
4614
|
+
]);
|
|
4615
|
+
function isProductNameList(s) {
|
|
4616
|
+
const parts = s.split(/[/,\u00B7|]/).map((p) => p.trim()).filter(Boolean);
|
|
4617
|
+
return parts.length > 0 && parts.every((p) => I18N_PRODUCT_NAMES.has(p));
|
|
4618
|
+
}
|
|
4619
|
+
function looksLikeMessageKey(s) {
|
|
4620
|
+
return /^[a-z][a-z0-9]*(\.[a-z0-9_]+)+$/i.test(s);
|
|
4621
|
+
}
|
|
4622
|
+
function looksTranslatable(s) {
|
|
4623
|
+
const t = s.trim();
|
|
4624
|
+
if (t.length < 3)
|
|
4625
|
+
return false;
|
|
4626
|
+
if (I18N_UNIVERSAL.has(t))
|
|
4627
|
+
return false;
|
|
4628
|
+
if (isProductNameList(t))
|
|
4629
|
+
return false;
|
|
4630
|
+
if (!/[a-z]{2}/.test(t))
|
|
4631
|
+
return false;
|
|
4632
|
+
if (/^[a-z0-9_.\-/]+$/.test(t))
|
|
4633
|
+
return false;
|
|
4634
|
+
if (/^[a-z0-9_]+=/.test(t))
|
|
4635
|
+
return false;
|
|
4636
|
+
if (t.startsWith("http"))
|
|
4637
|
+
return false;
|
|
4638
|
+
return /^[A-Z]/.test(t) || t.split(/\s+/).length > 1;
|
|
4639
|
+
}
|
|
4640
|
+
function collectI18nStrings(node, acc) {
|
|
4641
|
+
if (Array.isArray(node)) {
|
|
4642
|
+
for (const x of node)
|
|
4643
|
+
collectI18nStrings(x, acc);
|
|
4644
|
+
return;
|
|
4645
|
+
}
|
|
4646
|
+
if (node && typeof node === "object") {
|
|
4647
|
+
for (const [k, v] of Object.entries(node)) {
|
|
4648
|
+
if (SDUI_I18N_SLOTS.includes(k) && typeof v === "string" && v)
|
|
4649
|
+
acc.push({ slot: k, value: v });
|
|
4650
|
+
else
|
|
4651
|
+
collectI18nStrings(v, acc);
|
|
4652
|
+
}
|
|
4653
|
+
}
|
|
4654
|
+
}
|
|
4655
|
+
function collectEndpoints(node, acc) {
|
|
4656
|
+
if (Array.isArray(node)) {
|
|
4657
|
+
for (const x of node)
|
|
4658
|
+
collectEndpoints(x, acc);
|
|
4659
|
+
return;
|
|
4660
|
+
}
|
|
4661
|
+
if (node && typeof node === "object") {
|
|
4662
|
+
for (const [k, v] of Object.entries(node)) {
|
|
4663
|
+
if ((k === "dataSource" || k === "endpoint" || k === "saveEndpoint" || k === "catalogDataSource" || k === "loadEndpoint") && typeof v === "string")
|
|
4664
|
+
acc.push(v);
|
|
4665
|
+
else
|
|
4666
|
+
collectEndpoints(v, acc);
|
|
4667
|
+
}
|
|
4668
|
+
}
|
|
4669
|
+
}
|
|
4670
|
+
function validateSduiSchema(input) {
|
|
4671
|
+
const out = [];
|
|
4672
|
+
const { schema, extName, file } = input;
|
|
4673
|
+
if (schema === null || typeof schema !== "object" || Array.isArray(schema)) {
|
|
4674
|
+
out.push(err("SDUI_NOT_OBJECT", "schema must be a JSON object", file));
|
|
4675
|
+
return out;
|
|
4676
|
+
}
|
|
4677
|
+
const s = schema;
|
|
4678
|
+
if (typeof s.title !== "string" || !s.title)
|
|
4679
|
+
out.push(err("SDUI_NO_TITLE", 'schema missing "title"', file));
|
|
4680
|
+
if (s.kind === "settings") {
|
|
4681
|
+
if (typeof s.dataSource !== "string" || typeof s.saveEndpoint !== "string")
|
|
4682
|
+
out.push(err("SDUI_SETTINGS_SHAPE", "settings schema needs dataSource + saveEndpoint", file));
|
|
4683
|
+
} else if (!Array.isArray(s.resources) || s.resources.length === 0) {
|
|
4684
|
+
out.push(err("SDUI_NO_RESOURCES", 'page schema needs a non-empty "resources" array', file));
|
|
4685
|
+
}
|
|
4686
|
+
const mount = `/ext/${extName}`;
|
|
4687
|
+
const depMounts = (input.dependencies ?? []).map((d) => `/ext/${d}`);
|
|
4688
|
+
const provided = input.providedRoutes ? new Set(input.providedRoutes.map(normalizeRoute)) : undefined;
|
|
4689
|
+
const inMount = (bare, m) => bare === m || bare.startsWith(`${m}/`);
|
|
4690
|
+
const endpoints = [];
|
|
4691
|
+
collectEndpoints(schema, endpoints);
|
|
4692
|
+
for (const raw of endpoints) {
|
|
4693
|
+
const bare = raw.split("?")[0];
|
|
4694
|
+
if (SDUI_SHARED_READ_ALLOWLIST.has(bare))
|
|
4695
|
+
continue;
|
|
4696
|
+
if (depMounts.some((m) => inMount(bare, m)))
|
|
4697
|
+
continue;
|
|
4698
|
+
if (!inMount(bare, mount)) {
|
|
4699
|
+
out.push(err("SDUI_ENDPOINT_FOREIGN", `endpoint "${raw}" is outside this extension's namespace "${mount}/" (or a declared dependency, or the shared-read allowlist). Schemas may only call their own routes.`, file));
|
|
4700
|
+
continue;
|
|
4701
|
+
}
|
|
4702
|
+
if (provided) {
|
|
4703
|
+
const rel = normalizeRoute(bare.slice(mount.length) || "/");
|
|
4704
|
+
if (!isProvidedRoute(provided, rel)) {
|
|
4705
|
+
out.push(err("SDUI_ENDPOINT_UNKNOWN", `endpoint "${raw}" resolves to "${rel}", which the engine does not serve. Provided routes: ${[...provided].sort().join(", ") || "(none found)"}.`, file));
|
|
4706
|
+
}
|
|
4707
|
+
}
|
|
4708
|
+
}
|
|
4709
|
+
if (input.messageKeys) {
|
|
4710
|
+
const known = new Set(input.messageKeys);
|
|
4711
|
+
const strings = [];
|
|
4712
|
+
collectI18nStrings(schema, strings);
|
|
4713
|
+
for (const { slot, value } of strings) {
|
|
4714
|
+
if (known.has(value))
|
|
4715
|
+
continue;
|
|
4716
|
+
if (looksLikeMessageKey(value)) {
|
|
4717
|
+
out.push(err("SDUI_I18N_KEY_MISSING", `${slot} "${value}" is not a known message key. The host renders an unknown key as literal text, so this reaches the user as-is. Add it to studio/messages/{locale}.json, or use a shared common.* key.`, file));
|
|
4718
|
+
} else if (looksTranslatable(value)) {
|
|
4719
|
+
out.push(warn("SDUI_I18N_HARDCODED", `${slot} "${value}" is hardcoded text, not a message key \u2014 it stays English in all nine locales. Move it to studio/messages/{locale}.json.`, file));
|
|
4720
|
+
}
|
|
4721
|
+
}
|
|
4722
|
+
}
|
|
4723
|
+
return out;
|
|
4724
|
+
}
|
|
4725
|
+
function joinRoutePath(prefix, child) {
|
|
4726
|
+
if (!child || child === "/")
|
|
4727
|
+
return prefix || "/";
|
|
4728
|
+
const p = prefix.endsWith("/") ? prefix.slice(0, -1) : prefix;
|
|
4729
|
+
return child.startsWith("/") ? `${p}${child}` : `${p}/${child}`;
|
|
4730
|
+
}
|
|
4731
|
+
function isProvidedRoute(provided, rel) {
|
|
4732
|
+
if (provided.has(rel))
|
|
4733
|
+
return true;
|
|
4734
|
+
for (const p of provided) {
|
|
4735
|
+
if (!p.endsWith("/*"))
|
|
4736
|
+
continue;
|
|
4737
|
+
const base = p.slice(0, -2);
|
|
4738
|
+
if (!base || base === "/")
|
|
4739
|
+
continue;
|
|
4740
|
+
if (rel === base || rel.startsWith(`${base}/`))
|
|
4741
|
+
return true;
|
|
4742
|
+
}
|
|
4743
|
+
return false;
|
|
4744
|
+
}
|
|
4745
|
+
function extractEngineRoutes(engineSourceTexts) {
|
|
4746
|
+
const routes = new Set;
|
|
4747
|
+
const methodRe = /\.(?:get|post|put|patch|delete)\(\s*['"`]([^'"`]+)['"`]/g;
|
|
4748
|
+
const mountRe = /\.route\(\s*['"`]([^'"`]+)['"`]/g;
|
|
4749
|
+
for (const text of engineSourceTexts) {
|
|
4750
|
+
let m;
|
|
4751
|
+
while ((m = methodRe.exec(text)) !== null) {
|
|
4752
|
+
if (m[1].startsWith("/"))
|
|
4753
|
+
routes.add(m[1]);
|
|
4754
|
+
}
|
|
4755
|
+
while ((m = mountRe.exec(text)) !== null) {
|
|
4756
|
+
const prefix = m[1];
|
|
4757
|
+
if (!prefix.startsWith("/"))
|
|
4758
|
+
continue;
|
|
4759
|
+
routes.add(prefix);
|
|
4760
|
+
if (prefix !== "/")
|
|
4761
|
+
routes.add(joinRoutePath(prefix, "/*"));
|
|
4762
|
+
}
|
|
4763
|
+
}
|
|
4764
|
+
return [...routes];
|
|
4765
|
+
}
|
|
4000
4766
|
function validateExtension(input) {
|
|
4001
4767
|
const errors = [
|
|
4002
4768
|
...validateManifest(input.manifest),
|
|
@@ -4006,7 +4772,7 @@ function validateExtension(input) {
|
|
|
4006
4772
|
...input.bundleSize ? validateBundleSize(input.bundleSize) : []
|
|
4007
4773
|
];
|
|
4008
4774
|
return {
|
|
4009
|
-
ok: errors.
|
|
4775
|
+
ok: errors.every((e) => e.severity === "warning"),
|
|
4010
4776
|
errors,
|
|
4011
4777
|
stats: {
|
|
4012
4778
|
tables: input.stats.tables,
|
|
@@ -4017,6 +4783,7 @@ function validateExtension(input) {
|
|
|
4017
4783
|
}
|
|
4018
4784
|
|
|
4019
4785
|
// src/commands/extension-validate.ts
|
|
4786
|
+
init_extension_pack();
|
|
4020
4787
|
var c7 = {
|
|
4021
4788
|
bold: (s) => `\x1B[1m${s}\x1B[0m`,
|
|
4022
4789
|
green: (s) => `\x1B[32m${s}\x1B[0m`,
|
|
@@ -4034,14 +4801,16 @@ var PEER_DEPS_ALLOWLIST = new Set([
|
|
|
4034
4801
|
"@aws-sdk/s3-request-presigner",
|
|
4035
4802
|
"nanoid",
|
|
4036
4803
|
"qrcode",
|
|
4804
|
+
"pdf-lib",
|
|
4805
|
+
"@pdf-lib/fontkit",
|
|
4037
4806
|
"pdfkit",
|
|
4038
4807
|
"graphql"
|
|
4039
4808
|
]);
|
|
4040
4809
|
function recursiveSize(dir) {
|
|
4041
4810
|
let total = 0;
|
|
4042
|
-
if (!
|
|
4811
|
+
if (!existsSync11(dir))
|
|
4043
4812
|
return 0;
|
|
4044
|
-
for (const name of
|
|
4813
|
+
for (const name of readdirSync3(dir)) {
|
|
4045
4814
|
const full = join10(dir, name);
|
|
4046
4815
|
if (name === "node_modules" || name === ".zveltio" || name === "dist")
|
|
4047
4816
|
continue;
|
|
@@ -4058,14 +4827,119 @@ function recursiveSize(dir) {
|
|
|
4058
4827
|
}
|
|
4059
4828
|
return total;
|
|
4060
4829
|
}
|
|
4830
|
+
function readEngineSources(dir) {
|
|
4831
|
+
const bundled = join10(dir, "engine", "index.js");
|
|
4832
|
+
if (existsSync11(bundled)) {
|
|
4833
|
+
try {
|
|
4834
|
+
return [readFileSync4(bundled, "utf8")];
|
|
4835
|
+
} catch {}
|
|
4836
|
+
}
|
|
4837
|
+
const out = [];
|
|
4838
|
+
const walk = (d) => {
|
|
4839
|
+
if (!existsSync11(d))
|
|
4840
|
+
return;
|
|
4841
|
+
for (const name of readdirSync3(d)) {
|
|
4842
|
+
if (name === "migrations" || name === "node_modules")
|
|
4843
|
+
continue;
|
|
4844
|
+
const full = join10(d, name);
|
|
4845
|
+
let st;
|
|
4846
|
+
try {
|
|
4847
|
+
st = statSync(full);
|
|
4848
|
+
} catch {
|
|
4849
|
+
continue;
|
|
4850
|
+
}
|
|
4851
|
+
if (st.isDirectory())
|
|
4852
|
+
walk(full);
|
|
4853
|
+
else if (/\.(ts|js)$/.test(name)) {
|
|
4854
|
+
try {
|
|
4855
|
+
out.push(readFileSync4(full, "utf8"));
|
|
4856
|
+
} catch {}
|
|
4857
|
+
}
|
|
4858
|
+
}
|
|
4859
|
+
};
|
|
4860
|
+
walk(join10(dir, "engine"));
|
|
4861
|
+
return out;
|
|
4862
|
+
}
|
|
4863
|
+
function readOwnMessageKeys(dir) {
|
|
4864
|
+
const path = join10(dir, "studio", "messages", "en.json");
|
|
4865
|
+
if (!existsSync11(path))
|
|
4866
|
+
return;
|
|
4867
|
+
try {
|
|
4868
|
+
const parsed = JSON.parse(readFileSync4(path, "utf8"));
|
|
4869
|
+
return new Set(Object.keys(parsed));
|
|
4870
|
+
} catch {
|
|
4871
|
+
return;
|
|
4872
|
+
}
|
|
4873
|
+
}
|
|
4874
|
+
function readDependencyMessageKeys(dir, extName, deps) {
|
|
4875
|
+
const out = new Set;
|
|
4876
|
+
const depth = extName.split("/").length;
|
|
4877
|
+
const root = join10(dir, ...Array(depth).fill(".."));
|
|
4878
|
+
for (const dep of deps) {
|
|
4879
|
+
const path = join10(root, dep, "studio", "messages", "en.json");
|
|
4880
|
+
if (!existsSync11(path))
|
|
4881
|
+
continue;
|
|
4882
|
+
try {
|
|
4883
|
+
for (const k of Object.keys(JSON.parse(readFileSync4(path, "utf8"))))
|
|
4884
|
+
out.add(k);
|
|
4885
|
+
} catch {}
|
|
4886
|
+
}
|
|
4887
|
+
return out;
|
|
4888
|
+
}
|
|
4061
4889
|
function inferExpectedName(dir) {
|
|
4062
4890
|
const norm = dir.replace(/\\/g, "/");
|
|
4063
|
-
const
|
|
4891
|
+
const marker = "zveltio-extensions/";
|
|
4892
|
+
const idx = norm.lastIndexOf(marker);
|
|
4064
4893
|
if (idx >= 0) {
|
|
4065
|
-
return norm.slice(idx +
|
|
4894
|
+
return norm.slice(idx + marker.length).replace(/\/$/, "");
|
|
4066
4895
|
}
|
|
4067
4896
|
return norm.split("/").filter(Boolean).pop() ?? "";
|
|
4068
4897
|
}
|
|
4898
|
+
function checkBundleIntegrity(dir, manifest) {
|
|
4899
|
+
const bundle = join10(dir, "engine", "index.js");
|
|
4900
|
+
const integrity = manifest?.integrity;
|
|
4901
|
+
if (!integrity)
|
|
4902
|
+
return [];
|
|
4903
|
+
if (!existsSync11(bundle)) {
|
|
4904
|
+
return integrity.engineSha256 && existsSync11(join10(dir, "engine", "index.ts")) ? [
|
|
4905
|
+
{
|
|
4906
|
+
code: "BUNDLE_MISSING",
|
|
4907
|
+
message: "manifest records integrity.engineSha256 but engine/index.js does not exist. " + "Run `zveltio extension pack`.",
|
|
4908
|
+
file: "engine/index.js"
|
|
4909
|
+
}
|
|
4910
|
+
] : [];
|
|
4911
|
+
}
|
|
4912
|
+
const out = [];
|
|
4913
|
+
if (integrity.engineSha256) {
|
|
4914
|
+
const actual = createHash2("sha256").update(readFileSync4(bundle)).digest("hex");
|
|
4915
|
+
if (actual !== integrity.engineSha256) {
|
|
4916
|
+
out.push({
|
|
4917
|
+
code: "BUNDLE_HASH_MISMATCH",
|
|
4918
|
+
message: `engine/index.js (sha256 ${actual.slice(0, 12)}\u2026) does not match ` + `integrity.engineSha256 (${integrity.engineSha256.slice(0, 12)}\u2026). ` + "Re-pack \u2014 the engine refuses a bundle whose hash disagrees with its manifest.",
|
|
4919
|
+
file: "engine/index.js"
|
|
4920
|
+
});
|
|
4921
|
+
}
|
|
4922
|
+
}
|
|
4923
|
+
if (!existsSync11(join10(dir, "engine", "index.ts")))
|
|
4924
|
+
return out;
|
|
4925
|
+
if (!integrity.sourceSha256) {
|
|
4926
|
+
out.push({
|
|
4927
|
+
code: "BUNDLE_SOURCE_UNRECORDED",
|
|
4928
|
+
severity: "warning",
|
|
4929
|
+
message: "manifest has no integrity.sourceSha256, so nothing can tell whether the bundle " + "was built from the committed source. Re-pack to record it.",
|
|
4930
|
+
file: "manifest.json"
|
|
4931
|
+
});
|
|
4932
|
+
return out;
|
|
4933
|
+
}
|
|
4934
|
+
if (hashEngineSources(dir) !== integrity.sourceSha256) {
|
|
4935
|
+
out.push({
|
|
4936
|
+
code: "BUNDLE_OLDER_THAN_SOURCE",
|
|
4937
|
+
message: "engine/index.js was NOT built from the current engine/ sources, so the changes in " + "them do not run anywhere. Run `zveltio extension pack`.",
|
|
4938
|
+
file: "engine/index.js"
|
|
4939
|
+
});
|
|
4940
|
+
}
|
|
4941
|
+
return out;
|
|
4942
|
+
}
|
|
4069
4943
|
async function extensionValidateCommand(opts = {}) {
|
|
4070
4944
|
const dir = opts.dir ?? process.cwd();
|
|
4071
4945
|
console.log(`
|
|
@@ -4074,7 +4948,7 @@ ${c7.bold("Extension validate")}
|
|
|
4074
4948
|
console.log(` Extension dir: ${c7.dim(dir)}`);
|
|
4075
4949
|
const manifestPath = join10(dir, "manifest.json");
|
|
4076
4950
|
let manifest = null;
|
|
4077
|
-
if (
|
|
4951
|
+
if (existsSync11(manifestPath)) {
|
|
4078
4952
|
try {
|
|
4079
4953
|
manifest = JSON.parse(readFileSync4(manifestPath, "utf8"));
|
|
4080
4954
|
} catch (e) {
|
|
@@ -4086,8 +4960,8 @@ ${c7.bold("Extension validate")}
|
|
|
4086
4960
|
const migrationsDir = join10(dir, "engine", "migrations");
|
|
4087
4961
|
let sqlFiles = [];
|
|
4088
4962
|
let parsedTableCount = 0;
|
|
4089
|
-
if (
|
|
4090
|
-
sqlFiles =
|
|
4963
|
+
if (existsSync11(migrationsDir)) {
|
|
4964
|
+
sqlFiles = readdirSync3(migrationsDir).filter((f) => f.endsWith(".sql")).sort().map((filename) => ({
|
|
4091
4965
|
filename,
|
|
4092
4966
|
sql: readFileSync4(join10(migrationsDir, filename), "utf8")
|
|
4093
4967
|
}));
|
|
@@ -4099,7 +4973,7 @@ ${c7.bold("Extension validate")}
|
|
|
4099
4973
|
}
|
|
4100
4974
|
const paths = {};
|
|
4101
4975
|
for (const rel of ["manifest.json", "engine/index.ts", "engine/index.js"]) {
|
|
4102
|
-
paths[rel] =
|
|
4976
|
+
paths[rel] = existsSync11(join10(dir, rel));
|
|
4103
4977
|
}
|
|
4104
4978
|
const required = ["manifest.json"];
|
|
4105
4979
|
if (!isStudioOnly && !(paths["engine/index.ts"] || paths["engine/index.js"])) {
|
|
@@ -4126,6 +5000,60 @@ ${c7.bold("Extension validate")}
|
|
|
4126
5000
|
},
|
|
4127
5001
|
stats: { tables: parsedTableCount, migrations: sqlFiles.length }
|
|
4128
5002
|
});
|
|
5003
|
+
const sduiErrors = [];
|
|
5004
|
+
{
|
|
5005
|
+
const pages = manifest && typeof manifest === "object" ? manifest?.studio?.pages ?? [] : [];
|
|
5006
|
+
const schemaPages = (Array.isArray(pages) ? pages : []).filter((p) => typeof p?.schema === "string");
|
|
5007
|
+
if (schemaPages.length > 0) {
|
|
5008
|
+
const manifestRecord = manifest && typeof manifest === "object" ? manifest : undefined;
|
|
5009
|
+
const extName = typeof manifestRecord?.name === "string" ? manifestRecord.name : inferExpectedName(dir);
|
|
5010
|
+
const provided = extractEngineRoutes(readEngineSources(dir));
|
|
5011
|
+
const ownMessageKeys = readOwnMessageKeys(dir);
|
|
5012
|
+
const dependencies = Array.isArray(manifest?.dependencies) ? manifest.dependencies.map((d) => typeof d === "string" ? d : d?.name).filter((x) => typeof x === "string") : [];
|
|
5013
|
+
for (const p of schemaPages) {
|
|
5014
|
+
const rel = join10("studio", p.schema).replace(/\\/g, "/");
|
|
5015
|
+
const abs = join10(dir, "studio", p.schema);
|
|
5016
|
+
let parsed = null;
|
|
5017
|
+
if (!existsSync11(abs)) {
|
|
5018
|
+
sduiErrors.push({
|
|
5019
|
+
code: "SDUI_SCHEMA_MISSING",
|
|
5020
|
+
message: "schema file not found",
|
|
5021
|
+
file: rel
|
|
5022
|
+
});
|
|
5023
|
+
continue;
|
|
5024
|
+
}
|
|
5025
|
+
try {
|
|
5026
|
+
parsed = JSON.parse(readFileSync4(abs, "utf8"));
|
|
5027
|
+
} catch (e) {
|
|
5028
|
+
sduiErrors.push({
|
|
5029
|
+
code: "SDUI_SCHEMA_BAD_JSON",
|
|
5030
|
+
message: `not valid JSON: ${e.message}`,
|
|
5031
|
+
file: rel
|
|
5032
|
+
});
|
|
5033
|
+
continue;
|
|
5034
|
+
}
|
|
5035
|
+
sduiErrors.push(...validateSduiSchema({
|
|
5036
|
+
schema: parsed,
|
|
5037
|
+
extName,
|
|
5038
|
+
providedRoutes: provided,
|
|
5039
|
+
dependencies,
|
|
5040
|
+
messageKeys: ownMessageKeys && new Set([
|
|
5041
|
+
...ownMessageKeys,
|
|
5042
|
+
...SHARED_MESSAGE_KEYS,
|
|
5043
|
+
...readDependencyMessageKeys(dir, extName, dependencies)
|
|
5044
|
+
]),
|
|
5045
|
+
file: rel
|
|
5046
|
+
}));
|
|
5047
|
+
}
|
|
5048
|
+
result.errors.push(...sduiErrors);
|
|
5049
|
+
const sduiHard = sduiErrors.filter((e) => e.severity !== "warning");
|
|
5050
|
+
if (sduiHard.length > 0)
|
|
5051
|
+
result.ok = false;
|
|
5052
|
+
const sduiWarn = sduiErrors.length - sduiHard.length;
|
|
5053
|
+
const sduiSummary = sduiHard.length > 0 ? c7.red(`${sduiHard.length} error(s)`) : sduiWarn > 0 ? c7.yellow(`${schemaPages.length} OK, ${sduiWarn} warning(s)`) : c7.green(`${schemaPages.length} OK`);
|
|
5054
|
+
console.log(` SDUI schemas: ${sduiSummary}`);
|
|
5055
|
+
}
|
|
5056
|
+
}
|
|
4129
5057
|
const m = manifest;
|
|
4130
5058
|
const studioOnly = isStudioOnly;
|
|
4131
5059
|
const v2Status = (() => {
|
|
@@ -4170,8 +5098,8 @@ ${c7.bold("Extension validate")}
|
|
|
4170
5098
|
}
|
|
4171
5099
|
console.log("");
|
|
4172
5100
|
if (isolation !== "worker" && !inlineOk) {
|
|
4173
|
-
console.error(c7.red(`Validation failed: ${resolved.tier} publishers must declare ` + `engine.isolation: "worker" (
|
|
4174
|
-
console.error(c7.dim(' Add `"isolation": "worker"` to the engine block in manifest.json, ' + "then re-pack. If you are a verified/first-party publisher, pass " + "--first-party or set ZVELTIO_REGISTRY_TOKEN so the tier can be confirmed. " + "See docs/
|
|
5101
|
+
console.error(c7.red(`Validation failed: ${resolved.tier} publishers must declare ` + `engine.isolation: "worker" (marketplace-policy.md \xA72).`));
|
|
5102
|
+
console.error(c7.dim(' Add `"isolation": "worker"` to the engine block in manifest.json, ' + "then re-pack. If you are a verified/first-party publisher, pass " + "--first-party or set ZVELTIO_REGISTRY_TOKEN so the tier can be confirmed. " + "See docs/extensions/marketplace-policy.md \xA72."));
|
|
4175
5103
|
if (opts.silentExit)
|
|
4176
5104
|
throw new Error("Validation failed: community inline isolation");
|
|
4177
5105
|
process.exit(1);
|
|
@@ -4179,7 +5107,7 @@ ${c7.bold("Extension validate")}
|
|
|
4179
5107
|
}
|
|
4180
5108
|
if (v2Status === "v1-legacy") {
|
|
4181
5109
|
console.error(c7.red("Validation failed: manifest is v1 (no engine.bundled block)."));
|
|
4182
|
-
console.error(c7.dim(" v1 manifests are unsupported since 1.0.0-beta.1. Run `zveltio extension pack` to " + "generate the v2 engine + integrity blocks. See docs/
|
|
5110
|
+
console.error(c7.dim(" v1 manifests are unsupported since 1.0.0-beta.1. Run `zveltio extension pack` to " + "generate the v2 engine + integrity blocks. See docs/extensions/developer-guide.md \xA74."));
|
|
4183
5111
|
if (opts.silentExit)
|
|
4184
5112
|
throw new Error("Validation failed: v1 manifest unsupported");
|
|
4185
5113
|
process.exit(1);
|
|
@@ -4191,28 +5119,43 @@ ${c7.bold("Extension validate")}
|
|
|
4191
5119
|
throw new Error("Validation failed: incomplete v2 manifest");
|
|
4192
5120
|
process.exit(1);
|
|
4193
5121
|
}
|
|
5122
|
+
const integrityErrors = checkBundleIntegrity(dir, manifest);
|
|
5123
|
+
if (integrityErrors.length > 0) {
|
|
5124
|
+
result.errors.push(...integrityErrors);
|
|
5125
|
+
if (integrityErrors.some((e) => e.severity !== "warning"))
|
|
5126
|
+
result.ok = false;
|
|
5127
|
+
}
|
|
5128
|
+
const warnings = result.errors.filter((e) => e.severity === "warning");
|
|
5129
|
+
const hardErrors = result.errors.filter((e) => e.severity !== "warning");
|
|
5130
|
+
if (warnings.length > 0) {
|
|
5131
|
+
console.log(c7.yellow(`${warnings.length} warning(s):`));
|
|
5132
|
+
for (const e of warnings)
|
|
5133
|
+
printError(e, "warning");
|
|
5134
|
+
console.log("");
|
|
5135
|
+
}
|
|
4194
5136
|
if (result.ok) {
|
|
4195
5137
|
console.log(c7.green("Validation passed."));
|
|
4196
5138
|
console.log(c7.dim(" Run `zveltio extension types` to refresh generated types, then `zveltio extension publish` when ready."));
|
|
4197
5139
|
console.log("");
|
|
4198
5140
|
return;
|
|
4199
5141
|
}
|
|
4200
|
-
console.log(c7.red(`Validation failed with ${
|
|
5142
|
+
console.log(c7.red(`Validation failed with ${hardErrors.length} error(s):`));
|
|
4201
5143
|
console.log("");
|
|
4202
|
-
for (const e of
|
|
4203
|
-
printError(e);
|
|
5144
|
+
for (const e of hardErrors) {
|
|
5145
|
+
printError(e, "error");
|
|
4204
5146
|
}
|
|
4205
5147
|
console.log("");
|
|
4206
5148
|
if (!opts.silentExit)
|
|
4207
5149
|
process.exit(1);
|
|
4208
5150
|
}
|
|
4209
|
-
function printError(e) {
|
|
5151
|
+
function printError(e, severity = "error") {
|
|
4210
5152
|
const loc = e.file ? c7.dim(` (${e.file})`) : "";
|
|
4211
|
-
|
|
5153
|
+
const code = severity === "warning" ? c7.yellow(e.code) : c7.red(e.code);
|
|
5154
|
+
console.log(` ${code} ${e.message}${loc}`);
|
|
4212
5155
|
}
|
|
4213
5156
|
|
|
4214
5157
|
// src/commands/extension-publish.ts
|
|
4215
|
-
import { existsSync as
|
|
5158
|
+
import { existsSync as existsSync13, mkdirSync as mkdirSync4, readFileSync as readFileSync6, statSync as statSync3, writeFileSync as writeFileSync4 } from "fs";
|
|
4216
5159
|
import { join as join12, resolve as resolve2 } from "path";
|
|
4217
5160
|
import { tmpdir } from "os";
|
|
4218
5161
|
|
|
@@ -4281,16 +5224,16 @@ init_extension_pack();
|
|
|
4281
5224
|
|
|
4282
5225
|
// src/commands/keys.ts
|
|
4283
5226
|
import {
|
|
4284
|
-
existsSync as
|
|
5227
|
+
existsSync as existsSync12,
|
|
4285
5228
|
mkdirSync as mkdirSync3,
|
|
4286
|
-
readdirSync as
|
|
5229
|
+
readdirSync as readdirSync4,
|
|
4287
5230
|
readFileSync as readFileSync5,
|
|
4288
5231
|
writeFileSync as writeFileSync3,
|
|
4289
5232
|
chmodSync,
|
|
4290
5233
|
statSync as statSync2
|
|
4291
5234
|
} from "fs";
|
|
4292
5235
|
import { join as join11 } from "path";
|
|
4293
|
-
import { homedir, platform } from "os";
|
|
5236
|
+
import { homedir as homedir2, platform } from "os";
|
|
4294
5237
|
var c8 = {
|
|
4295
5238
|
bold: (s) => `\x1B[1m${s}\x1B[0m`,
|
|
4296
5239
|
green: (s) => `\x1B[32m${s}\x1B[0m`,
|
|
@@ -4299,11 +5242,11 @@ var c8 = {
|
|
|
4299
5242
|
dim: (s) => `\x1B[2m${s}\x1B[0m`
|
|
4300
5243
|
};
|
|
4301
5244
|
function keysDir() {
|
|
4302
|
-
return join11(
|
|
5245
|
+
return join11(homedir2(), ".zveltio", "keys");
|
|
4303
5246
|
}
|
|
4304
5247
|
function ensureKeysDir() {
|
|
4305
5248
|
const dir = keysDir();
|
|
4306
|
-
if (!
|
|
5249
|
+
if (!existsSync12(dir)) {
|
|
4307
5250
|
mkdirSync3(dir, { recursive: true });
|
|
4308
5251
|
if (platform() !== "win32") {
|
|
4309
5252
|
try {
|
|
@@ -4327,7 +5270,7 @@ function writeKeyFile(kp) {
|
|
|
4327
5270
|
}
|
|
4328
5271
|
function readKeyFile(keyId) {
|
|
4329
5272
|
const path = keyPath(keyId);
|
|
4330
|
-
if (!
|
|
5273
|
+
if (!existsSync12(path)) {
|
|
4331
5274
|
throw new Error(`Keypair "${keyId}" not found at ${path}. Run \`zveltio keys generate --id ${keyId}\` first.`);
|
|
4332
5275
|
}
|
|
4333
5276
|
const raw = readFileSync5(path, "utf8");
|
|
@@ -4341,10 +5284,10 @@ function resolveKeyId(explicit) {
|
|
|
4341
5284
|
if (explicit)
|
|
4342
5285
|
return explicit;
|
|
4343
5286
|
const dir = keysDir();
|
|
4344
|
-
if (!
|
|
5287
|
+
if (!existsSync12(dir)) {
|
|
4345
5288
|
throw new Error("No keypairs found. Run `zveltio keys generate` first.");
|
|
4346
5289
|
}
|
|
4347
|
-
const ids =
|
|
5290
|
+
const ids = readdirSync4(dir).filter((f) => f.endsWith(".json")).map((f) => f.slice(0, -5));
|
|
4348
5291
|
if (ids.length === 0)
|
|
4349
5292
|
throw new Error("No keypairs found. Run `zveltio keys generate` first.");
|
|
4350
5293
|
if (ids.length > 1) {
|
|
@@ -4355,7 +5298,7 @@ function resolveKeyId(explicit) {
|
|
|
4355
5298
|
async function keysGenerateCommand(opts = {}) {
|
|
4356
5299
|
ensureKeysDir();
|
|
4357
5300
|
const kp = await generateKeypair(opts.id);
|
|
4358
|
-
if (
|
|
5301
|
+
if (existsSync12(keyPath(kp.keyId)) && !opts.force) {
|
|
4359
5302
|
console.error(c8.red(`Key "${kp.keyId}" already exists. Pass --force to overwrite.`));
|
|
4360
5303
|
process.exit(1);
|
|
4361
5304
|
}
|
|
@@ -4379,11 +5322,11 @@ ${c8.bold("Public key (share with registry admin):")}
|
|
|
4379
5322
|
}
|
|
4380
5323
|
async function keysListCommand() {
|
|
4381
5324
|
const dir = keysDir();
|
|
4382
|
-
if (!
|
|
5325
|
+
if (!existsSync12(dir)) {
|
|
4383
5326
|
console.log(`${c8.yellow("No keypairs yet.")} Run ${c8.dim("zveltio keys generate")} to create one.`);
|
|
4384
5327
|
return;
|
|
4385
5328
|
}
|
|
4386
|
-
const files =
|
|
5329
|
+
const files = readdirSync4(dir).filter((f) => f.endsWith(".json")).sort();
|
|
4387
5330
|
if (files.length === 0) {
|
|
4388
5331
|
console.log(`${c8.yellow("No keypairs yet.")} Run ${c8.dim("zveltio keys generate")} to create one.`);
|
|
4389
5332
|
return;
|
|
@@ -4420,7 +5363,7 @@ var c9 = {
|
|
|
4420
5363
|
};
|
|
4421
5364
|
function readManifest2(dir) {
|
|
4422
5365
|
const path = join12(dir, "manifest.json");
|
|
4423
|
-
if (!
|
|
5366
|
+
if (!existsSync13(path)) {
|
|
4424
5367
|
throw new Error(`No manifest.json at ${path}. Run from the extension's root or pass --dir.`);
|
|
4425
5368
|
}
|
|
4426
5369
|
const raw = readFileSync6(path, "utf8");
|
|
@@ -4460,7 +5403,7 @@ async function createArchive(dir, outFile) {
|
|
|
4460
5403
|
throw new Error(`tar failed (exit ${code})`);
|
|
4461
5404
|
}
|
|
4462
5405
|
async function runStudioBuild(dir) {
|
|
4463
|
-
if (!
|
|
5406
|
+
if (!existsSync13(join12(dir, "studio")))
|
|
4464
5407
|
return;
|
|
4465
5408
|
const proc = Bun.spawn(["bun", "run", "build"], {
|
|
4466
5409
|
cwd: join12(dir, "studio"),
|
|
@@ -4472,7 +5415,7 @@ async function runStudioBuild(dir) {
|
|
|
4472
5415
|
throw new Error(`Studio build failed (exit ${code})`);
|
|
4473
5416
|
}
|
|
4474
5417
|
async function runEnginePack(dir, tierOpts) {
|
|
4475
|
-
if (!
|
|
5418
|
+
if (!existsSync13(join12(dir, "engine", "index.ts")))
|
|
4476
5419
|
return;
|
|
4477
5420
|
await extensionPackCommand({ dir, ...tierOpts });
|
|
4478
5421
|
}
|
|
@@ -4569,7 +5512,7 @@ ${c9.green("Dry-run complete")} \u2014 skipped archive/sign/publish.
|
|
|
4569
5512
|
console.log(`
|
|
4570
5513
|
${c9.bold("Step 4/6: archive")}`);
|
|
4571
5514
|
const tmpDir = opts.output ? resolve2(opts.output) : join12(tmpdir(), `zveltio-publish-${Date.now()}`);
|
|
4572
|
-
if (!
|
|
5515
|
+
if (!existsSync13(tmpDir))
|
|
4573
5516
|
mkdirSync4(tmpDir, { recursive: true });
|
|
4574
5517
|
const zvextPath = join12(tmpDir, `${artifactBaseName(manifest)}.zvext`);
|
|
4575
5518
|
try {
|
|
@@ -4671,10 +5614,10 @@ ${c9.bold("Step 6/6: upload")}`);
|
|
|
4671
5614
|
}
|
|
4672
5615
|
|
|
4673
5616
|
// src/commands/extension-dev.ts
|
|
4674
|
-
import { existsSync as
|
|
5617
|
+
import { existsSync as existsSync14, statSync as statSync4 } from "fs";
|
|
4675
5618
|
import { watch as fsWatch } from "fs";
|
|
4676
|
-
import { join as join13, resolve as resolve3, relative as
|
|
4677
|
-
import { readdirSync as
|
|
5619
|
+
import { join as join13, resolve as resolve3, relative as relative4 } from "path";
|
|
5620
|
+
import { readdirSync as readdirSync5 } from "fs";
|
|
4678
5621
|
var c10 = {
|
|
4679
5622
|
bold: (s) => `\x1B[1m${s}\x1B[0m`,
|
|
4680
5623
|
green: (s) => `\x1B[32m${s}\x1B[0m`,
|
|
@@ -4687,7 +5630,7 @@ function readManifestName(dir, override) {
|
|
|
4687
5630
|
if (override)
|
|
4688
5631
|
return override;
|
|
4689
5632
|
const manifestPath = join13(dir, "manifest.json");
|
|
4690
|
-
if (!
|
|
5633
|
+
if (!existsSync14(manifestPath)) {
|
|
4691
5634
|
throw new Error(`No manifest.json at ${manifestPath}. Pass --name or run from the extension's root.`);
|
|
4692
5635
|
}
|
|
4693
5636
|
const Bun_ = globalThis.Bun;
|
|
@@ -4703,7 +5646,7 @@ function readManifestName(dir, override) {
|
|
|
4703
5646
|
return m.name;
|
|
4704
5647
|
}
|
|
4705
5648
|
function collectEngineFiles(engineDir) {
|
|
4706
|
-
if (!
|
|
5649
|
+
if (!existsSync14(engineDir))
|
|
4707
5650
|
return [];
|
|
4708
5651
|
const out = [];
|
|
4709
5652
|
const stack = [engineDir];
|
|
@@ -4711,7 +5654,7 @@ function collectEngineFiles(engineDir) {
|
|
|
4711
5654
|
const dir = stack.pop();
|
|
4712
5655
|
let entries;
|
|
4713
5656
|
try {
|
|
4714
|
-
entries =
|
|
5657
|
+
entries = readdirSync5(dir);
|
|
4715
5658
|
} catch {
|
|
4716
5659
|
continue;
|
|
4717
5660
|
}
|
|
@@ -4778,17 +5721,20 @@ ${c10.bold("Extension dev")}
|
|
|
4778
5721
|
process.exit(1);
|
|
4779
5722
|
}
|
|
4780
5723
|
let studioProc = null;
|
|
4781
|
-
|
|
5724
|
+
const studioPkg = join13(dir, "studio", "package.json");
|
|
5725
|
+
if (!opts.noStudio && existsSync14(studioPkg)) {
|
|
4782
5726
|
studioProc = Bun.spawn(["bun", "run", "dev"], {
|
|
4783
5727
|
cwd: join13(dir, "studio"),
|
|
4784
5728
|
stdout: "inherit",
|
|
4785
5729
|
stderr: "inherit"
|
|
4786
5730
|
});
|
|
4787
|
-
console.log(` Studio: ${c10.green("watching (vite dev)")}`);
|
|
5731
|
+
console.log(` Studio: ${c10.green("watching (legacy studio/package.json vite dev)")}`);
|
|
5732
|
+
} else if (!opts.noStudio && existsSync14(join13(dir, "studio"))) {
|
|
5733
|
+
console.log(` Studio: ${c10.dim("tier-3 pages \u2014 run packages/studio dev after sync-extensions (see dev guide \xA72)")}`);
|
|
4788
5734
|
} else {
|
|
4789
5735
|
console.log(` Studio: ${c10.dim(opts.noStudio ? "skipped (--no-studio)" : "no studio/ folder \u2014 skipped")}`);
|
|
4790
5736
|
}
|
|
4791
|
-
if (!
|
|
5737
|
+
if (!existsSync14(engineDir)) {
|
|
4792
5738
|
console.log(` Engine src: ${c10.yellow("no engine/ folder \u2014 engine watch disabled")}`);
|
|
4793
5739
|
} else {
|
|
4794
5740
|
const files = collectEngineFiles(engineDir);
|
|
@@ -4797,7 +5743,7 @@ ${c10.bold("Extension dev")}
|
|
|
4797
5743
|
let lastTrigger = "";
|
|
4798
5744
|
const debounceMs = 250;
|
|
4799
5745
|
const onChange = (filePath) => {
|
|
4800
|
-
lastTrigger =
|
|
5746
|
+
lastTrigger = relative4(dir, filePath);
|
|
4801
5747
|
if (timer)
|
|
4802
5748
|
clearTimeout(timer);
|
|
4803
5749
|
timer = setTimeout(async () => {
|
|
@@ -4899,7 +5845,7 @@ ${c11.bold("Generate Types")}
|
|
|
4899
5845
|
}
|
|
4900
5846
|
|
|
4901
5847
|
// src/commands/install.ts
|
|
4902
|
-
import { existsSync as
|
|
5848
|
+
import { existsSync as existsSync15 } from "fs";
|
|
4903
5849
|
import { readFile, mkdir as mkdir2, cp } from "fs/promises";
|
|
4904
5850
|
import { join as join14 } from "path";
|
|
4905
5851
|
async function installCommand(name, opts) {
|
|
@@ -4914,7 +5860,7 @@ async function installCommand(name, opts) {
|
|
|
4914
5860
|
await installFromCatalog(name, engineUrl);
|
|
4915
5861
|
}
|
|
4916
5862
|
async function installFromPath(name, sourcePath, engineUrl, force) {
|
|
4917
|
-
if (!
|
|
5863
|
+
if (!existsSync15(join14(sourcePath, "manifest.json"))) {
|
|
4918
5864
|
console.error("\u274C No manifest.json found at", sourcePath);
|
|
4919
5865
|
process.exit(1);
|
|
4920
5866
|
}
|
|
@@ -4945,7 +5891,7 @@ async function installFromPath(name, sourcePath, engineUrl, force) {
|
|
|
4945
5891
|
console.log(` Version: ${manifest.version || "?"}`);
|
|
4946
5892
|
console.log(` Category: ${category}`);
|
|
4947
5893
|
const enginePkgPath = join14(process.cwd(), "packages/engine/package.json");
|
|
4948
|
-
if (
|
|
5894
|
+
if (existsSync15(enginePkgPath)) {
|
|
4949
5895
|
const enginePkg = JSON.parse(await readFile(enginePkgPath, "utf-8"));
|
|
4950
5896
|
const engineVersion = enginePkg.version || "unknown";
|
|
4951
5897
|
if (manifest.zveltioMinVersion) {
|
|
@@ -4957,7 +5903,7 @@ async function installFromPath(name, sourcePath, engineUrl, force) {
|
|
|
4957
5903
|
}
|
|
4958
5904
|
const extName = manifest.name;
|
|
4959
5905
|
const targetDir = join14(process.cwd(), "extensions", category, extName);
|
|
4960
|
-
if (
|
|
5906
|
+
if (existsSync15(targetDir) && !force) {
|
|
4961
5907
|
console.error(`\u274C Extension already exists at ${targetDir}`);
|
|
4962
5908
|
console.error(` Use --force to overwrite.`);
|
|
4963
5909
|
process.exit(1);
|
|
@@ -5123,6 +6069,7 @@ async function promptHidden(question) {
|
|
|
5123
6069
|
}
|
|
5124
6070
|
async function createGodCommand(opts) {
|
|
5125
6071
|
const engineUrl = opts.url || process.env.ENGINE_URL || "http://localhost:3000";
|
|
6072
|
+
const recoveryToken = opts.recoveryToken || process.env.RECOVERY_TOKEN || "";
|
|
5126
6073
|
console.log(`
|
|
5127
6074
|
\u26A0\uFE0F SYSTEM RECOVERY OVERRIDE ACCOUNT`);
|
|
5128
6075
|
console.log("\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501");
|
|
@@ -5200,28 +6147,29 @@ Creating God (super-admin) user at ${engineUrl}
|
|
|
5200
6147
|
const err2 = await res.json().catch(() => ({ message: res.statusText }));
|
|
5201
6148
|
throw new Error(err2.message || `HTTP ${res.status}`);
|
|
5202
6149
|
}
|
|
5203
|
-
|
|
5204
|
-
|
|
5205
|
-
const loginRes = await fetch(`${engineUrl}/api/auth/sign-in/email`, {
|
|
5206
|
-
method: "POST",
|
|
5207
|
-
headers: { "Content-Type": "application/json" },
|
|
5208
|
-
body: JSON.stringify({ email: email.trim(), password })
|
|
5209
|
-
});
|
|
5210
|
-
if (!loginRes.ok) {
|
|
5211
|
-
console.warn("\u26A0\uFE0F Could not auto-grant admin role \u2014 grant manually via permissions API");
|
|
6150
|
+
await res.json();
|
|
6151
|
+
if (!recoveryToken) {
|
|
5212
6152
|
console.log(`
|
|
5213
|
-
\u2705 User created: ${email}
|
|
6153
|
+
\u2705 User created: ${email.trim()}`);
|
|
6154
|
+
console.warn(`
|
|
6155
|
+
\u26A0\uFE0F NOT granted the god role \u2014 no recovery token available.`);
|
|
6156
|
+
console.warn(" `god` is handed out by one endpoint only, and it requires a token:");
|
|
6157
|
+
console.warn(" RECOVERY_TOKEN=<32+ chars> zveltio create-god --email \u2026");
|
|
6158
|
+
console.warn(" zveltio create-god --recovery-token <token> --email \u2026");
|
|
6159
|
+
console.warn(" The engine must be started with the same RECOVERY_TOKEN set.");
|
|
6160
|
+
console.warn(" With direct database access, the release binary does it without a");
|
|
6161
|
+
console.warn(` token: DATABASE_URL=\u2026 zveltio create-god --email \u2026 --password \u2026
|
|
5214
6162
|
`);
|
|
5215
|
-
|
|
6163
|
+
process.exit(1);
|
|
5216
6164
|
}
|
|
5217
|
-
|
|
5218
|
-
const permRes = await fetch(`${engineUrl}/api/permissions/
|
|
6165
|
+
console.log("\uD83D\uDD11 Granting god permissions...");
|
|
6166
|
+
const permRes = await fetch(`${engineUrl}/api/permissions/bootstrap`, {
|
|
5219
6167
|
method: "POST",
|
|
5220
6168
|
headers: {
|
|
5221
6169
|
"Content-Type": "application/json",
|
|
5222
|
-
|
|
6170
|
+
Authorization: `Bearer ${recoveryToken}`
|
|
5223
6171
|
},
|
|
5224
|
-
body: JSON.stringify({
|
|
6172
|
+
body: JSON.stringify({ email: email.trim() })
|
|
5225
6173
|
});
|
|
5226
6174
|
if (permRes.ok) {
|
|
5227
6175
|
console.log(`
|
|
@@ -5229,15 +6177,20 @@ Creating God (super-admin) user at ${engineUrl}
|
|
|
5229
6177
|
console.log(` Email: ${email.trim()}`);
|
|
5230
6178
|
console.log(` Name: ${name.trim()}`);
|
|
5231
6179
|
console.log(` Role: god
|
|
6180
|
+
`);
|
|
6181
|
+
console.log(" The recovery token is now spent. Rotate RECOVERY_TOKEN before the");
|
|
6182
|
+
console.log(` next recovery, and note that any previous god was stood down.
|
|
5232
6183
|
`);
|
|
5233
6184
|
console.log(`\uD83D\uDC49 Log in at: ${engineUrl}/admin
|
|
5234
6185
|
`);
|
|
5235
6186
|
} else {
|
|
6187
|
+
const body = await permRes.json().catch(() => ({}));
|
|
5236
6188
|
console.log(`
|
|
5237
6189
|
\u2705 User created: ${email.trim()}`);
|
|
5238
|
-
console.
|
|
5239
|
-
|
|
6190
|
+
console.error(`
|
|
6191
|
+
\u274C The role was NOT granted: ${body.error ?? `engine returned ${permRes.status}`}
|
|
5240
6192
|
`);
|
|
6193
|
+
process.exit(1);
|
|
5241
6194
|
}
|
|
5242
6195
|
} catch (err2) {
|
|
5243
6196
|
rl2.close();
|
|
@@ -5319,20 +6272,27 @@ var rollbackCommand = new Command("rollback").description("Rollback database mig
|
|
|
5319
6272
|
process.exit(1);
|
|
5320
6273
|
}
|
|
5321
6274
|
process.env.DATABASE_URL = databaseUrl;
|
|
5322
|
-
|
|
5323
|
-
|
|
5324
|
-
|
|
5325
|
-
|
|
6275
|
+
let engine;
|
|
6276
|
+
try {
|
|
6277
|
+
engine = await loadEngineMigrationModules();
|
|
6278
|
+
} catch (err2) {
|
|
6279
|
+
console.error(`
|
|
6280
|
+
\u274C ${err2.message}
|
|
6281
|
+
`);
|
|
6282
|
+
process.exit(1);
|
|
6283
|
+
}
|
|
6284
|
+
const { initDatabase } = engine.db;
|
|
6285
|
+
const { getLastAppliedMigration, rollbackMigration } = engine.migrations;
|
|
5326
6286
|
let targetVersion;
|
|
5327
6287
|
if (opts.to !== undefined) {
|
|
5328
6288
|
targetVersion = parseInt(opts.to);
|
|
5329
|
-
if (isNaN(targetVersion)) {
|
|
6289
|
+
if (Number.isNaN(targetVersion)) {
|
|
5330
6290
|
console.error("\u274C --to must be a valid integer");
|
|
5331
6291
|
process.exit(1);
|
|
5332
6292
|
}
|
|
5333
6293
|
} else {
|
|
5334
6294
|
const steps = parseInt(opts.steps);
|
|
5335
|
-
if (isNaN(steps) || steps < 1) {
|
|
6295
|
+
if (Number.isNaN(steps) || steps < 1) {
|
|
5336
6296
|
console.error("\u274C --steps must be a positive integer");
|
|
5337
6297
|
process.exit(1);
|
|
5338
6298
|
}
|
|
@@ -5373,8 +6333,9 @@ var rollbackCommand = new Command("rollback").description("Rollback database mig
|
|
|
5373
6333
|
console.log(`
|
|
5374
6334
|
\u2705 Rolled back to schema version ${targetVersion}.
|
|
5375
6335
|
`);
|
|
5376
|
-
console.log(" \u26A0\uFE0F
|
|
5377
|
-
console.log(
|
|
6336
|
+
console.log(" \u26A0\uFE0F Before restarting, install the engine build that matches this");
|
|
6337
|
+
console.log(" schema version, or set MIGRATIONS_AUTO=false \u2014 restarting this build");
|
|
6338
|
+
console.log(` re-applies the rolled-back migrations.
|
|
5378
6339
|
`);
|
|
5379
6340
|
} catch (err2) {
|
|
5380
6341
|
console.error(`
|
|
@@ -5430,7 +6391,7 @@ var versionCommand = new Command("version").description("Show version informatio
|
|
|
5430
6391
|
});
|
|
5431
6392
|
|
|
5432
6393
|
// src/commands/update.ts
|
|
5433
|
-
import { existsSync as
|
|
6394
|
+
import { existsSync as existsSync16 } from "fs";
|
|
5434
6395
|
import { join as join15 } from "path";
|
|
5435
6396
|
var c12 = {
|
|
5436
6397
|
bold: (s) => `\x1B[1m${s}\x1B[0m`,
|
|
@@ -5448,7 +6409,7 @@ ${c12.bold("Zveltio Update")}
|
|
|
5448
6409
|
const installDir = opts.dir || process.env.ZVELTIO_DIR || join15(process.cwd(), "zveltio");
|
|
5449
6410
|
const metaPath = join15(installDir, ".zveltio-install.json");
|
|
5450
6411
|
let meta = null;
|
|
5451
|
-
if (
|
|
6412
|
+
if (existsSync16(metaPath)) {
|
|
5452
6413
|
try {
|
|
5453
6414
|
meta = JSON.parse(await Bun.file(metaPath).text());
|
|
5454
6415
|
} catch {}
|
|
@@ -5465,8 +6426,8 @@ ${c12.bold("Zveltio Update")}
|
|
|
5465
6426
|
throw new Error(`Registry returned ${res.status}`);
|
|
5466
6427
|
}
|
|
5467
6428
|
versionsData = await res.json();
|
|
5468
|
-
if (!versionsData?.latest) {
|
|
5469
|
-
throw new Error('Invalid versions response (
|
|
6429
|
+
if (!versionsData?.latest && !versionsData?.latest_beta) {
|
|
6430
|
+
throw new Error('Invalid versions response (no "latest" or "latest_beta")');
|
|
5470
6431
|
}
|
|
5471
6432
|
} catch (err2) {
|
|
5472
6433
|
if (opts.check) {
|
|
@@ -5496,6 +6457,13 @@ ${c12.bold("Zveltio Update")}
|
|
|
5496
6457
|
process.exit(1);
|
|
5497
6458
|
}
|
|
5498
6459
|
const targetVersion = opts.version || (opts.channel === "beta" ? versionsData.latest_beta ?? versionsData.latest : versionsData.latest);
|
|
6460
|
+
if (!targetVersion) {
|
|
6461
|
+
console.error(c12.red(`
|
|
6462
|
+
No stable release has been published yet.`));
|
|
6463
|
+
console.error(c12.dim(` Latest beta: v${versionsData.latest_beta} \u2014 run ${c12.cyan("zveltio update --channel beta")}
|
|
6464
|
+
`));
|
|
6465
|
+
process.exit(1);
|
|
6466
|
+
}
|
|
5499
6467
|
console.log(` Target version: ${c12.cyan(`v${targetVersion}`)}`);
|
|
5500
6468
|
if (opts.check) {
|
|
5501
6469
|
const hasUpdate = compareVersions(targetVersion, currentVersion) > 0;
|
|
@@ -5557,7 +6525,7 @@ async function getVersionFromPackageJson() {
|
|
|
5557
6525
|
join15(process.cwd(), "package.json")
|
|
5558
6526
|
];
|
|
5559
6527
|
for (const pkgPath of candidates) {
|
|
5560
|
-
if (!
|
|
6528
|
+
if (!existsSync16(pkgPath))
|
|
5561
6529
|
continue;
|
|
5562
6530
|
try {
|
|
5563
6531
|
const pkg = JSON.parse(await Bun.file(pkgPath).text());
|
|
@@ -5581,14 +6549,14 @@ async function getCurrentVersionFromAPI(port) {
|
|
|
5581
6549
|
}
|
|
5582
6550
|
async function runUpdateScript(targetVersion, installDir, meta, force) {
|
|
5583
6551
|
const envPath = join15(installDir, ".env");
|
|
5584
|
-
if (
|
|
6552
|
+
if (existsSync16(envPath)) {
|
|
5585
6553
|
const backupPath = `${envPath}.backup.${Date.now()}`;
|
|
5586
6554
|
const envContent = await Bun.file(envPath).text();
|
|
5587
6555
|
await Bun.write(backupPath, envContent);
|
|
5588
6556
|
console.log(` Backed up .env \u2192 ${backupPath}`);
|
|
5589
6557
|
}
|
|
5590
6558
|
const installScript = join15(installDir, "install.sh");
|
|
5591
|
-
if (
|
|
6559
|
+
if (existsSync16(installScript)) {
|
|
5592
6560
|
const args = [
|
|
5593
6561
|
"bash",
|
|
5594
6562
|
installScript,
|
|
@@ -5607,7 +6575,7 @@ async function runUpdateScript(targetVersion, installDir, meta, force) {
|
|
|
5607
6575
|
if (code !== 0) {
|
|
5608
6576
|
console.error(`
|
|
5609
6577
|
Update failed. Your previous version is still running.`);
|
|
5610
|
-
if (
|
|
6578
|
+
if (existsSync16(envPath)) {
|
|
5611
6579
|
console.log(` Restore .env: copy ${envPath}.backup.* to ${envPath}`);
|
|
5612
6580
|
}
|
|
5613
6581
|
process.exit(1);
|
|
@@ -5772,14 +6740,14 @@ ${c13.bold("Zveltio Status")}
|
|
|
5772
6740
|
console.log("");
|
|
5773
6741
|
}
|
|
5774
6742
|
async function getLocalVersion() {
|
|
5775
|
-
const { existsSync:
|
|
6743
|
+
const { existsSync: existsSync17 } = await import("fs");
|
|
5776
6744
|
const { join: join16 } = await import("path");
|
|
5777
6745
|
const candidates = [
|
|
5778
6746
|
join16(process.cwd(), "packages/engine/package.json"),
|
|
5779
6747
|
join16(process.cwd(), "package.json")
|
|
5780
6748
|
];
|
|
5781
6749
|
for (const p of candidates) {
|
|
5782
|
-
if (!
|
|
6750
|
+
if (!existsSync17(p))
|
|
5783
6751
|
continue;
|
|
5784
6752
|
try {
|
|
5785
6753
|
const pkg = JSON.parse(await Bun.file(p).text());
|
|
@@ -5792,7 +6760,7 @@ async function getLocalVersion() {
|
|
|
5792
6760
|
// package.json
|
|
5793
6761
|
var package_default = {
|
|
5794
6762
|
name: "@zveltio/cli",
|
|
5795
|
-
version: "3.0.0-beta.
|
|
6763
|
+
version: "3.0.0-beta.70",
|
|
5796
6764
|
type: "module",
|
|
5797
6765
|
bin: {
|
|
5798
6766
|
zveltio: "./dist/index.js"
|
|
@@ -5807,8 +6775,8 @@ var package_default = {
|
|
|
5807
6775
|
dev: "bun run --watch src/index.ts"
|
|
5808
6776
|
},
|
|
5809
6777
|
dependencies: {
|
|
5810
|
-
"@zveltio/sdk": "3.0.0-beta.
|
|
5811
|
-
commander: "^12.
|
|
6778
|
+
"@zveltio/sdk": "3.0.0-beta.70",
|
|
6779
|
+
commander: "^12.1.0"
|
|
5812
6780
|
},
|
|
5813
6781
|
devDependencies: {
|
|
5814
6782
|
"bun-types": "latest",
|
|
@@ -5822,7 +6790,7 @@ var package_default = {
|
|
|
5822
6790
|
|
|
5823
6791
|
// src/index.ts
|
|
5824
6792
|
var program2 = new Command;
|
|
5825
|
-
program2.name("zveltio").description("The official Zveltio CLI").version(package_default.version);
|
|
6793
|
+
program2.name("zveltio").allowExcessArguments(false).description("The official Zveltio CLI").version(package_default.version);
|
|
5826
6794
|
program2.command("init [dir]").description("Initialize a new Zveltio project (interactive)").option("--template <template>", "Starter template to use", "default").action(initCommand);
|
|
5827
6795
|
program2.command("dev").description("Start Zveltio in development mode (with hot reload)").option("-p, --port <port>", "Port to listen on", "3000").option("--no-studio", "Disable Studio embed (API only)").action(devCommand);
|
|
5828
6796
|
program2.command("start").description("Start Zveltio in production mode (uses compiled binary if available)").option("-p, --port <port>", "Port to listen on", "3000").option("--binary <path>", "Path to compiled binary").action(startCommand);
|
|
@@ -5832,7 +6800,7 @@ program2.addCommand(rollbackCommand);
|
|
|
5832
6800
|
program2.command("status").description("Show system status (DB, cache, uptime, version)").option("--url <url>", "Engine URL", "http://localhost:3000").option("--json", "Output as JSON").action(statusCommand);
|
|
5833
6801
|
program2.addCommand(versionCommand);
|
|
5834
6802
|
program2.addCommand(updateCommand);
|
|
5835
|
-
program2.command("create-god").description("Create the first super-admin (god) user interactively").option("--url <url>", "Engine URL", "http://localhost:3000").option("--email <email>", "Admin email (skip prompt)").option("--name <name>", "Admin name (skip prompt)").action(createGodCommand);
|
|
6803
|
+
program2.command("create-god").description("Create the first super-admin (god) user interactively").option("--url <url>", "Engine URL", "http://localhost:3000").option("--email <email>", "Admin email (skip prompt)").option("--name <name>", "Admin name (skip prompt)").option("--recovery-token <token>", "RECOVERY_TOKEN the engine was started with \u2014 required to grant the god role (env: RECOVERY_TOKEN)").action(createGodCommand);
|
|
5836
6804
|
var generate = program2.command("generate").description("Code generation utilities");
|
|
5837
6805
|
generate.command("types [collection]").description("Generate TypeScript types for collections (writes to ./types/zveltio.d.ts)").option("-o, --output <path>", "Output file path", "./types/zveltio.d.ts").option("--url <url>", "Engine URL", "http://localhost:3000").action(generateTypesCommand);
|
|
5838
6806
|
program2.command("generate-types [collection]").description("Generate TypeScript types for your collections").option("-o, --output <path>", "Output file path", "./types/zveltio.d.ts").option("--url <url>", "Engine URL", "http://localhost:3000").action(generateTypesCommand);
|
|
@@ -5860,10 +6828,10 @@ extensions.command("disable <name>").description("Disable an active extension").
|
|
|
5860
6828
|
console.log(`${name} disabled.`);
|
|
5861
6829
|
});
|
|
5862
6830
|
var ext = program2.command("extension").description('Manage Zveltio extensions (use "extensions" for new commands)');
|
|
5863
|
-
ext.command("create <name>").description("Scaffold a new extension").option("--category <category>", "Extension category", "custom").action((name, opts) => extensionCommand("create", name, opts));
|
|
6831
|
+
ext.command("create <name>").description("Scaffold a new extension (admin page as an SDUI schema)").option("--category <category>", "Extension category", "custom").option("--code-page", "Scaffold a Svelte page instead of a schema \u2014 for UI a schema cannot express (canvas, chat, map, inbox)").action((name, opts) => extensionCommand("create", name, opts));
|
|
5864
6832
|
ext.command("build").description("[deprecated] Alias for `extension pack` \u2014 produces the v2 engine bundle").option("--dir <dir>", "Extension root directory (defaults to cwd)").action((opts) => extensionCommand("build", "", opts));
|
|
5865
6833
|
ext.command("dev").description("Watch engine/ + studio/, hot-reload the running engine on changes (S4-03)").option("--dir <dir>", "Extension root directory (defaults to cwd)").option("--url <url>", "Engine URL (env: ZVELTIO_ENGINE_URL)", "http://localhost:3000").option("--name <name>", "Extension name (default: read from manifest.json)").option("--no-studio", "Skip the Studio dev process (engine watch only)").action((opts) => extensionDevCommand(opts));
|
|
5866
|
-
ext.command("pack").description("Bundle engine/index.ts \u2192 engine/index.js (target=bun, ESM) and write engine + integrity blocks into manifest.json. Required before publish on v2 channels.").option("--dir <dir>", "Extension root directory (defaults to cwd)").option("--sourcemap", "Emit engine/index.js.map alongside the bundle").option("--no-manifest-update", "Build the bundle but do not patch manifest.json").option("--first-party", "Vendor / monorepo build \u2014 keep inline isolation, skip worker auto-inject").option("--token <token>", "Registry token for the publisher-tier lookup (env: ZVELTIO_REGISTRY_TOKEN)").option("--registry-url <url>", "Registry base URL (env: ZVELTIO_REGISTRY_URL)").action(async (opts) => {
|
|
6834
|
+
ext.command("pack").description("Bundle engine/index.ts \u2192 engine/index.js (target=bun, ESM) and write engine + integrity blocks into manifest.json. Required before publish on v2 channels.").option("--dir <dir>", "Extension root directory (defaults to cwd)").option("--sourcemap", "Emit engine/index.js.map alongside the bundle").option("--no-manifest-update", "Build the bundle but do not patch manifest.json").option("--first-party", "Vendor / monorepo build \u2014 keep inline isolation, skip worker auto-inject").option("--keep-isolation", 'With --first-party, retain engine.isolation: "worker" if already set (default: clear sticky worker)').option("--token <token>", "Registry token for the publisher-tier lookup (env: ZVELTIO_REGISTRY_TOKEN)").option("--registry-url <url>", "Registry base URL (env: ZVELTIO_REGISTRY_URL)").action(async (opts) => {
|
|
5867
6835
|
const { extensionPackCommand: extensionPackCommand2 } = await Promise.resolve().then(() => (init_extension_pack(), exports_extension_pack));
|
|
5868
6836
|
return extensionPackCommand2(opts);
|
|
5869
6837
|
});
|