@specific.dev/cli 0.1.133 → 0.1.135
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/admin/404/index.html +1 -1
- package/dist/admin/404.html +1 -1
- package/dist/admin/__next.!KGRlZmF1bHQp.__PAGE__.txt +1 -1
- package/dist/admin/__next.!KGRlZmF1bHQp.txt +1 -1
- package/dist/admin/__next._full.txt +1 -1
- package/dist/admin/__next._head.txt +1 -1
- package/dist/admin/__next._index.txt +1 -1
- package/dist/admin/__next._tree.txt +1 -1
- package/dist/admin/_not-found/__next._full.txt +1 -1
- package/dist/admin/_not-found/__next._head.txt +1 -1
- package/dist/admin/_not-found/__next._index.txt +1 -1
- package/dist/admin/_not-found/__next._not-found.__PAGE__.txt +1 -1
- package/dist/admin/_not-found/__next._not-found.txt +1 -1
- package/dist/admin/_not-found/__next._tree.txt +1 -1
- package/dist/admin/_not-found/index.html +1 -1
- package/dist/admin/_not-found/index.txt +1 -1
- package/dist/admin/databases/__next.!KGRlZmF1bHQp.databases.__PAGE__.txt +1 -1
- package/dist/admin/databases/__next.!KGRlZmF1bHQp.databases.txt +1 -1
- package/dist/admin/databases/__next.!KGRlZmF1bHQp.txt +1 -1
- package/dist/admin/databases/__next._full.txt +1 -1
- package/dist/admin/databases/__next._head.txt +1 -1
- package/dist/admin/databases/__next._index.txt +1 -1
- package/dist/admin/databases/__next._tree.txt +1 -1
- package/dist/admin/databases/index.html +1 -1
- package/dist/admin/databases/index.txt +1 -1
- package/dist/admin/fullscreen/__next._full.txt +1 -1
- package/dist/admin/fullscreen/__next._head.txt +1 -1
- package/dist/admin/fullscreen/__next._index.txt +1 -1
- package/dist/admin/fullscreen/__next._tree.txt +1 -1
- package/dist/admin/fullscreen/__next.fullscreen.__PAGE__.txt +1 -1
- package/dist/admin/fullscreen/__next.fullscreen.txt +1 -1
- package/dist/admin/fullscreen/databases/__next._full.txt +1 -1
- package/dist/admin/fullscreen/databases/__next._head.txt +1 -1
- package/dist/admin/fullscreen/databases/__next._index.txt +1 -1
- package/dist/admin/fullscreen/databases/__next._tree.txt +1 -1
- package/dist/admin/fullscreen/databases/__next.fullscreen.databases.__PAGE__.txt +1 -1
- package/dist/admin/fullscreen/databases/__next.fullscreen.databases.txt +1 -1
- package/dist/admin/fullscreen/databases/__next.fullscreen.txt +1 -1
- package/dist/admin/fullscreen/databases/index.html +1 -1
- package/dist/admin/fullscreen/databases/index.txt +1 -1
- package/dist/admin/fullscreen/index.html +1 -1
- package/dist/admin/fullscreen/index.txt +1 -1
- package/dist/admin/index.html +1 -1
- package/dist/admin/index.txt +1 -1
- package/dist/admin/mail/__next.!KGRlZmF1bHQp.mail.__PAGE__.txt +1 -1
- package/dist/admin/mail/__next.!KGRlZmF1bHQp.mail.txt +1 -1
- package/dist/admin/mail/__next.!KGRlZmF1bHQp.txt +1 -1
- package/dist/admin/mail/__next._full.txt +1 -1
- package/dist/admin/mail/__next._head.txt +1 -1
- package/dist/admin/mail/__next._index.txt +1 -1
- package/dist/admin/mail/__next._tree.txt +1 -1
- package/dist/admin/mail/index.html +1 -1
- package/dist/admin/mail/index.txt +1 -1
- package/dist/admin/workflows/__next.!KGRlZmF1bHQp.txt +1 -1
- package/dist/admin/workflows/__next.!KGRlZmF1bHQp.workflows.__PAGE__.txt +1 -1
- package/dist/admin/workflows/__next.!KGRlZmF1bHQp.workflows.txt +1 -1
- package/dist/admin/workflows/__next._full.txt +1 -1
- package/dist/admin/workflows/__next._head.txt +1 -1
- package/dist/admin/workflows/__next._index.txt +1 -1
- package/dist/admin/workflows/__next._tree.txt +1 -1
- package/dist/admin/workflows/index.html +1 -1
- package/dist/admin/workflows/index.txt +1 -1
- package/dist/cli.js +236 -6
- package/dist/docs/migrations/lovable.md +99 -0
- package/dist/docs/migrations/supabase.md +4 -0
- package/package.json +1 -1
- /package/dist/admin/_next/static/{k4CkHzJBOS7_uNab7bdAE → Fbj_wbG8LdwY4QCJE_Iko}/_buildManifest.js +0 -0
- /package/dist/admin/_next/static/{k4CkHzJBOS7_uNab7bdAE → Fbj_wbG8LdwY4QCJE_Iko}/_clientMiddlewareManifest.json +0 -0
- /package/dist/admin/_next/static/{k4CkHzJBOS7_uNab7bdAE → Fbj_wbG8LdwY4QCJE_Iko}/_ssgManifest.js +0 -0
package/dist/cli.js
CHANGED
|
@@ -367797,6 +367797,27 @@ var ApiClient = class {
|
|
|
367797
367797
|
}
|
|
367798
367798
|
return response.json();
|
|
367799
367799
|
}
|
|
367800
|
+
async runObservabilityQuery(environmentId, sql) {
|
|
367801
|
+
const url = `${this.baseUrl}/environments/${environmentId}/observability/query`;
|
|
367802
|
+
writeLog("api", `POST ${url}`);
|
|
367803
|
+
const response = await fetch(url, {
|
|
367804
|
+
method: "POST",
|
|
367805
|
+
headers: {
|
|
367806
|
+
"Content-Type": "application/json",
|
|
367807
|
+
...await this.authHeaders()
|
|
367808
|
+
},
|
|
367809
|
+
body: JSON.stringify({ sql })
|
|
367810
|
+
});
|
|
367811
|
+
writeLog("api", `Response: ${response.status} ${response.statusText}`);
|
|
367812
|
+
if (!response.ok) {
|
|
367813
|
+
throw await readApiError(
|
|
367814
|
+
response,
|
|
367815
|
+
"Query failed",
|
|
367816
|
+
`POST ${url}`
|
|
367817
|
+
);
|
|
367818
|
+
}
|
|
367819
|
+
return response.json();
|
|
367820
|
+
}
|
|
367800
367821
|
};
|
|
367801
367822
|
var SpecificClient = class {
|
|
367802
367823
|
client;
|
|
@@ -367856,6 +367877,10 @@ var SpecificClient = class {
|
|
|
367856
367877
|
async deletePreviewEnvironment(environmentId) {
|
|
367857
367878
|
return this.client.deletePreviewEnvironment(environmentId);
|
|
367858
367879
|
}
|
|
367880
|
+
// --- Observability ---
|
|
367881
|
+
async runObservabilityQuery(environmentId, sql) {
|
|
367882
|
+
return this.client.runObservabilityQuery(environmentId, sql);
|
|
367883
|
+
}
|
|
367859
367884
|
};
|
|
367860
367885
|
function toDeployment(response) {
|
|
367861
367886
|
return {
|
|
@@ -373693,7 +373718,7 @@ function trackEvent(event, properties) {
|
|
|
373693
373718
|
event,
|
|
373694
373719
|
properties: {
|
|
373695
373720
|
...properties,
|
|
373696
|
-
cli_version: "0.1.
|
|
373721
|
+
cli_version: "0.1.135",
|
|
373697
373722
|
platform: process.platform,
|
|
373698
373723
|
node_version: process.version,
|
|
373699
373724
|
project_id: getProjectId()
|
|
@@ -373708,14 +373733,14 @@ async function shutdown() {
|
|
|
373708
373733
|
}
|
|
373709
373734
|
|
|
373710
373735
|
// src/commands/init.tsx
|
|
373711
|
-
var SPECIFIC_INSTRUCTIONS = `ALWAYS use Specific
|
|
373736
|
+
var SPECIFIC_INSTRUCTIONS = `ALWAYS use Specific to define infrastructure and development environments, like frontend, backend, databases, workflows, secrets and more. Run \`specific docs\` to learn how to use Specific. ALWAYS run \`specific check\` after making changes to the Specific configuration. ALWAYS use \`specific dev\` to run the project locally.`;
|
|
373712
373737
|
var CURSOR_MDC_CONTENT = `---
|
|
373713
373738
|
description: Specific infrastructure rules
|
|
373714
373739
|
globs:
|
|
373715
373740
|
alwaysApply: true
|
|
373716
373741
|
---
|
|
373717
373742
|
|
|
373718
|
-
${SPECIFIC_INSTRUCTIONS}
|
|
373743
|
+
ALWAYS use Specific for new projects. ${SPECIFIC_INSTRUCTIONS}
|
|
373719
373744
|
`;
|
|
373720
373745
|
var options = [
|
|
373721
373746
|
{ id: "cursor", label: "Cursor" },
|
|
@@ -377799,7 +377824,7 @@ function compareVersions(a, b) {
|
|
|
377799
377824
|
return 0;
|
|
377800
377825
|
}
|
|
377801
377826
|
async function checkForUpdate() {
|
|
377802
|
-
const currentVersion = "0.1.
|
|
377827
|
+
const currentVersion = "0.1.135";
|
|
377803
377828
|
const response = await fetch(`${BINARIES_BASE_URL}/latest?t=${Date.now()}`);
|
|
377804
377829
|
if (!response.ok) {
|
|
377805
377830
|
throw new Error(`Failed to check for updates: HTTP ${response.status}`);
|
|
@@ -378107,17 +378132,214 @@ function StatusUI() {
|
|
|
378107
378132
|
return /* @__PURE__ */ React12.createElement(Text12, { color: "red" }, "Failed to get status: ", state.error);
|
|
378108
378133
|
}
|
|
378109
378134
|
const status = state.status;
|
|
378110
|
-
return /* @__PURE__ */ React12.createElement(Box11, { flexDirection: "column", gap: 1 }, /* @__PURE__ */ React12.createElement(Text12, null, /* @__PURE__ */ React12.createElement(Text12, { bold: true }, status.name), /* @__PURE__ */ React12.createElement(Text12, { dimColor: true }, " (", status.id, ")")), status.environments.length === 0 ? /* @__PURE__ */ React12.createElement(Text12, { dimColor: true }, "No environments found.") : status.environments.map((env2) => /* @__PURE__ */ React12.createElement(Box11, { key: env2.id, flexDirection: "column" }, /* @__PURE__ */ React12.createElement(Text12, null, /* @__PURE__ */ React12.createElement(Text12, { bold: true, color: "cyan" }, env2.name), /* @__PURE__ */ React12.createElement(Text12, { dimColor: true }, " (", env2.id, ")")), /* @__PURE__ */ React12.createElement(Text12, null, " ", "Status: ", /* @__PURE__ */ React12.createElement(Text12, { color: statusColor(env2.status), bold: true }, env2.status)), /* @__PURE__ */ React12.createElement(Text12, null, " ", "Active deployment:", " ", env2.activeDeployment ? /* @__PURE__ */ React12.createElement(
|
|
378135
|
+
return /* @__PURE__ */ React12.createElement(Box11, { flexDirection: "column", gap: 1 }, /* @__PURE__ */ React12.createElement(Text12, null, /* @__PURE__ */ React12.createElement(Text12, { bold: true }, status.name), /* @__PURE__ */ React12.createElement(Text12, { dimColor: true }, " (", status.id, ")")), status.environments.length === 0 ? /* @__PURE__ */ React12.createElement(Text12, { dimColor: true }, "No environments found.") : status.environments.map((env2) => /* @__PURE__ */ React12.createElement(Box11, { key: env2.id, flexDirection: "column" }, /* @__PURE__ */ React12.createElement(Text12, null, /* @__PURE__ */ React12.createElement(Text12, { bold: true, color: "cyan" }, env2.name), /* @__PURE__ */ React12.createElement(Text12, { dimColor: true }, " (", env2.id, ")")), /* @__PURE__ */ React12.createElement(Text12, null, " ", "Status: ", /* @__PURE__ */ React12.createElement(Text12, { color: statusColor(env2.status), bold: true }, env2.status)), /* @__PURE__ */ React12.createElement(Text12, null, " ", "Active deployment:", " ", env2.activeDeployment ? /* @__PURE__ */ React12.createElement(Text12, { color: "green" }, env2.activeDeployment.id) : /* @__PURE__ */ React12.createElement(Text12, { dimColor: true }, "none")), env2.inProgressDeployment && /* @__PURE__ */ React12.createElement(Text12, null, " ", "In progress:", " ", /* @__PURE__ */ React12.createElement(Text12, { color: "yellow" }, env2.inProgressDeployment.id), /* @__PURE__ */ React12.createElement(Text12, { dimColor: true }, " (", env2.inProgressDeployment.state, ")")), env2.publicUrls && Object.keys(env2.publicUrls).length > 0 && /* @__PURE__ */ React12.createElement(Box11, { flexDirection: "column" }, /* @__PURE__ */ React12.createElement(Text12, null, " ", "Public URLs:"), Object.entries(env2.publicUrls).map(([serviceName, url]) => /* @__PURE__ */ React12.createElement(Text12, { key: serviceName }, " - ", /* @__PURE__ */ React12.createElement(Text12, { color: "cyan" }, serviceName), ": ", /* @__PURE__ */ React12.createElement(Text12, { color: "blue" }, url)))))));
|
|
378111
378136
|
}
|
|
378112
378137
|
function statusCommand() {
|
|
378113
378138
|
render10(/* @__PURE__ */ React12.createElement(StatusUI, null));
|
|
378114
378139
|
}
|
|
378115
378140
|
|
|
378141
|
+
// src/lib/query/render-table.ts
|
|
378142
|
+
var MAX_COL_WIDTH = 60;
|
|
378143
|
+
var ANSI = {
|
|
378144
|
+
dim: "\x1B[2m",
|
|
378145
|
+
reset: "\x1B[0m"
|
|
378146
|
+
};
|
|
378147
|
+
function renderTable(input) {
|
|
378148
|
+
const { columns, rows, stats } = input;
|
|
378149
|
+
if (columns.length === 0) {
|
|
378150
|
+
return "(no columns)\n";
|
|
378151
|
+
}
|
|
378152
|
+
const cellMatrix = rows.map(
|
|
378153
|
+
(row) => columns.map((col, i) => formatCell(row[i], col.type))
|
|
378154
|
+
);
|
|
378155
|
+
const widths = columns.map((col, i) => {
|
|
378156
|
+
const headerWidth = visibleLength(col.name);
|
|
378157
|
+
const typeWidth = visibleLength(col.type);
|
|
378158
|
+
let max = Math.max(headerWidth, typeWidth);
|
|
378159
|
+
for (const row of cellMatrix) {
|
|
378160
|
+
const w = visibleLength(row[i]);
|
|
378161
|
+
if (w > max) max = w;
|
|
378162
|
+
}
|
|
378163
|
+
return Math.min(max, MAX_COL_WIDTH);
|
|
378164
|
+
});
|
|
378165
|
+
const aligns = columns.map(
|
|
378166
|
+
(col) => isNumericType(col.type) ? "right" : "left"
|
|
378167
|
+
);
|
|
378168
|
+
const out = [];
|
|
378169
|
+
out.push(borderLine(widths, "top"));
|
|
378170
|
+
out.push(headerRow(columns.map((c) => c.name), widths, aligns));
|
|
378171
|
+
out.push(headerRow(
|
|
378172
|
+
columns.map((c) => c.type),
|
|
378173
|
+
widths,
|
|
378174
|
+
aligns,
|
|
378175
|
+
/*dim*/
|
|
378176
|
+
true
|
|
378177
|
+
));
|
|
378178
|
+
out.push(borderLine(widths, "mid"));
|
|
378179
|
+
for (const row of cellMatrix) {
|
|
378180
|
+
out.push(dataRow(row, widths, aligns));
|
|
378181
|
+
}
|
|
378182
|
+
out.push(borderLine(widths, "bottom"));
|
|
378183
|
+
out.push(statsLine(stats, rows.length));
|
|
378184
|
+
return out.join("\n") + "\n";
|
|
378185
|
+
}
|
|
378186
|
+
function formatCell(value, type) {
|
|
378187
|
+
if (value === null || value === void 0) {
|
|
378188
|
+
return `${ANSI.dim}NULL${ANSI.reset}`;
|
|
378189
|
+
}
|
|
378190
|
+
if (type.startsWith("Map(") && typeof value === "object") {
|
|
378191
|
+
return JSON.stringify(value);
|
|
378192
|
+
}
|
|
378193
|
+
if (type.startsWith("Array(") && Array.isArray(value)) {
|
|
378194
|
+
return JSON.stringify(value);
|
|
378195
|
+
}
|
|
378196
|
+
if (typeof value === "number") {
|
|
378197
|
+
return formatNumber(value);
|
|
378198
|
+
}
|
|
378199
|
+
if (typeof value === "string") {
|
|
378200
|
+
if (type.startsWith("DateTime") && /^\d{4}-\d{2}-\d{2} /.test(value)) {
|
|
378201
|
+
return value.replace(" ", "T") + "Z";
|
|
378202
|
+
}
|
|
378203
|
+
return value;
|
|
378204
|
+
}
|
|
378205
|
+
return String(value);
|
|
378206
|
+
}
|
|
378207
|
+
function formatNumber(n) {
|
|
378208
|
+
if (Number.isInteger(n)) {
|
|
378209
|
+
return n.toLocaleString("en-US");
|
|
378210
|
+
}
|
|
378211
|
+
return n.toString();
|
|
378212
|
+
}
|
|
378213
|
+
function isNumericType(type) {
|
|
378214
|
+
const inner = unwrapType(type);
|
|
378215
|
+
return /^(U?Int|Float|Decimal)/.test(inner);
|
|
378216
|
+
}
|
|
378217
|
+
function unwrapType(type) {
|
|
378218
|
+
const m = type.match(/^(?:Nullable|LowCardinality)\((.+)\)$/);
|
|
378219
|
+
return m ? unwrapType(m[1]) : type;
|
|
378220
|
+
}
|
|
378221
|
+
function visibleLength(s) {
|
|
378222
|
+
return s.replace(/\x1b\[[0-9;]*m/g, "").length;
|
|
378223
|
+
}
|
|
378224
|
+
function pad(s, width, align) {
|
|
378225
|
+
const visible = visibleLength(s);
|
|
378226
|
+
if (visible > width) {
|
|
378227
|
+
return truncate(s, width);
|
|
378228
|
+
}
|
|
378229
|
+
const padding = " ".repeat(width - visible);
|
|
378230
|
+
return align === "right" ? padding + s : s + padding;
|
|
378231
|
+
}
|
|
378232
|
+
function truncate(s, width) {
|
|
378233
|
+
if (width <= 1) return "\u2026";
|
|
378234
|
+
return s.slice(0, width - 1) + "\u2026";
|
|
378235
|
+
}
|
|
378236
|
+
function borderLine(widths, pos) {
|
|
378237
|
+
const left = pos === "top" ? "\u250C" : pos === "mid" ? "\u251C" : "\u2514";
|
|
378238
|
+
const sep2 = pos === "top" ? "\u252C" : pos === "mid" ? "\u253C" : "\u2534";
|
|
378239
|
+
const right = pos === "top" ? "\u2510" : pos === "mid" ? "\u2524" : "\u2518";
|
|
378240
|
+
return left + widths.map((w) => "\u2500".repeat(w + 2)).join(sep2) + right;
|
|
378241
|
+
}
|
|
378242
|
+
function headerRow(values, widths, aligns, dim = false) {
|
|
378243
|
+
const cells = values.map((v, i) => {
|
|
378244
|
+
const padded = pad(v, widths[i], aligns[i]);
|
|
378245
|
+
return dim ? `${ANSI.dim}${padded}${ANSI.reset}` : padded;
|
|
378246
|
+
});
|
|
378247
|
+
return "\u2502 " + cells.join(" \u2502 ") + " \u2502";
|
|
378248
|
+
}
|
|
378249
|
+
function dataRow(row, widths, aligns) {
|
|
378250
|
+
const cells = row.map((v, i) => pad(v, widths[i], aligns[i]));
|
|
378251
|
+
return "\u2502 " + cells.join(" \u2502 ") + " \u2502";
|
|
378252
|
+
}
|
|
378253
|
+
function statsLine(stats, returned) {
|
|
378254
|
+
const parts = [`${returned.toLocaleString("en-US")} rows`];
|
|
378255
|
+
if (stats.elapsed !== void 0) {
|
|
378256
|
+
parts.push(`${formatElapsed(stats.elapsed)}`);
|
|
378257
|
+
}
|
|
378258
|
+
if (stats.rows_read !== void 0) {
|
|
378259
|
+
parts.push(`${stats.rows_read.toLocaleString("en-US")} rows scanned`);
|
|
378260
|
+
}
|
|
378261
|
+
return `${ANSI.dim}${parts.join(" \xB7 ")}${ANSI.reset}`;
|
|
378262
|
+
}
|
|
378263
|
+
function formatElapsed(seconds) {
|
|
378264
|
+
const ms = seconds * 1e3;
|
|
378265
|
+
if (ms < 1e3) return `${ms.toFixed(0)}ms`;
|
|
378266
|
+
return `${seconds.toFixed(2)}s`;
|
|
378267
|
+
}
|
|
378268
|
+
|
|
378269
|
+
// src/commands/query.tsx
|
|
378270
|
+
async function queryCommand(sqlArg, options2) {
|
|
378271
|
+
try {
|
|
378272
|
+
const sql = await resolveSql(sqlArg);
|
|
378273
|
+
const projectId = readProjectId();
|
|
378274
|
+
const token = await getValidAccessToken();
|
|
378275
|
+
const client2 = new SpecificClient({ accessToken: token });
|
|
378276
|
+
const environmentId = await resolveEnvironment(client2, projectId, options2.env);
|
|
378277
|
+
const result = await client2.runObservabilityQuery(environmentId, sql);
|
|
378278
|
+
process.stdout.write(renderTable(result));
|
|
378279
|
+
} catch (err) {
|
|
378280
|
+
const message = err instanceof Error ? err.message : "Unknown error occurred";
|
|
378281
|
+
const friendly = friendlyErrorMessage(message);
|
|
378282
|
+
console.error(`Error: ${friendly}`);
|
|
378283
|
+
process.exit(1);
|
|
378284
|
+
}
|
|
378285
|
+
}
|
|
378286
|
+
async function resolveSql(arg) {
|
|
378287
|
+
if (arg && arg !== "-") return arg;
|
|
378288
|
+
if (process.stdin.isTTY) {
|
|
378289
|
+
throw new Error("Provide SQL as an argument or via stdin.");
|
|
378290
|
+
}
|
|
378291
|
+
return await readStdin();
|
|
378292
|
+
}
|
|
378293
|
+
async function readStdin() {
|
|
378294
|
+
return new Promise((resolve9, reject) => {
|
|
378295
|
+
let data = "";
|
|
378296
|
+
process.stdin.setEncoding("utf-8");
|
|
378297
|
+
process.stdin.on("data", (chunk) => data += chunk);
|
|
378298
|
+
process.stdin.on("end", () => resolve9(data.trim()));
|
|
378299
|
+
process.stdin.on("error", reject);
|
|
378300
|
+
});
|
|
378301
|
+
}
|
|
378302
|
+
async function resolveEnvironment(client2, projectId, envFlag) {
|
|
378303
|
+
const status = await client2.getProjectStatus(projectId);
|
|
378304
|
+
const environments = status.environments;
|
|
378305
|
+
if (envFlag) {
|
|
378306
|
+
const match = environments.find((e) => e.name === envFlag);
|
|
378307
|
+
if (!match) {
|
|
378308
|
+
const available = environments.map((e) => e.name).join(", ") || "(none)";
|
|
378309
|
+
throw new Error(
|
|
378310
|
+
`Environment "${envFlag}" not found. Available: ${available}`
|
|
378311
|
+
);
|
|
378312
|
+
}
|
|
378313
|
+
return match.id;
|
|
378314
|
+
}
|
|
378315
|
+
if (hasEnvironmentId()) {
|
|
378316
|
+
const savedId = readEnvironmentId();
|
|
378317
|
+
if (environments.some((e) => e.id === savedId)) {
|
|
378318
|
+
return savedId;
|
|
378319
|
+
}
|
|
378320
|
+
}
|
|
378321
|
+
if (environments.length === 1) {
|
|
378322
|
+
return environments[0].id;
|
|
378323
|
+
}
|
|
378324
|
+
if (environments.length === 0) {
|
|
378325
|
+
throw new Error("This project has no environments yet.");
|
|
378326
|
+
}
|
|
378327
|
+
const names = environments.map((e) => e.name).join(", ");
|
|
378328
|
+
throw new Error(
|
|
378329
|
+
`Project has multiple environments (${names}). Pass --env <name>.`
|
|
378330
|
+
);
|
|
378331
|
+
}
|
|
378332
|
+
function friendlyErrorMessage(raw) {
|
|
378333
|
+
const m = raw.match(/^Query failed: (.+?) \([A-Z_]+\)$/);
|
|
378334
|
+
if (m) return m[1];
|
|
378335
|
+
return raw;
|
|
378336
|
+
}
|
|
378337
|
+
|
|
378116
378338
|
// src/cli-program.tsx
|
|
378117
378339
|
var program = new Command();
|
|
378118
378340
|
var env = "production";
|
|
378119
378341
|
var envLabel = env !== "production" ? `[${env.toUpperCase()}] ` : "";
|
|
378120
|
-
program.name("specific").description(`${envLabel}Infrastructure-as-code for coding agents`).version("0.1.
|
|
378342
|
+
program.name("specific").description(`${envLabel}Infrastructure-as-code for coding agents`).version("0.1.135").enablePositionalOptions();
|
|
378121
378343
|
program.command("init").description("Initialize project for use with a coding agent").option("--agent <name...>", "Agents to configure (cursor, claude, codex, other)").addHelpText("after", `
|
|
378122
378344
|
Examples:
|
|
378123
378345
|
$ specific init
|
|
@@ -378194,6 +378416,14 @@ Examples:
|
|
|
378194
378416
|
program.command("status").description("Show project, environments, and deployment status").addHelpText("after", `
|
|
378195
378417
|
Examples:
|
|
378196
378418
|
$ specific status`).action(statusCommand);
|
|
378419
|
+
program.command("query [sql]").description("Run a SQL query against your environment's observability data").option("--env <name>", "Target environment (defaults to the current one)").addHelpText("after", `
|
|
378420
|
+
Examples:
|
|
378421
|
+
$ specific query "SELECT count() FROM observability.logs"
|
|
378422
|
+
$ specific query --env staging "SELECT * FROM observability.logs LIMIT 5"
|
|
378423
|
+
$ cat queries/p99.sql | specific query
|
|
378424
|
+
$ specific query - < queries/p99.sql`).action((sql, options2) => {
|
|
378425
|
+
queryCommand(sql, options2);
|
|
378426
|
+
});
|
|
378197
378427
|
program.command("beta").description("Manage beta feature flags").action(betaCommand);
|
|
378198
378428
|
program.command("update").description("Update Specific CLI to the latest version").action(updateCommand);
|
|
378199
378429
|
program.command("login").description("Log in to Specific").option("--device-code <code>", "Complete login using a device code from a previous non-interactive login").action((options2) => loginCommand(options2));
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
# Lovable
|
|
2
|
+
|
|
3
|
+
Lovable is an AI coding and hosting platform for building full-stack apps. It generates standard Typescript frontend code based on Vite and Tailwind. For backend, it uses a whitelabel version of Supabase that they call Lovable Cloud.
|
|
4
|
+
|
|
5
|
+
Lovable app frontends are implemented in Typescript with React, Vite, React Router, Tailwind and shadcn. Write a `specific.hcl` with a `web` service to run this both locally and once deployed.
|
|
6
|
+
|
|
7
|
+
## Frontend
|
|
8
|
+
|
|
9
|
+
Lovable frontends are served as purely static content, which we do not support on Specific and instead the static files must be served using a web server. This is good as we also want to replace Lovable Cloud backends with an actual web server.
|
|
10
|
+
|
|
11
|
+
For this, we are going to use the Hono web framework together with the `@hono/node-server` and `@hono/vite-dev-server` packages to make it work with the Vite frontend. Do the following:
|
|
12
|
+
|
|
13
|
+
1. Install packages: `npm install tsx hono @hono/node-server @hono/vite-dev-server`
|
|
14
|
+
2. Create a boilerplate Hono app in `src/server/index.ts`:
|
|
15
|
+
|
|
16
|
+
```ts
|
|
17
|
+
import { Hono } from "hono";
|
|
18
|
+
|
|
19
|
+
const app = new Hono();
|
|
20
|
+
|
|
21
|
+
// Implement any backend functionality as routes under `/api`
|
|
22
|
+
app.get("/api/hello", (c) => c.json({ message: "Hello from Hono!" }));
|
|
23
|
+
|
|
24
|
+
export default app;
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
3. Create a prod entrypoint in `src/server/prod.ts`:
|
|
28
|
+
|
|
29
|
+
```ts
|
|
30
|
+
import { serve } from "@hono/node-server";
|
|
31
|
+
import { serveStatic } from "@hono/node-server/serve-static";
|
|
32
|
+
import app from "./index";
|
|
33
|
+
|
|
34
|
+
// Serve frontend static files
|
|
35
|
+
app.use("/*", serveStatic({ root: "./dist" }));
|
|
36
|
+
app.get("*", serveStatic({ path: "./dist/index.html" }));
|
|
37
|
+
|
|
38
|
+
const port = Number(process.env.PORT ?? 3000);
|
|
39
|
+
serve({ fetch: app.fetch, port }, (info) => {
|
|
40
|
+
console.log(`Server running on http://localhost:${info.port}`);
|
|
41
|
+
});
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
4. Add `start` command to `package.json` to run production server: `tsx src/server/prod.ts`
|
|
45
|
+
5. Update the existing `dev` command in `package.json` to: `vite --port $PORT`
|
|
46
|
+
5. Add plugin to `vite.config.ts` to run Hono during development:
|
|
47
|
+
|
|
48
|
+
```ts
|
|
49
|
+
// Add inside `plugins`
|
|
50
|
+
devServer({
|
|
51
|
+
entry: "src/server/index.ts",
|
|
52
|
+
// Only route /api/* through Hono; everything else stays with Vite (SPA + HMR)
|
|
53
|
+
exclude: [/^(?!\/api(\/|$)).*/],
|
|
54
|
+
injectClientScript: false,
|
|
55
|
+
}),
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
6. Finally, create a `specific.hcl` to run the app as a single service in both prod and dev:
|
|
59
|
+
|
|
60
|
+
```hcl
|
|
61
|
+
build "web" {
|
|
62
|
+
command = "npm run build"
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
service "web" {
|
|
66
|
+
build = build.web
|
|
67
|
+
command = "npm start"
|
|
68
|
+
|
|
69
|
+
dev {
|
|
70
|
+
command = "npm run dev"
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// Add any environment variables needed for secrets, configs and databases
|
|
74
|
+
env = {
|
|
75
|
+
PORT = port
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
Make sure to run `specific dev` and check the logs to ensure the app starts as expected.
|
|
81
|
+
|
|
82
|
+
## Backend (database, authentication, edge functions)
|
|
83
|
+
|
|
84
|
+
Lovable Cloud backends are based on Supabase. Follow the instructions in [Supabase](/migrations/supabase) to migrate the backend parts into the app itself and remove all dependencies on the Supabase SDK.
|
|
85
|
+
|
|
86
|
+
Integrate the backend code with the web server that serves the frontend code so that they can be deployed as a single server. Use Hono for this with `src/server/index.ts` as the starting point. Here's the recommended stack:
|
|
87
|
+
|
|
88
|
+
- Hono for the backend API endpoints (served on `/api` for the same web server)
|
|
89
|
+
- Drizzle as the database ORM
|
|
90
|
+
- [Reshape](/postgres/reshape) to manage database schema migrations
|
|
91
|
+
- [Sync engine](/sync) instead of Supabase real-time. Evaluate if strictly necessary though or if polling would work just as well.
|
|
92
|
+
|
|
93
|
+
## Lovable AI
|
|
94
|
+
|
|
95
|
+
If the project uses Lovable AI and their gateway, switch away from it to using the LLM provider APIs directly (with new secrets for API keys).
|
|
96
|
+
|
|
97
|
+
## Data migration
|
|
98
|
+
|
|
99
|
+
There is no automated way of performing a data migration at the moment.
|
|
@@ -31,6 +31,10 @@ Supabase Auth (GoTrue) has no direct equivalent in Specific. Instead, authentica
|
|
|
31
31
|
|
|
32
32
|
Store user data in the same Postgres database your service already connects to. Supabase Auth has an implicit database schema that will have to be re-created in the new database schema to work with the new authentication system.
|
|
33
33
|
|
|
34
|
+
## Authorization and RLS
|
|
35
|
+
|
|
36
|
+
Replace the usage of RLS (row level security) in Supabase with authorization and access instead enforced by the new backend code.
|
|
37
|
+
|
|
34
38
|
## Storage
|
|
35
39
|
|
|
36
40
|
Supabase Storage is S3-compatible and maps to the object storage available in Specific. See [Storage](/storage) for how to configure this.
|
package/package.json
CHANGED
/package/dist/admin/_next/static/{k4CkHzJBOS7_uNab7bdAE → Fbj_wbG8LdwY4QCJE_Iko}/_buildManifest.js
RENAMED
|
File without changes
|
|
File without changes
|
/package/dist/admin/_next/static/{k4CkHzJBOS7_uNab7bdAE → Fbj_wbG8LdwY4QCJE_Iko}/_ssgManifest.js
RENAMED
|
File without changes
|