@specific.dev/cli 0.1.67 → 0.1.68
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 +2 -2
- package/dist/admin/__next.!KGRlZmF1bHQp.txt +5 -5
- package/dist/admin/__next._full.txt +9 -9
- package/dist/admin/__next._head.txt +1 -1
- package/dist/admin/__next._index.txt +4 -4
- package/dist/admin/__next._tree.txt +2 -2
- package/dist/admin/_next/static/chunks/47a5dab862795de7.js +1 -0
- package/dist/admin/_next/static/chunks/{77284e343252b102.js → 63a5ddab5f6a075d.js} +2 -2
- package/dist/admin/_next/static/chunks/{c7954d71061f1f9b.js → 71775bad64b386a3.js} +2 -2
- package/dist/admin/_next/static/chunks/8cd2655984f0da65.js +1 -0
- package/dist/admin/_next/static/chunks/bac545c7353852cd.css +4 -0
- package/dist/admin/_not-found/__next._full.txt +4 -4
- package/dist/admin/_not-found/__next._head.txt +1 -1
- package/dist/admin/_not-found/__next._index.txt +4 -4
- 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 +2 -2
- package/dist/admin/_not-found/index.html +1 -1
- package/dist/admin/_not-found/index.txt +4 -4
- package/dist/admin/databases/__next.!KGRlZmF1bHQp.databases.__PAGE__.txt +2 -2
- package/dist/admin/databases/__next.!KGRlZmF1bHQp.databases.txt +1 -1
- package/dist/admin/databases/__next.!KGRlZmF1bHQp.txt +5 -5
- package/dist/admin/databases/__next._full.txt +9 -9
- package/dist/admin/databases/__next._head.txt +1 -1
- package/dist/admin/databases/__next._index.txt +4 -4
- package/dist/admin/databases/__next._tree.txt +2 -2
- package/dist/admin/databases/index.html +1 -1
- package/dist/admin/databases/index.txt +9 -9
- package/dist/admin/fullscreen/__next._full.txt +5 -5
- package/dist/admin/fullscreen/__next._head.txt +1 -1
- package/dist/admin/fullscreen/__next._index.txt +4 -4
- package/dist/admin/fullscreen/__next._tree.txt +2 -2
- package/dist/admin/fullscreen/__next.fullscreen.__PAGE__.txt +2 -2
- package/dist/admin/fullscreen/__next.fullscreen.txt +1 -1
- package/dist/admin/fullscreen/databases/__next._full.txt +5 -5
- package/dist/admin/fullscreen/databases/__next._head.txt +1 -1
- package/dist/admin/fullscreen/databases/__next._index.txt +4 -4
- package/dist/admin/fullscreen/databases/__next._tree.txt +2 -2
- package/dist/admin/fullscreen/databases/__next.fullscreen.databases.__PAGE__.txt +2 -2
- 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 +5 -5
- package/dist/admin/fullscreen/index.html +1 -1
- package/dist/admin/fullscreen/index.txt +5 -5
- package/dist/admin/index.html +1 -1
- package/dist/admin/index.txt +9 -9
- package/dist/admin/mail/__next.!KGRlZmF1bHQp.mail.__PAGE__.txt +9 -0
- package/dist/admin/mail/__next.!KGRlZmF1bHQp.mail.txt +4 -0
- package/dist/admin/mail/__next.!KGRlZmF1bHQp.txt +8 -0
- package/dist/admin/mail/__next._full.txt +27 -0
- package/dist/admin/mail/__next._head.txt +6 -0
- package/dist/admin/mail/__next._index.txt +7 -0
- package/dist/admin/mail/__next._tree.txt +5 -0
- package/dist/admin/mail/index.html +1 -0
- package/dist/admin/mail/index.txt +27 -0
- package/dist/admin/workflows/__next.!KGRlZmF1bHQp.txt +5 -5
- package/dist/admin/workflows/__next.!KGRlZmF1bHQp.workflows.__PAGE__.txt +2 -2
- package/dist/admin/workflows/__next.!KGRlZmF1bHQp.workflows.txt +1 -1
- package/dist/admin/workflows/__next._full.txt +9 -9
- package/dist/admin/workflows/__next._head.txt +1 -1
- package/dist/admin/workflows/__next._index.txt +4 -4
- package/dist/admin/workflows/__next._tree.txt +2 -2
- package/dist/admin/workflows/index.html +1 -1
- package/dist/admin/workflows/index.txt +9 -9
- package/dist/cli.js +235 -19
- package/dist/docs/index.md +3 -0
- package/dist/docs/mail.md +66 -0
- package/dist/postinstall.js +1 -1
- package/package.json +5 -1
- package/dist/admin/_next/static/chunks/497f00630c8a5681.js +0 -1
- package/dist/admin/_next/static/chunks/8342a9e3e2851626.css +0 -4
- /package/dist/admin/_next/static/{B_l0oWRS4jgPRx3kI3HDj → w4VP36_YGzWIvqWZUyEgj}/_buildManifest.js +0 -0
- /package/dist/admin/_next/static/{B_l0oWRS4jgPRx3kI3HDj → w4VP36_YGzWIvqWZUyEgj}/_clientMiddlewareManifest.json +0 -0
- /package/dist/admin/_next/static/{B_l0oWRS4jgPRx3kI3HDj → w4VP36_YGzWIvqWZUyEgj}/_ssgManifest.js +0 -0
package/dist/cli.js
CHANGED
|
@@ -184496,7 +184496,7 @@ function trackEvent(event, properties) {
|
|
|
184496
184496
|
event,
|
|
184497
184497
|
properties: {
|
|
184498
184498
|
...properties,
|
|
184499
|
-
cli_version: "0.1.
|
|
184499
|
+
cli_version: "0.1.68",
|
|
184500
184500
|
platform: process.platform,
|
|
184501
184501
|
node_version: process.version,
|
|
184502
184502
|
project_id: getProjectId(),
|
|
@@ -184868,6 +184868,10 @@ var BETA_REGISTRY = [
|
|
|
184868
184868
|
{
|
|
184869
184869
|
name: "temporal",
|
|
184870
184870
|
description: "Managed Temporal workflow engine for durable workflows and background tasks"
|
|
184871
|
+
},
|
|
184872
|
+
{
|
|
184873
|
+
name: "mail",
|
|
184874
|
+
description: "Managed email sending via SMTP for transactional emails"
|
|
184871
184875
|
}
|
|
184872
184876
|
];
|
|
184873
184877
|
|
|
@@ -184938,10 +184942,11 @@ function filterBetaTags(content, enabledBetas) {
|
|
|
184938
184942
|
);
|
|
184939
184943
|
}
|
|
184940
184944
|
function resolveDocContent(path30) {
|
|
184945
|
+
const normalized = path30?.replace(/^\/+|\/+$/g, "") || void 0;
|
|
184941
184946
|
if (_embeddedDocs) {
|
|
184942
|
-
return resolveEmbeddedDoc(
|
|
184947
|
+
return resolveEmbeddedDoc(normalized);
|
|
184943
184948
|
}
|
|
184944
|
-
return resolveFilesystemDoc(
|
|
184949
|
+
return resolveFilesystemDoc(normalized);
|
|
184945
184950
|
}
|
|
184946
184951
|
function resolveEmbeddedDoc(path30) {
|
|
184947
184952
|
if (!path30) {
|
|
@@ -185087,6 +185092,17 @@ function parseReferenceString(str) {
|
|
|
185087
185092
|
};
|
|
185088
185093
|
}
|
|
185089
185094
|
}
|
|
185095
|
+
const mailMatch = str.match(new RegExp(`^mail\\.(${id})\\.(\\w+)$`));
|
|
185096
|
+
if (mailMatch && mailMatch[1] && mailMatch[2]) {
|
|
185097
|
+
const attr = mailMatch[2];
|
|
185098
|
+
if (["host", "port", "user", "password", "from"].includes(attr)) {
|
|
185099
|
+
return {
|
|
185100
|
+
type: "mail",
|
|
185101
|
+
name: mailMatch[1],
|
|
185102
|
+
attribute: attr
|
|
185103
|
+
};
|
|
185104
|
+
}
|
|
185105
|
+
}
|
|
185090
185106
|
const volumeMatch = str.match(new RegExp(`^volume\\.(${id})\\.(\\w+)$`));
|
|
185091
185107
|
if (volumeMatch && volumeMatch[1] && volumeMatch[2]) {
|
|
185092
185108
|
const attr = volumeMatch[2];
|
|
@@ -185131,7 +185147,7 @@ function parseReferenceString(str) {
|
|
|
185131
185147
|
attribute: serviceMatch[2]
|
|
185132
185148
|
};
|
|
185133
185149
|
}
|
|
185134
|
-
const knownPrefixes = ["build", "postgres", "redis", "storage", "temporal", "volume", "config", "secret", "endpoint", "service"];
|
|
185150
|
+
const knownPrefixes = ["build", "postgres", "redis", "storage", "temporal", "mail", "volume", "config", "secret", "endpoint", "service"];
|
|
185135
185151
|
const prefixMatch = str.match(/^(\w+)\./);
|
|
185136
185152
|
if (prefixMatch && knownPrefixes.includes(prefixMatch[1])) {
|
|
185137
185153
|
throw new Error(`Invalid reference "\${${str}}". The prefix "${prefixMatch[1]}" is recognized but the reference format is invalid.`);
|
|
@@ -185481,6 +185497,16 @@ function parseTemporal(data) {
|
|
|
185481
185497
|
}
|
|
185482
185498
|
return result;
|
|
185483
185499
|
}
|
|
185500
|
+
function parseMail(data) {
|
|
185501
|
+
if (!data || typeof data !== "object" || Array.isArray(data)) {
|
|
185502
|
+
return [];
|
|
185503
|
+
}
|
|
185504
|
+
const result = [];
|
|
185505
|
+
for (const [name] of Object.entries(data)) {
|
|
185506
|
+
result.push({ name });
|
|
185507
|
+
}
|
|
185508
|
+
return result;
|
|
185509
|
+
}
|
|
185484
185510
|
function parseConfigDevBlock(dev) {
|
|
185485
185511
|
if (!dev) {
|
|
185486
185512
|
return void 0;
|
|
@@ -185593,6 +185619,7 @@ async function parseConfig(hcl) {
|
|
|
185593
185619
|
redis: parseRedis(json.redis),
|
|
185594
185620
|
storage: parseStorage(json.storage),
|
|
185595
185621
|
temporal: parseTemporal(json.temporal),
|
|
185622
|
+
mail: parseMail(json.mail),
|
|
185596
185623
|
configs: parseConfigs(json.config),
|
|
185597
185624
|
secrets: parseSecrets(json.secret),
|
|
185598
185625
|
environments: parseEnvironments(json.environment)
|
|
@@ -188624,6 +188651,26 @@ function resolveEnvValue(value, resources, secrets, configs, servicePort, servic
|
|
|
188624
188651
|
throw new Error(`Unknown temporal attribute: ${String(value.attribute)}`);
|
|
188625
188652
|
}
|
|
188626
188653
|
}
|
|
188654
|
+
case "mail": {
|
|
188655
|
+
const mail = resources.get(value.name);
|
|
188656
|
+
if (!mail || mail.type !== "mail") {
|
|
188657
|
+
throw new Error(`Mail "${value.name}" not found`);
|
|
188658
|
+
}
|
|
188659
|
+
switch (value.attribute) {
|
|
188660
|
+
case "host":
|
|
188661
|
+
return mail.host;
|
|
188662
|
+
case "port":
|
|
188663
|
+
return String(mail.port);
|
|
188664
|
+
case "user":
|
|
188665
|
+
return "specific";
|
|
188666
|
+
case "password":
|
|
188667
|
+
return "specific";
|
|
188668
|
+
case "from":
|
|
188669
|
+
return "noreply@localhost";
|
|
188670
|
+
default:
|
|
188671
|
+
throw new Error(`Unknown mail attribute: ${String(value.attribute)}`);
|
|
188672
|
+
}
|
|
188673
|
+
}
|
|
188627
188674
|
case "config": {
|
|
188628
188675
|
const configValue = configs.get(value.name);
|
|
188629
188676
|
if (configValue === void 0) {
|
|
@@ -189688,16 +189735,144 @@ function sleep3(ms) {
|
|
|
189688
189735
|
return new Promise((resolve10) => setTimeout(resolve10, ms));
|
|
189689
189736
|
}
|
|
189690
189737
|
|
|
189738
|
+
// src/lib/dev/mail-manager.ts
|
|
189739
|
+
import * as http2 from "http";
|
|
189740
|
+
import * as crypto3 from "crypto";
|
|
189741
|
+
async function startMailServer(mail, smtpPort, apiPort) {
|
|
189742
|
+
const emails = [];
|
|
189743
|
+
const { SMTPServer } = await import("smtp-server");
|
|
189744
|
+
const { simpleParser } = await import("mailparser");
|
|
189745
|
+
const smtpServer = new SMTPServer({
|
|
189746
|
+
authOptional: true,
|
|
189747
|
+
disabledCommands: ["STARTTLS"],
|
|
189748
|
+
onAuth(auth, _session, callback) {
|
|
189749
|
+
if (auth.username === "specific" && auth.password === "specific") {
|
|
189750
|
+
callback(null, { user: auth.username });
|
|
189751
|
+
} else {
|
|
189752
|
+
callback(new Error("Invalid credentials"));
|
|
189753
|
+
}
|
|
189754
|
+
},
|
|
189755
|
+
onData(stream, session, callback) {
|
|
189756
|
+
let rawData = "";
|
|
189757
|
+
stream.on("data", (chunk) => {
|
|
189758
|
+
rawData += chunk.toString();
|
|
189759
|
+
});
|
|
189760
|
+
stream.on("end", async () => {
|
|
189761
|
+
try {
|
|
189762
|
+
const parsed = await simpleParser(rawData);
|
|
189763
|
+
const email = {
|
|
189764
|
+
id: crypto3.randomUUID(),
|
|
189765
|
+
from: parsed.from?.text ?? "",
|
|
189766
|
+
to: Array.isArray(parsed.to) ? parsed.to.map((addr) => addr.text) : parsed.to ? [parsed.to.text] : [],
|
|
189767
|
+
subject: parsed.subject ?? "(no subject)",
|
|
189768
|
+
text: parsed.text,
|
|
189769
|
+
html: typeof parsed.html === "string" ? parsed.html : void 0,
|
|
189770
|
+
date: parsed.date ?? /* @__PURE__ */ new Date()
|
|
189771
|
+
};
|
|
189772
|
+
emails.push(email);
|
|
189773
|
+
} catch {
|
|
189774
|
+
emails.push({
|
|
189775
|
+
id: crypto3.randomUUID(),
|
|
189776
|
+
from: session.envelope.mailFrom ? session.envelope.mailFrom.address : "",
|
|
189777
|
+
to: session.envelope.rcptTo.map((r) => r.address),
|
|
189778
|
+
subject: "(parse error)",
|
|
189779
|
+
text: rawData,
|
|
189780
|
+
date: /* @__PURE__ */ new Date()
|
|
189781
|
+
});
|
|
189782
|
+
}
|
|
189783
|
+
callback();
|
|
189784
|
+
});
|
|
189785
|
+
}
|
|
189786
|
+
});
|
|
189787
|
+
const httpServer = http2.createServer((req, res) => {
|
|
189788
|
+
res.setHeader("Access-Control-Allow-Origin", "*");
|
|
189789
|
+
res.setHeader("Access-Control-Allow-Methods", "GET, DELETE, OPTIONS");
|
|
189790
|
+
res.setHeader("Access-Control-Allow-Headers", "Content-Type");
|
|
189791
|
+
if (req.method === "OPTIONS") {
|
|
189792
|
+
res.writeHead(204);
|
|
189793
|
+
res.end();
|
|
189794
|
+
return;
|
|
189795
|
+
}
|
|
189796
|
+
const url = new URL(req.url ?? "/", `http://localhost:${apiPort}`);
|
|
189797
|
+
if (req.method === "GET" && url.pathname === "/api/emails") {
|
|
189798
|
+
const summaries = [...emails].reverse().map(({ id, from, to, subject, date }) => ({
|
|
189799
|
+
id,
|
|
189800
|
+
from,
|
|
189801
|
+
to,
|
|
189802
|
+
subject,
|
|
189803
|
+
date
|
|
189804
|
+
}));
|
|
189805
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
189806
|
+
res.end(JSON.stringify(summaries));
|
|
189807
|
+
return;
|
|
189808
|
+
}
|
|
189809
|
+
const emailMatch = url.pathname.match(/^\/api\/emails\/(.+)$/);
|
|
189810
|
+
if (req.method === "GET" && emailMatch) {
|
|
189811
|
+
const email = emails.find((e) => e.id === emailMatch[1]);
|
|
189812
|
+
if (email) {
|
|
189813
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
189814
|
+
res.end(JSON.stringify(email));
|
|
189815
|
+
} else {
|
|
189816
|
+
res.writeHead(404, { "Content-Type": "application/json" });
|
|
189817
|
+
res.end(JSON.stringify({ error: "Email not found" }));
|
|
189818
|
+
}
|
|
189819
|
+
return;
|
|
189820
|
+
}
|
|
189821
|
+
if (req.method === "DELETE" && url.pathname === "/api/emails") {
|
|
189822
|
+
emails.length = 0;
|
|
189823
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
189824
|
+
res.end(JSON.stringify({ ok: true }));
|
|
189825
|
+
return;
|
|
189826
|
+
}
|
|
189827
|
+
res.writeHead(404);
|
|
189828
|
+
res.end();
|
|
189829
|
+
});
|
|
189830
|
+
await new Promise((resolve10, reject) => {
|
|
189831
|
+
smtpServer.listen(smtpPort, "127.0.0.1", () => resolve10());
|
|
189832
|
+
smtpServer.on("error", reject);
|
|
189833
|
+
});
|
|
189834
|
+
await new Promise((resolve10, reject) => {
|
|
189835
|
+
httpServer.listen(apiPort, "127.0.0.1", () => resolve10());
|
|
189836
|
+
httpServer.on("error", reject);
|
|
189837
|
+
});
|
|
189838
|
+
const stop = async () => {
|
|
189839
|
+
await new Promise((resolve10) => {
|
|
189840
|
+
smtpServer.close(() => resolve10());
|
|
189841
|
+
});
|
|
189842
|
+
await new Promise((resolve10) => {
|
|
189843
|
+
httpServer.close(() => resolve10());
|
|
189844
|
+
});
|
|
189845
|
+
};
|
|
189846
|
+
const resource = {
|
|
189847
|
+
name: mail.name,
|
|
189848
|
+
type: "mail",
|
|
189849
|
+
port: smtpPort,
|
|
189850
|
+
url: `smtp://127.0.0.1:${smtpPort}`,
|
|
189851
|
+
host: "127.0.0.1",
|
|
189852
|
+
user: "",
|
|
189853
|
+
password: "",
|
|
189854
|
+
dbName: mail.name,
|
|
189855
|
+
stop
|
|
189856
|
+
};
|
|
189857
|
+
const mailServer = {
|
|
189858
|
+
smtpPort,
|
|
189859
|
+
apiPort,
|
|
189860
|
+
getEmails: () => emails,
|
|
189861
|
+
stop
|
|
189862
|
+
};
|
|
189863
|
+
return { resource, mailServer };
|
|
189864
|
+
}
|
|
189865
|
+
|
|
189691
189866
|
// src/lib/dev/drizzle-gateway-manager.ts
|
|
189692
189867
|
import * as net3 from "net";
|
|
189693
189868
|
import * as fs18 from "fs";
|
|
189694
189869
|
import * as path15 from "path";
|
|
189695
189870
|
import { spawn as spawn4 } from "child_process";
|
|
189696
|
-
import { randomUUID } from "crypto";
|
|
189871
|
+
import { randomUUID as randomUUID2 } from "crypto";
|
|
189697
189872
|
function generateStoreJson(postgresInstances) {
|
|
189698
|
-
const storeId =
|
|
189873
|
+
const storeId = randomUUID2();
|
|
189699
189874
|
const slots = postgresInstances.map((pg) => {
|
|
189700
|
-
const slotId =
|
|
189875
|
+
const slotId = randomUUID2();
|
|
189701
189876
|
return [
|
|
189702
189877
|
slotId,
|
|
189703
189878
|
{
|
|
@@ -190215,7 +190390,7 @@ async function stopProcess4(proc) {
|
|
|
190215
190390
|
|
|
190216
190391
|
// src/lib/dev/resource-starter.ts
|
|
190217
190392
|
function findRequiredResources(service) {
|
|
190218
|
-
const required = { postgres: [], redis: [], storage: [], temporal: [] };
|
|
190393
|
+
const required = { postgres: [], redis: [], storage: [], temporal: [], mail: [] };
|
|
190219
190394
|
if (service.env) {
|
|
190220
190395
|
for (const value of Object.values(service.env)) {
|
|
190221
190396
|
if (typeof value !== "object" || value === null) continue;
|
|
@@ -190229,6 +190404,8 @@ function findRequiredResources(service) {
|
|
|
190229
190404
|
required.storage.push(ref.name);
|
|
190230
190405
|
} else if (ref.type === "temporal" && !required.temporal.includes(ref.name)) {
|
|
190231
190406
|
required.temporal.push(ref.name);
|
|
190407
|
+
} else if (ref.type === "mail" && !required.mail.includes(ref.name)) {
|
|
190408
|
+
required.mail.push(ref.name);
|
|
190232
190409
|
}
|
|
190233
190410
|
}
|
|
190234
190411
|
}
|
|
@@ -190250,26 +190427,30 @@ async function startResources(options2) {
|
|
|
190250
190427
|
});
|
|
190251
190428
|
const resources = /* @__PURE__ */ new Map();
|
|
190252
190429
|
const electric = /* @__PURE__ */ new Map();
|
|
190430
|
+
const mailServers = /* @__PURE__ */ new Map();
|
|
190253
190431
|
const startedResources = [];
|
|
190254
190432
|
const startedElectric = [];
|
|
190255
190433
|
let postgresConfigs;
|
|
190256
190434
|
let redisConfigs;
|
|
190257
190435
|
let storageConfigs;
|
|
190258
190436
|
let temporalConfigs;
|
|
190437
|
+
let mailConfigs;
|
|
190259
190438
|
if (selection.mode === "all") {
|
|
190260
190439
|
postgresConfigs = config.postgres;
|
|
190261
190440
|
redisConfigs = config.redis;
|
|
190262
190441
|
storageConfigs = config.storage;
|
|
190263
190442
|
temporalConfigs = config.temporal;
|
|
190443
|
+
mailConfigs = config.mail;
|
|
190264
190444
|
} else {
|
|
190265
190445
|
postgresConfigs = config.postgres.filter((p) => selection.postgres.includes(p.name));
|
|
190266
190446
|
redisConfigs = config.redis.filter((r) => selection.redis.includes(r.name));
|
|
190267
190447
|
storageConfigs = config.storage.filter((s) => selection.storage.includes(s.name));
|
|
190268
190448
|
temporalConfigs = config.temporal.filter((t) => selection.temporal.includes(t.name));
|
|
190449
|
+
mailConfigs = config.mail.filter((m) => selection.mail.includes(m.name));
|
|
190269
190450
|
}
|
|
190270
190451
|
for (const pg of postgresConfigs) {
|
|
190271
190452
|
if (signal?.cancelled) {
|
|
190272
|
-
return { resources, electric, startedResources, startedElectric, cancelled: true };
|
|
190453
|
+
return { resources, electric, mail: mailServers, startedResources, startedElectric, cancelled: true };
|
|
190273
190454
|
}
|
|
190274
190455
|
const port = portAllocator.allocate(`postgres:${pg.name}`);
|
|
190275
190456
|
log(`Starting postgres "${pg.name}" on port ${port}`);
|
|
@@ -190303,7 +190484,7 @@ async function startResources(options2) {
|
|
|
190303
190484
|
}
|
|
190304
190485
|
for (const redis of redisConfigs) {
|
|
190305
190486
|
if (signal?.cancelled) {
|
|
190306
|
-
return { resources, electric, startedResources, startedElectric, cancelled: true };
|
|
190487
|
+
return { resources, electric, mail: mailServers, startedResources, startedElectric, cancelled: true };
|
|
190307
190488
|
}
|
|
190308
190489
|
const port = portAllocator.allocate(`redis:${redis.name}`);
|
|
190309
190490
|
log(`Starting redis "${redis.name}" on port ${port}`);
|
|
@@ -190327,7 +190508,7 @@ async function startResources(options2) {
|
|
|
190327
190508
|
}
|
|
190328
190509
|
for (const storage of storageConfigs) {
|
|
190329
190510
|
if (signal?.cancelled) {
|
|
190330
|
-
return { resources, electric, startedResources, startedElectric, cancelled: true };
|
|
190511
|
+
return { resources, electric, mail: mailServers, startedResources, startedElectric, cancelled: true };
|
|
190331
190512
|
}
|
|
190332
190513
|
const port = portAllocator.allocate(`storage:${storage.name}`);
|
|
190333
190514
|
log(`Starting storage "${storage.name}" on port ${port}`);
|
|
@@ -190353,7 +190534,7 @@ async function startResources(options2) {
|
|
|
190353
190534
|
}
|
|
190354
190535
|
if (temporalConfigs.length > 0) {
|
|
190355
190536
|
if (signal?.cancelled) {
|
|
190356
|
-
return { resources, electric, startedResources, startedElectric, cancelled: true };
|
|
190537
|
+
return { resources, electric, mail: mailServers, startedResources, startedElectric, cancelled: true };
|
|
190357
190538
|
}
|
|
190358
190539
|
const grpcPort = portAllocator.allocate("temporal-grpc");
|
|
190359
190540
|
const uiPort = portAllocator.allocate("temporal-ui");
|
|
@@ -190370,11 +190551,36 @@ async function startResources(options2) {
|
|
|
190370
190551
|
log(`Temporal namespace "${instance.name}" ready`);
|
|
190371
190552
|
}
|
|
190372
190553
|
}
|
|
190554
|
+
for (const mail of mailConfigs) {
|
|
190555
|
+
if (signal?.cancelled) {
|
|
190556
|
+
return { resources, electric, mail: mailServers, startedResources, startedElectric, cancelled: true };
|
|
190557
|
+
}
|
|
190558
|
+
const smtpPort = portAllocator.allocate(`mail-smtp:${mail.name}`);
|
|
190559
|
+
const mailApiPort = portAllocator.allocate(`mail-api:${mail.name}`);
|
|
190560
|
+
log(`Starting mail "${mail.name}" on SMTP port ${smtpPort} (API: ${mailApiPort})`);
|
|
190561
|
+
callbacks.onResourceStarting?.(mail.name, "mail");
|
|
190562
|
+
const { resource, mailServer } = await startMailServer(mail, smtpPort, mailApiPort);
|
|
190563
|
+
resources.set(mail.name, resource);
|
|
190564
|
+
startedResources.push(resource);
|
|
190565
|
+
mailServers.set(mail.name, mailServer);
|
|
190566
|
+
callbacks.onResourceReady?.(mail.name, resource);
|
|
190567
|
+
log(`Mail "${mail.name}" ready`);
|
|
190568
|
+
await stateManager.registerDatabase(mail.name, {
|
|
190569
|
+
engine: "mail",
|
|
190570
|
+
port: smtpPort,
|
|
190571
|
+
host: "127.0.0.1",
|
|
190572
|
+
user: "",
|
|
190573
|
+
password: "",
|
|
190574
|
+
dbName: mail.name,
|
|
190575
|
+
url: `smtp://127.0.0.1:${smtpPort}`,
|
|
190576
|
+
mailApiPort
|
|
190577
|
+
});
|
|
190578
|
+
}
|
|
190373
190579
|
if (shouldStartElectric) {
|
|
190374
190580
|
const syncDatabases = detectSyncDatabases(config);
|
|
190375
190581
|
for (const pgName of syncDatabases) {
|
|
190376
190582
|
if (signal?.cancelled) {
|
|
190377
|
-
return { resources, electric, startedResources, startedElectric, cancelled: true };
|
|
190583
|
+
return { resources, electric, mail: mailServers, startedResources, startedElectric, cancelled: true };
|
|
190378
190584
|
}
|
|
190379
190585
|
const pg = resources.get(pgName);
|
|
190380
190586
|
if (!pg || pg.type !== "postgres") continue;
|
|
@@ -190394,7 +190600,7 @@ async function startResources(options2) {
|
|
|
190394
190600
|
log(`Electric sync for "${pgName}" ready at ${electricInstance.url}`);
|
|
190395
190601
|
}
|
|
190396
190602
|
}
|
|
190397
|
-
return { resources, electric, startedResources, startedElectric, cancelled: false };
|
|
190603
|
+
return { resources, electric, mail: mailServers, startedResources, startedElectric, cancelled: false };
|
|
190398
190604
|
}
|
|
190399
190605
|
|
|
190400
190606
|
// src/lib/dev/config-watcher.ts
|
|
@@ -191051,12 +191257,14 @@ function SecretInput({ secretName, onSubmit, onCancel }) {
|
|
|
191051
191257
|
// src/lib/ui/ConfigInput.tsx
|
|
191052
191258
|
import React5, { useState as useState4 } from "react";
|
|
191053
191259
|
import { Box as Box5, Text as Text5, useInput as useInput3 } from "ink";
|
|
191054
|
-
function ConfigInput({ configName, onSubmit, onCancel }) {
|
|
191260
|
+
function ConfigInput({ configName, defaultValue, onSubmit, onCancel }) {
|
|
191055
191261
|
const [value, setValue] = useState4("");
|
|
191056
191262
|
useInput3((input, key) => {
|
|
191057
191263
|
if (key.return) {
|
|
191058
191264
|
if (value.trim() !== "") {
|
|
191059
191265
|
onSubmit(value);
|
|
191266
|
+
} else if (defaultValue !== void 0) {
|
|
191267
|
+
onSubmit(defaultValue);
|
|
191060
191268
|
}
|
|
191061
191269
|
} else if (key.escape) {
|
|
191062
191270
|
onCancel();
|
|
@@ -191066,7 +191274,7 @@ function ConfigInput({ configName, onSubmit, onCancel }) {
|
|
|
191066
191274
|
setValue((prev) => prev + input);
|
|
191067
191275
|
}
|
|
191068
191276
|
});
|
|
191069
|
-
return /* @__PURE__ */ React5.createElement(Box5, { flexDirection: "column", marginTop: 1 }, /* @__PURE__ */ React5.createElement(Text5, null, "Enter value for config ", /* @__PURE__ */ React5.createElement(Text5, { color: "cyan" }, configName), ":"), /* @__PURE__ */ React5.createElement(Box5, null, /* @__PURE__ */ React5.createElement(Text5, { color: "cyan" }, "> "), /* @__PURE__ */ React5.createElement(Text5, null, value), /* @__PURE__ */ React5.createElement(Text5, { color: "gray" }, "|")), /* @__PURE__ */ React5.createElement(Text5, { dimColor: true }, "(Press Enter to save, Esc to cancel)"));
|
|
191277
|
+
return /* @__PURE__ */ React5.createElement(Box5, { flexDirection: "column", marginTop: 1 }, /* @__PURE__ */ React5.createElement(Text5, null, "Enter value for config ", /* @__PURE__ */ React5.createElement(Text5, { color: "cyan" }, configName), defaultValue !== void 0 && /* @__PURE__ */ React5.createElement(Text5, { dimColor: true }, " (default: ", defaultValue, ")"), ":"), /* @__PURE__ */ React5.createElement(Box5, null, /* @__PURE__ */ React5.createElement(Text5, { color: "cyan" }, "> "), /* @__PURE__ */ React5.createElement(Text5, null, value), /* @__PURE__ */ React5.createElement(Text5, { color: "gray" }, "|")), /* @__PURE__ */ React5.createElement(Text5, { dimColor: true }, defaultValue !== void 0 ? "(Press Enter to accept default, type to override, Esc to cancel)" : "(Press Enter to save, Esc to cancel)"));
|
|
191070
191278
|
}
|
|
191071
191279
|
|
|
191072
191280
|
// src/commands/dev.tsx
|
|
@@ -191472,6 +191680,7 @@ function DevUI({ instanceKey, tunnelEnabled }) {
|
|
|
191472
191680
|
const resourceStatus = /* @__PURE__ */ new Map();
|
|
191473
191681
|
const syncDatabases = detectSyncDatabases(config2);
|
|
191474
191682
|
let resources2;
|
|
191683
|
+
let mailServers = /* @__PURE__ */ new Map();
|
|
191475
191684
|
try {
|
|
191476
191685
|
const result = await startResources({
|
|
191477
191686
|
config: config2,
|
|
@@ -191519,6 +191728,7 @@ function DevUI({ instanceKey, tunnelEnabled }) {
|
|
|
191519
191728
|
});
|
|
191520
191729
|
if (result.cancelled) return;
|
|
191521
191730
|
resources2 = result.resources;
|
|
191731
|
+
mailServers = result.mail;
|
|
191522
191732
|
startedResources.push(...result.startedResources);
|
|
191523
191733
|
} catch (err) {
|
|
191524
191734
|
const errorMsg = `Failed to start resources: ${err instanceof Error ? err.message : String(err)}`;
|
|
@@ -191913,6 +192123,9 @@ Add them to the config block in specific.local`);
|
|
|
191913
192123
|
}
|
|
191914
192124
|
const projectId = hasProjectId() ? readProjectId() : void 0;
|
|
191915
192125
|
const hasTemporal = config2.temporal.length > 0;
|
|
192126
|
+
const hasMail = config2.mail.length > 0;
|
|
192127
|
+
const firstMailServer = mailServers.size > 0 ? [...mailServers.values()][0] : void 0;
|
|
192128
|
+
const mailApiUrl = firstMailServer ? `http://127.0.0.1:${firstMailServer.apiPort}` : void 0;
|
|
191916
192129
|
const getState = () => ({
|
|
191917
192130
|
status: "running",
|
|
191918
192131
|
services: config2.services.filter((svc) => runningServicePorts.has(svc.name) || svc.serve).map((svc) => ({
|
|
@@ -191929,7 +192142,9 @@ Add them to the config block in specific.local`);
|
|
|
191929
192142
|
syncEnabled: r.type === "postgres" && syncDatabases.has(name)
|
|
191930
192143
|
})),
|
|
191931
192144
|
projectId,
|
|
191932
|
-
hasTemporal
|
|
192145
|
+
hasTemporal,
|
|
192146
|
+
hasMail,
|
|
192147
|
+
mailApiUrl
|
|
191933
192148
|
});
|
|
191934
192149
|
const adminServer = await startAdminServer(getState);
|
|
191935
192150
|
adminServerRef.current = adminServer;
|
|
@@ -193563,6 +193778,7 @@ ${errorMsg}`
|
|
|
193563
193778
|
{
|
|
193564
193779
|
key: currentConfig,
|
|
193565
193780
|
configName: currentConfig,
|
|
193781
|
+
defaultValue: config.configs?.find((c) => c.name === currentConfig)?.default,
|
|
193566
193782
|
onSubmit: handleConfigSubmit,
|
|
193567
193783
|
onCancel: handleConfigCancel
|
|
193568
193784
|
}
|
|
@@ -194385,7 +194601,7 @@ function compareVersions(a, b) {
|
|
|
194385
194601
|
return 0;
|
|
194386
194602
|
}
|
|
194387
194603
|
async function checkForUpdate() {
|
|
194388
|
-
const currentVersion = "0.1.
|
|
194604
|
+
const currentVersion = "0.1.68";
|
|
194389
194605
|
const response = await fetch(`${BINARIES_BASE_URL}/latest`);
|
|
194390
194606
|
if (!response.ok) {
|
|
194391
194607
|
throw new Error(`Failed to check for updates: HTTP ${response.status}`);
|
|
@@ -194583,7 +194799,7 @@ function updateCommand() {
|
|
|
194583
194799
|
var program = new Command();
|
|
194584
194800
|
var env = "production";
|
|
194585
194801
|
var envLabel = env !== "production" ? `[${env.toUpperCase()}] ` : "";
|
|
194586
|
-
program.name("specific").description(`${envLabel}Infrastructure-as-code for coding agents`).version("0.1.
|
|
194802
|
+
program.name("specific").description(`${envLabel}Infrastructure-as-code for coding agents`).version("0.1.68").enablePositionalOptions();
|
|
194587
194803
|
program.command("init").description("Initialize project for use with a coding agent").option("--agent <name...>", "Agents to configure (cursor, claude, codex, other)").action((options2) => initCommand(options2));
|
|
194588
194804
|
program.command("docs [topic]").description("Fetch LLM-optimized documentation").action(docsCommand);
|
|
194589
194805
|
program.command("check").description("Validate specific.hcl configuration").action(checkCommand);
|
package/dist/docs/index.md
CHANGED
|
@@ -27,6 +27,9 @@ A full development environment can be started with `specific dev`. To deploy any
|
|
|
27
27
|
<!-- beta:temporal -->
|
|
28
28
|
- [Temporal](/temporal): managed durable workflow engine for background tasks, AI agents, cron jobs and more.
|
|
29
29
|
<!-- /beta:temporal -->
|
|
30
|
+
<!-- beta:mail -->
|
|
31
|
+
- [Mail](/mail): managed email sending via SMTP for transactional emails.
|
|
32
|
+
<!-- /beta:mail -->
|
|
30
33
|
|
|
31
34
|
## Common integrations
|
|
32
35
|
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
# Mail
|
|
2
|
+
|
|
3
|
+
Managed email sending via SMTP.
|
|
4
|
+
|
|
5
|
+
```hcl
|
|
6
|
+
mail "notifications" {}
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
Reference mail attributes in env blocks:
|
|
10
|
+
|
|
11
|
+
```hcl
|
|
12
|
+
service "api" {
|
|
13
|
+
build = build.api
|
|
14
|
+
command = "./api"
|
|
15
|
+
|
|
16
|
+
endpoint {
|
|
17
|
+
public = true
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
env = {
|
|
21
|
+
SMTP_HOST = mail.notifications.host
|
|
22
|
+
SMTP_PORT = mail.notifications.port
|
|
23
|
+
SMTP_USER = mail.notifications.user
|
|
24
|
+
SMTP_PASSWORD = mail.notifications.password
|
|
25
|
+
MAIL_FROM = mail.notifications.from
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
mail "notifications" {}
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
## Available mail attributes
|
|
33
|
+
|
|
34
|
+
- `host` - SMTP server hostname
|
|
35
|
+
- `port` - SMTP server port
|
|
36
|
+
- `user` - SMTP authentication username
|
|
37
|
+
- `password` - SMTP authentication password
|
|
38
|
+
- `from` - Default sender address
|
|
39
|
+
|
|
40
|
+
## Development
|
|
41
|
+
|
|
42
|
+
In development, emails are captured by a local SMTP server and displayed in the admin dashboard under the Mail tab. No emails are actually sent.
|
|
43
|
+
|
|
44
|
+
## Example using Nodemailer (JavaScript)
|
|
45
|
+
|
|
46
|
+
```javascript
|
|
47
|
+
import nodemailer from "nodemailer";
|
|
48
|
+
|
|
49
|
+
const transporter = nodemailer.createTransport({
|
|
50
|
+
host: process.env.SMTP_HOST,
|
|
51
|
+
port: Number(process.env.SMTP_PORT),
|
|
52
|
+
secure: false,
|
|
53
|
+
auth: {
|
|
54
|
+
user: process.env.SMTP_USER,
|
|
55
|
+
pass: process.env.SMTP_PASSWORD,
|
|
56
|
+
},
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
await transporter.sendMail({
|
|
60
|
+
from: process.env.MAIL_FROM,
|
|
61
|
+
to: "user@example.com",
|
|
62
|
+
subject: "Welcome!",
|
|
63
|
+
text: "Thanks for signing up.",
|
|
64
|
+
html: "<p>Thanks for signing up.</p>",
|
|
65
|
+
});
|
|
66
|
+
```
|
package/dist/postinstall.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@specific.dev/cli",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.68",
|
|
4
4
|
"description": "CLI for Specific infrastructure-as-code",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/cli.js",
|
|
@@ -44,12 +44,14 @@
|
|
|
44
44
|
"http-proxy": "^1.18.1",
|
|
45
45
|
"ink": "^6.5.1",
|
|
46
46
|
"ink-spinner": "^5.0.0",
|
|
47
|
+
"mailparser": "^3.9.3",
|
|
47
48
|
"node-forge": "^1.3.1",
|
|
48
49
|
"open": "^11.0.0",
|
|
49
50
|
"posthog-node": "^5.24.1",
|
|
50
51
|
"random-word-slugs": "^0.1.7",
|
|
51
52
|
"react": "^19.0.0",
|
|
52
53
|
"s3rver": "^3.7.1",
|
|
54
|
+
"smtp-server": "^3.18.1",
|
|
53
55
|
"tar-vern": "^1.3.0"
|
|
54
56
|
},
|
|
55
57
|
"devDependencies": {
|
|
@@ -57,9 +59,11 @@
|
|
|
57
59
|
"@specific/tunnel-client": "file:../tunnel/client",
|
|
58
60
|
"@types/babel__code-frame": "^7.27.0",
|
|
59
61
|
"@types/http-proxy": "^1.17.17",
|
|
62
|
+
"@types/mailparser": "^3.4.6",
|
|
60
63
|
"@types/node": "^25.0.1",
|
|
61
64
|
"@types/node-forge": "^1.3.11",
|
|
62
65
|
"@types/react": "^19.2.7",
|
|
66
|
+
"@types/smtp-server": "^3.5.12",
|
|
63
67
|
"esbuild": "^0.24.0",
|
|
64
68
|
"tsx": "^4.21.0",
|
|
65
69
|
"typescript": "^5.9.3"
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
(globalThis.TURBOPACK||(globalThis.TURBOPACK=[])).push(["object"==typeof document?document.currentScript:void 0,12283,e=>{"use strict";var t=e.i(90795),r=e.i(59369);let a=(0,r.createContext)(void 0);function s({children:e}){let[s,n]=(0,r.useState)(null),[l,o]=(0,r.useState)(null),[i,c]=(0,r.useState)(!1),u=(0,r.useMemo)(()=>(function(){let e=window.location.hostname,t=".local.spcf.app";if("local.spcf.app"===e)return"default";if(e.endsWith(t)){let r=e.slice(0,-t.length);if(r&&!r.includes("."))return r}return null})(),[]),m=(0,r.useMemo)(()=>u?"default"===u?"https://__drizzle_gateway.local.spcf.app":`https://__drizzle_gateway.${u}.local.spcf.app`:null,[u]);(0,r.useEffect)(()=>{async function e(){try{let e=await fetch("/api/state");if(!e.ok)throw Error("Failed to fetch state");let t=await e.json();n(t),c(!0),o(null)}catch(e){o(e instanceof Error?e.message:"Unknown error"),c(!1)}}e();let t=setInterval(e,2e3);return()=>clearInterval(t)},[]);let d=s?.resources.some(e=>"postgres"===e.type)??!1,h=s?.hasTemporal??!1,p=(0,r.useMemo)(()=>u?"default"===u?"https://__temporal.local.spcf.app":`https://__temporal.${u}.local.spcf.app`:null,[u]),f=s?.projectId??null,v=(0,r.useMemo)(()=>({state:s,error:l,connected:i,instanceKey:u,hasDatabases:d,drizzleGatewayUrl:m,hasTemporal:h,temporalUiUrl:p,projectId:f}),[s,l,i,u,d,m,h,p,f]);return(0,t.jsx)(a.Provider,{value:v,children:e})}function n(){let e=(0,r.useContext)(a);if(void 0===e)throw Error("useDevState must be used within a DevStateProvider");return e}e.s(["DevStateProvider",()=>s,"useDevState",()=>n],12283)},70932,e=>{"use strict";var t=e.i(59369),r=(e,t,r,a,s,n,l,o)=>{let i=document.documentElement,c=["light","dark"];function u(t){var r;(Array.isArray(e)?e:[e]).forEach(e=>{let r="class"===e,a=r&&n?s.map(e=>n[e]||e):s;r?(i.classList.remove(...a),i.classList.add(n&&n[t]?n[t]:t)):i.setAttribute(e,t)}),r=t,o&&c.includes(r)&&(i.style.colorScheme=r)}if(a)u(a);else try{let e=localStorage.getItem(t)||r,a=l&&"system"===e?window.matchMedia("(prefers-color-scheme: dark)").matches?"dark":"light":e;u(a)}catch(e){}},a=["light","dark"],s="(prefers-color-scheme: dark)",n="u"<typeof window,l=t.createContext(void 0),o={setTheme:e=>{},themes:[]},i=()=>{var e;return null!=(e=t.useContext(l))?e:o},c=e=>t.useContext(l)?t.createElement(t.Fragment,null,e.children):t.createElement(m,{...e}),u=["light","dark"],m=({forcedTheme:e,disableTransitionOnChange:r=!1,enableSystem:n=!0,enableColorScheme:o=!0,storageKey:i="theme",themes:c=u,defaultTheme:m=n?"system":"light",attribute:v="data-theme",value:y,children:g,nonce:w,scriptProps:b})=>{let[S,T]=t.useState(()=>h(i,m)),[E,C]=t.useState(()=>"system"===S?f():S),k=y?Object.values(y):c,P=t.useCallback(e=>{let t=e;if(!t)return;"system"===e&&n&&(t=f());let s=y?y[t]:t,l=r?p(w):null,i=document.documentElement,c=e=>{"class"===e?(i.classList.remove(...k),s&&i.classList.add(s)):e.startsWith("data-")&&(s?i.setAttribute(e,s):i.removeAttribute(e))};if(Array.isArray(v)?v.forEach(c):c(v),o){let e=a.includes(m)?m:null,r=a.includes(t)?t:e;i.style.colorScheme=r}null==l||l()},[w]),_=t.useCallback(e=>{let t="function"==typeof e?e(S):e;T(t);try{localStorage.setItem(i,t)}catch(e){}},[S]),A=t.useCallback(t=>{C(f(t)),"system"===S&&n&&!e&&P("system")},[S,e]);t.useEffect(()=>{let e=window.matchMedia(s);return e.addListener(A),A(e),()=>e.removeListener(A)},[A]),t.useEffect(()=>{let e=e=>{e.key===i&&(e.newValue?T(e.newValue):_(m))};return window.addEventListener("storage",e),()=>window.removeEventListener("storage",e)},[_]),t.useEffect(()=>{P(null!=e?e:S)},[e,S]);let L=t.useMemo(()=>({theme:S,setTheme:_,forcedTheme:e,resolvedTheme:"system"===S?E:S,themes:n?[...c,"system"]:c,systemTheme:n?E:void 0}),[S,_,e,E,n,c]);return t.createElement(l.Provider,{value:L},t.createElement(d,{forcedTheme:e,storageKey:i,attribute:v,enableSystem:n,enableColorScheme:o,defaultTheme:m,value:y,themes:c,nonce:w,scriptProps:b}),g)},d=t.memo(({forcedTheme:e,storageKey:a,attribute:s,enableSystem:n,enableColorScheme:l,defaultTheme:o,value:i,themes:c,nonce:u,scriptProps:m})=>{let d=JSON.stringify([s,a,o,e,c,i,n,l]).slice(1,-1);return t.createElement("script",{...m,suppressHydrationWarning:!0,nonce:"u"<typeof window?u:"",dangerouslySetInnerHTML:{__html:`(${r.toString()})(${d})`}})}),h=(e,t)=>{let r;if(!n){try{r=localStorage.getItem(e)||void 0}catch(e){}return r||t}},p=e=>{let t=document.createElement("style");return e&&t.setAttribute("nonce",e),t.appendChild(document.createTextNode("*,*::before,*::after{-webkit-transition:none!important;-moz-transition:none!important;-o-transition:none!important;-ms-transition:none!important;transition:none!important}")),document.head.appendChild(t),()=>{window.getComputedStyle(document.body),setTimeout(()=>{document.head.removeChild(t)},1)}},f=e=>(e||(e=window.matchMedia(s)),e.matches?"dark":"light");e.s(["ThemeProvider",()=>c,"useTheme",()=>i])},49311,e=>{"use strict";var t=e.i(90795),r=e.i(70932);function a({children:e,...a}){return(0,t.jsx)(r.ThemeProvider,{...a,children:e})}e.s(["ThemeProvider",()=>a])}]);
|