@specific.dev/cli 0.1.183 → 0.1.185
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/crons/__next.!KGRlZmF1bHQp.crons.__PAGE__.txt +1 -1
- package/dist/admin/crons/__next.!KGRlZmF1bHQp.crons.txt +1 -1
- package/dist/admin/crons/__next.!KGRlZmF1bHQp.txt +1 -1
- package/dist/admin/crons/__next._full.txt +1 -1
- package/dist/admin/crons/__next._head.txt +1 -1
- package/dist/admin/crons/__next._index.txt +1 -1
- package/dist/admin/crons/__next._tree.txt +1 -1
- package/dist/admin/crons/index.html +1 -1
- package/dist/admin/crons/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/services/__next.!KGRlZmF1bHQp.services.__PAGE__.txt +1 -1
- package/dist/admin/services/__next.!KGRlZmF1bHQp.services.txt +1 -1
- package/dist/admin/services/__next.!KGRlZmF1bHQp.txt +1 -1
- package/dist/admin/services/__next._full.txt +1 -1
- package/dist/admin/services/__next._head.txt +1 -1
- package/dist/admin/services/__next._index.txt +1 -1
- package/dist/admin/services/__next._tree.txt +1 -1
- package/dist/admin/services/index.html +1 -1
- package/dist/admin/services/index.txt +1 -1
- package/dist/admin/storage/__next.!KGRlZmF1bHQp.storage.__PAGE__.txt +1 -1
- package/dist/admin/storage/__next.!KGRlZmF1bHQp.storage.txt +1 -1
- package/dist/admin/storage/__next.!KGRlZmF1bHQp.txt +1 -1
- package/dist/admin/storage/__next._full.txt +1 -1
- package/dist/admin/storage/__next._head.txt +1 -1
- package/dist/admin/storage/__next._index.txt +1 -1
- package/dist/admin/storage/__next._tree.txt +1 -1
- package/dist/admin/storage/index.html +1 -1
- package/dist/admin/storage/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 +179 -34
- package/dist/docs/index.md +1 -1
- package/dist/docs/postgres.md +1 -0
- package/dist/docs/previews.md +7 -0
- package/dist/docs/secrets-config.md +6 -0
- package/package.json +1 -1
- /package/dist/admin/_next/static/{NNVAA5krd_olwtUDgk5Bo → gLcFBPDPi0InHgZzN22am}/_buildManifest.js +0 -0
- /package/dist/admin/_next/static/{NNVAA5krd_olwtUDgk5Bo → gLcFBPDPi0InHgZzN22am}/_clientMiddlewareManifest.json +0 -0
- /package/dist/admin/_next/static/{NNVAA5krd_olwtUDgk5Bo → gLcFBPDPi0InHgZzN22am}/_ssgManifest.js +0 -0
package/dist/cli.js
CHANGED
|
@@ -368225,6 +368225,14 @@ async function createTarball(config, projectDir) {
|
|
|
368225
368225
|
return await createProjectTarball(projectDir, widestContext);
|
|
368226
368226
|
}
|
|
368227
368227
|
var import_hcl2_json_parser = __toESM2(require_dist(), 1);
|
|
368228
|
+
function extractEnvReferences(value) {
|
|
368229
|
+
if (typeof value === "string")
|
|
368230
|
+
return [];
|
|
368231
|
+
if (value.type === "interpolated") {
|
|
368232
|
+
return value.parts.filter((p) => p.type === "ref").map((p) => p.ref);
|
|
368233
|
+
}
|
|
368234
|
+
return [value];
|
|
368235
|
+
}
|
|
368228
368236
|
var { parseToObject } = import_hcl2_json_parser.default;
|
|
368229
368237
|
function parseReference(value) {
|
|
368230
368238
|
if (typeof value !== "string") {
|
|
@@ -368996,19 +369004,11 @@ async function parseConfig(hcl) {
|
|
|
368996
369004
|
environments: parseEnvironments(json.environment)
|
|
368997
369005
|
};
|
|
368998
369006
|
}
|
|
368999
|
-
function envValueReferences(value) {
|
|
369000
|
-
if (typeof value === "string")
|
|
369001
|
-
return [];
|
|
369002
|
-
if (value.type === "interpolated") {
|
|
369003
|
-
return value.parts.filter((p) => p.type === "ref").map((p) => p.ref);
|
|
369004
|
-
}
|
|
369005
|
-
return [value];
|
|
369006
|
-
}
|
|
369007
369007
|
function hasPortReference(env2) {
|
|
369008
369008
|
if (!env2)
|
|
369009
369009
|
return false;
|
|
369010
369010
|
for (const value of Object.values(env2)) {
|
|
369011
|
-
for (const ref of
|
|
369011
|
+
for (const ref of extractEnvReferences(value)) {
|
|
369012
369012
|
if (ref.type === "port")
|
|
369013
369013
|
return true;
|
|
369014
369014
|
}
|
|
@@ -369025,7 +369025,7 @@ function validateEndpointReferences(config) {
|
|
|
369025
369025
|
if (!service.env)
|
|
369026
369026
|
continue;
|
|
369027
369027
|
for (const [key, value] of Object.entries(service.env)) {
|
|
369028
|
-
const refs =
|
|
369028
|
+
const refs = extractEnvReferences(value);
|
|
369029
369029
|
for (const ref of refs) {
|
|
369030
369030
|
if (service.endpoints.length > 1 && ref.type === "port") {
|
|
369031
369031
|
errors.push({
|
|
@@ -369104,6 +369104,82 @@ function validateEndpointReferences(config) {
|
|
|
369104
369104
|
}
|
|
369105
369105
|
return errors;
|
|
369106
369106
|
}
|
|
369107
|
+
var UnknownServiceError = class extends Error {
|
|
369108
|
+
unknownNames;
|
|
369109
|
+
availableServices;
|
|
369110
|
+
cronNames;
|
|
369111
|
+
constructor(unknownNames, availableServices, cronNames) {
|
|
369112
|
+
const plural = unknownNames.length > 1 ? "services" : "service";
|
|
369113
|
+
const names = unknownNames.map((n) => `"${n}"`).join(", ");
|
|
369114
|
+
const available = availableServices.length > 0 ? ` Available services: ${availableServices.join(", ")}` : " No services are defined in specific.hcl.";
|
|
369115
|
+
super(`Unknown ${plural} ${names}.${available}`);
|
|
369116
|
+
this.unknownNames = unknownNames;
|
|
369117
|
+
this.availableServices = availableServices;
|
|
369118
|
+
this.cronNames = cronNames;
|
|
369119
|
+
this.name = "UnknownServiceError";
|
|
369120
|
+
}
|
|
369121
|
+
};
|
|
369122
|
+
function resolveServiceClosure(config, roots, options2 = {}) {
|
|
369123
|
+
const devMode = options2.devMode ?? true;
|
|
369124
|
+
const byName = new Map(config.services.map((s) => [s.name, s]));
|
|
369125
|
+
const dedupedRoots = [...new Set(roots)];
|
|
369126
|
+
const unknown = dedupedRoots.filter((name) => !byName.has(name));
|
|
369127
|
+
if (unknown.length > 0) {
|
|
369128
|
+
throw new UnknownServiceError(unknown, [...byName.keys()], config.crons.map((c) => c.name));
|
|
369129
|
+
}
|
|
369130
|
+
const visited = new Set(dedupedRoots);
|
|
369131
|
+
const queue = [...dedupedRoots];
|
|
369132
|
+
const postgres = /* @__PURE__ */ new Set();
|
|
369133
|
+
const redis = /* @__PURE__ */ new Set();
|
|
369134
|
+
const storage = /* @__PURE__ */ new Set();
|
|
369135
|
+
const temporal = /* @__PURE__ */ new Set();
|
|
369136
|
+
const mail = /* @__PURE__ */ new Set();
|
|
369137
|
+
const syncPostgres = /* @__PURE__ */ new Set();
|
|
369138
|
+
const effectiveEnv = (service) => devMode ? { ...service.env, ...service.dev?.env } : { ...service.env };
|
|
369139
|
+
while (queue.length > 0) {
|
|
369140
|
+
const service = byName.get(queue.shift());
|
|
369141
|
+
for (const value of Object.values(effectiveEnv(service))) {
|
|
369142
|
+
for (const ref of extractEnvReferences(value)) {
|
|
369143
|
+
switch (ref.type) {
|
|
369144
|
+
case "service":
|
|
369145
|
+
if (byName.has(ref.serviceName) && !visited.has(ref.serviceName)) {
|
|
369146
|
+
visited.add(ref.serviceName);
|
|
369147
|
+
queue.push(ref.serviceName);
|
|
369148
|
+
}
|
|
369149
|
+
break;
|
|
369150
|
+
case "postgres":
|
|
369151
|
+
postgres.add(ref.name);
|
|
369152
|
+
if (ref.attribute === "sync.url" || ref.attribute === "sync.secret") {
|
|
369153
|
+
syncPostgres.add(ref.name);
|
|
369154
|
+
}
|
|
369155
|
+
break;
|
|
369156
|
+
case "redis":
|
|
369157
|
+
redis.add(ref.name);
|
|
369158
|
+
break;
|
|
369159
|
+
case "storage":
|
|
369160
|
+
storage.add(ref.name);
|
|
369161
|
+
break;
|
|
369162
|
+
case "temporal":
|
|
369163
|
+
temporal.add(ref.name);
|
|
369164
|
+
break;
|
|
369165
|
+
case "mail":
|
|
369166
|
+
mail.add(ref.name);
|
|
369167
|
+
break;
|
|
369168
|
+
}
|
|
369169
|
+
}
|
|
369170
|
+
}
|
|
369171
|
+
}
|
|
369172
|
+
const names = (items, set) => items.filter((item) => set.has(item.name)).map((item) => item.name);
|
|
369173
|
+
return {
|
|
369174
|
+
services: names(config.services, visited),
|
|
369175
|
+
postgres: names(config.postgres, postgres),
|
|
369176
|
+
redis: names(config.redis, redis),
|
|
369177
|
+
storage: names(config.storage, storage),
|
|
369178
|
+
temporal: names(config.temporal, temporal),
|
|
369179
|
+
mail: names(config.mail, mail),
|
|
369180
|
+
syncPostgres: names(config.postgres, syncPostgres)
|
|
369181
|
+
};
|
|
369182
|
+
}
|
|
369107
369183
|
function parseHclErrors(rawError) {
|
|
369108
369184
|
const raw = rawError.trim();
|
|
369109
369185
|
if (!raw.startsWith("parse config: [")) {
|
|
@@ -372588,7 +372664,7 @@ async function startResources(options2) {
|
|
|
372588
372664
|
});
|
|
372589
372665
|
}
|
|
372590
372666
|
if (shouldStartElectric) {
|
|
372591
|
-
const syncDatabases = detectSyncDatabases(config);
|
|
372667
|
+
const syncDatabases = options2.syncDatabases ?? detectSyncDatabases(config);
|
|
372592
372668
|
for (const pgName of syncDatabases) {
|
|
372593
372669
|
if (signal?.cancelled) {
|
|
372594
372670
|
return { resources, electric, mail: mailServers, startedResources, startedElectric, cancelled: true };
|
|
@@ -373087,9 +373163,11 @@ var DevEnvironment = class extends TypedEventEmitter {
|
|
|
373087
373163
|
tunnelEnabled;
|
|
373088
373164
|
preSuppliedSecrets;
|
|
373089
373165
|
preSuppliedConfigs;
|
|
373166
|
+
servicesFilter;
|
|
373090
373167
|
// Status
|
|
373091
373168
|
_status = "idle";
|
|
373092
373169
|
_config;
|
|
373170
|
+
_selection;
|
|
373093
373171
|
// Guards
|
|
373094
373172
|
started = false;
|
|
373095
373173
|
shuttingDown = false;
|
|
@@ -373133,6 +373211,7 @@ var DevEnvironment = class extends TypedEventEmitter {
|
|
|
373133
373211
|
this.tunnelEnabled = options2?.tunnel ?? false;
|
|
373134
373212
|
this.preSuppliedSecrets = options2?.secrets ?? {};
|
|
373135
373213
|
this.preSuppliedConfigs = options2?.configs ?? {};
|
|
373214
|
+
this.servicesFilter = options2?.services ?? [];
|
|
373136
373215
|
}
|
|
373137
373216
|
// ── Public getters ─────────────────────────────────────────────────────────
|
|
373138
373217
|
get status() {
|
|
@@ -373141,6 +373220,10 @@ var DevEnvironment = class extends TypedEventEmitter {
|
|
|
373141
373220
|
get config() {
|
|
373142
373221
|
return this._config;
|
|
373143
373222
|
}
|
|
373223
|
+
/** The resolved service selection when a services filter is active. */
|
|
373224
|
+
get selection() {
|
|
373225
|
+
return this._selection;
|
|
373226
|
+
}
|
|
373144
373227
|
// ── Status helper ──────────────────────────────────────────────────────────
|
|
373145
373228
|
setStatus(status) {
|
|
373146
373229
|
this._status = status;
|
|
@@ -373515,6 +373598,31 @@ var DevEnvironment = class extends TypedEventEmitter {
|
|
|
373515
373598
|
}
|
|
373516
373599
|
if (this.cancelled) return;
|
|
373517
373600
|
this._config = config;
|
|
373601
|
+
let closure;
|
|
373602
|
+
if (this.servicesFilter.length > 0) {
|
|
373603
|
+
try {
|
|
373604
|
+
closure = resolveServiceClosure(config, this.servicesFilter);
|
|
373605
|
+
} catch (err) {
|
|
373606
|
+
let message = err instanceof Error ? err.message : String(err);
|
|
373607
|
+
if (err instanceof UnknownServiceError) {
|
|
373608
|
+
const crons = err.unknownNames.filter(
|
|
373609
|
+
(n) => err.cronNames.includes(n)
|
|
373610
|
+
);
|
|
373611
|
+
for (const cron of crons) {
|
|
373612
|
+
message += `
|
|
373613
|
+
"${cron}" is a cron \u2014 crons don't run in specific dev. Trigger it with: specific exec ${cron}`;
|
|
373614
|
+
}
|
|
373615
|
+
}
|
|
373616
|
+
this.setStatus("error");
|
|
373617
|
+
this.emit("error", new Error(message));
|
|
373618
|
+
return;
|
|
373619
|
+
}
|
|
373620
|
+
this.systemLog(
|
|
373621
|
+
`Starting ${closure.services.length} of ${config.services.length} services: ${closure.services.join(", ")}`
|
|
373622
|
+
);
|
|
373623
|
+
}
|
|
373624
|
+
this._selection = closure;
|
|
373625
|
+
const servicesToRun = closure ? config.services.filter((s) => closure.services.includes(s.name)) : config.services;
|
|
373518
373626
|
this.colorMap = /* @__PURE__ */ new Map();
|
|
373519
373627
|
let colorIndex = 0;
|
|
373520
373628
|
for (const pg of config.postgres) {
|
|
@@ -373552,13 +373660,21 @@ var DevEnvironment = class extends TypedEventEmitter {
|
|
|
373552
373660
|
this.setStatus("starting");
|
|
373553
373661
|
const portAllocator = await BlockPortAllocator.create();
|
|
373554
373662
|
this.systemLog(`Using port block ${portAllocator.getBasePort()}\u2013${portAllocator.getBasePort() + 99}`);
|
|
373555
|
-
const syncDatabases = detectSyncDatabases(config);
|
|
373663
|
+
const syncDatabases = closure ? new Set(closure.syncPostgres) : detectSyncDatabases(config);
|
|
373556
373664
|
let resources;
|
|
373557
373665
|
const startedElectric = [];
|
|
373558
373666
|
try {
|
|
373559
373667
|
const result = await startResources({
|
|
373560
373668
|
config,
|
|
373561
|
-
selection:
|
|
373669
|
+
selection: closure ? {
|
|
373670
|
+
mode: "required",
|
|
373671
|
+
postgres: closure.postgres,
|
|
373672
|
+
redis: closure.redis,
|
|
373673
|
+
storage: closure.storage,
|
|
373674
|
+
temporal: closure.temporal,
|
|
373675
|
+
mail: closure.mail
|
|
373676
|
+
} : { mode: "all" },
|
|
373677
|
+
syncDatabases,
|
|
373562
373678
|
stateManager,
|
|
373563
373679
|
dataDir: `.specific/keys/${this.instanceKey}/data`,
|
|
373564
373680
|
portAllocator,
|
|
@@ -373720,8 +373836,8 @@ var DevEnvironment = class extends TypedEventEmitter {
|
|
|
373720
373836
|
if (this.cancelled) return;
|
|
373721
373837
|
const secrets = await prepareSecrets(config.secrets, true);
|
|
373722
373838
|
const configs = await prepareConfigs(config.configs, void 0, true);
|
|
373723
|
-
const usedSecrets = findUsedSecrets(
|
|
373724
|
-
const usedConfigs = findUsedConfigs(
|
|
373839
|
+
const usedSecrets = findUsedSecrets(servicesToRun, true);
|
|
373840
|
+
const usedConfigs = findUsedConfigs(servicesToRun, true);
|
|
373725
373841
|
let missingSecrets = findMissingSecrets(
|
|
373726
373842
|
config.secrets,
|
|
373727
373843
|
secrets,
|
|
@@ -373805,7 +373921,7 @@ Add them to the config block in specific.local`
|
|
|
373805
373921
|
}
|
|
373806
373922
|
const serviceEndpoints = /* @__PURE__ */ new Map();
|
|
373807
373923
|
const serviceEndpointPorts = /* @__PURE__ */ new Map();
|
|
373808
|
-
for (const service of
|
|
373924
|
+
for (const service of servicesToRun) {
|
|
373809
373925
|
if (!service.command && !service.dev?.command) continue;
|
|
373810
373926
|
const endpointPorts = /* @__PURE__ */ new Map();
|
|
373811
373927
|
const endpointInfos = [];
|
|
@@ -373832,7 +373948,7 @@ Add them to the config block in specific.local`
|
|
|
373832
373948
|
serviceEndpoints.set(service.name, endpointInfos);
|
|
373833
373949
|
}
|
|
373834
373950
|
const exposedServices = [];
|
|
373835
|
-
for (const service of
|
|
373951
|
+
for (const service of servicesToRun) {
|
|
373836
373952
|
const endpointInfos = serviceEndpoints.get(service.name) || [];
|
|
373837
373953
|
for (const info of endpointInfos) {
|
|
373838
373954
|
const endpointConfig = service.endpoints.find(
|
|
@@ -373878,7 +373994,7 @@ Add them to the config block in specific.local`
|
|
|
373878
373994
|
}
|
|
373879
373995
|
return this.projectDir;
|
|
373880
373996
|
};
|
|
373881
|
-
for (const service of
|
|
373997
|
+
for (const service of servicesToRun) {
|
|
373882
373998
|
if (this.cancelled) break;
|
|
373883
373999
|
if (!service.command && !service.dev?.command) {
|
|
373884
374000
|
continue;
|
|
@@ -373947,7 +374063,7 @@ Add them to the config block in specific.local`
|
|
|
373947
374063
|
this.systemLog("Restarting services (migration changed)");
|
|
373948
374064
|
await Promise.all(this.services.map((s) => s.stop()));
|
|
373949
374065
|
const newServices = [];
|
|
373950
|
-
for (const service of
|
|
374066
|
+
for (const service of servicesToRun) {
|
|
373951
374067
|
if (!service.command && !service.dev?.command) continue;
|
|
373952
374068
|
try {
|
|
373953
374069
|
const endpointPorts = serviceEndpointPorts.get(service.name) || /* @__PURE__ */ new Map();
|
|
@@ -374770,7 +374886,7 @@ function trackEvent(event, properties) {
|
|
|
374770
374886
|
event,
|
|
374771
374887
|
properties: {
|
|
374772
374888
|
...properties,
|
|
374773
|
-
cli_version: "0.1.
|
|
374889
|
+
cli_version: "0.1.185",
|
|
374774
374890
|
platform: process.platform,
|
|
374775
374891
|
node_version: process.version,
|
|
374776
374892
|
project_id: getProjectId()
|
|
@@ -376238,7 +376354,18 @@ function ConfigInput({ configName, defaultValue, onSubmit, onCancel }) {
|
|
|
376238
376354
|
|
|
376239
376355
|
// src/commands/dev.tsx
|
|
376240
376356
|
var COLORS2 = ["cyan", "yellow", "green", "magenta", "blue"];
|
|
376241
|
-
function
|
|
376357
|
+
function filterConfigToSelection(config, selection) {
|
|
376358
|
+
return {
|
|
376359
|
+
...config,
|
|
376360
|
+
services: config.services.filter((s) => selection.services.includes(s.name)),
|
|
376361
|
+
postgres: config.postgres.filter((p) => selection.postgres.includes(p.name)),
|
|
376362
|
+
redis: config.redis.filter((r) => selection.redis.includes(r.name)),
|
|
376363
|
+
storage: config.storage.filter((s) => selection.storage.includes(s.name)),
|
|
376364
|
+
temporal: config.temporal.filter((t) => selection.temporal.includes(t.name)),
|
|
376365
|
+
mail: config.mail.filter((m) => selection.mail.includes(m.name))
|
|
376366
|
+
};
|
|
376367
|
+
}
|
|
376368
|
+
function DevUI({ instanceKey, tunnelEnabled, services: serviceFilter }) {
|
|
376242
376369
|
const { exit } = useApp2();
|
|
376243
376370
|
const [state, setState] = useState5({
|
|
376244
376371
|
status: "loading",
|
|
@@ -376281,7 +376408,8 @@ function DevUI({ instanceKey, tunnelEnabled }) {
|
|
|
376281
376408
|
const devEnv = new DevEnvironment({
|
|
376282
376409
|
projectDir: process.cwd(),
|
|
376283
376410
|
instanceKey,
|
|
376284
|
-
tunnel: tunnelEnabled
|
|
376411
|
+
tunnel: tunnelEnabled,
|
|
376412
|
+
services: serviceFilter
|
|
376285
376413
|
});
|
|
376286
376414
|
devEnvRef.current = devEnv;
|
|
376287
376415
|
let currentColorMap = /* @__PURE__ */ new Map();
|
|
@@ -376291,12 +376419,18 @@ function DevUI({ instanceKey, tunnelEnabled }) {
|
|
|
376291
376419
|
} else if (status === "starting") {
|
|
376292
376420
|
const config2 = devEnv.config;
|
|
376293
376421
|
if (config2) {
|
|
376294
|
-
|
|
376422
|
+
const selection = devEnv.selection;
|
|
376423
|
+
const displayConfig = selection ? filterConfigToSelection(config2, selection) : config2;
|
|
376424
|
+
currentColorMap = buildColorMap(displayConfig);
|
|
376295
376425
|
setState((s) => ({
|
|
376296
376426
|
...s,
|
|
376297
376427
|
status: "starting",
|
|
376298
|
-
config:
|
|
376299
|
-
colorMap: currentColorMap
|
|
376428
|
+
config: displayConfig,
|
|
376429
|
+
colorMap: currentColorMap,
|
|
376430
|
+
filterInfo: selection ? {
|
|
376431
|
+
selected: selection.services.length,
|
|
376432
|
+
total: config2.services.length
|
|
376433
|
+
} : void 0
|
|
376300
376434
|
}));
|
|
376301
376435
|
} else {
|
|
376302
376436
|
setState((s) => ({ ...s, status: "starting" }));
|
|
@@ -376413,7 +376547,7 @@ function DevUI({ instanceKey, tunnelEnabled }) {
|
|
|
376413
376547
|
devEnv.removeAllListeners();
|
|
376414
376548
|
devEnvRef.current = null;
|
|
376415
376549
|
};
|
|
376416
|
-
}, [instanceKey, tunnelEnabled]);
|
|
376550
|
+
}, [instanceKey, tunnelEnabled, serviceFilter.join(",")]);
|
|
376417
376551
|
useEffect5(() => {
|
|
376418
376552
|
const handleSignal = () => {
|
|
376419
376553
|
const devEnv = devEnvRef.current;
|
|
@@ -376560,7 +376694,7 @@ function DevUI({ instanceKey, tunnelEnabled }) {
|
|
|
376560
376694
|
return /* @__PURE__ */ React6.createElement(Box6, null, /* @__PURE__ */ React6.createElement(Text6, { color: "blue" }, /* @__PURE__ */ React6.createElement(Spinner3, { type: "dots" })));
|
|
376561
376695
|
}
|
|
376562
376696
|
if (state.status === "starting") {
|
|
376563
|
-
return /* @__PURE__ */ React6.createElement(Box6, { flexDirection: "column" }, /* @__PURE__ */ React6.createElement(Text6, null, /* @__PURE__ */ React6.createElement(Text6, { bold: true, color: "cyan" }, "Specific dev server"), /* @__PURE__ */ React6.createElement(Text6, { dimColor: true }, " (Ctrl+C to stop)")), /* @__PURE__ */ React6.createElement(Text6, null, " "), (config.postgres.length > 0 || config.redis.length > 0 || config.storage.length > 0 || config.temporal.length > 0) && /* @__PURE__ */ React6.createElement(React6.Fragment, null, /* @__PURE__ */ React6.createElement(Text6, { bold: true }, "Resources:"), config.postgres.map((pg) => {
|
|
376697
|
+
return /* @__PURE__ */ React6.createElement(Box6, { flexDirection: "column" }, /* @__PURE__ */ React6.createElement(Text6, null, /* @__PURE__ */ React6.createElement(Text6, { bold: true, color: "cyan" }, "Specific dev server"), state.filterInfo && /* @__PURE__ */ React6.createElement(Text6, { dimColor: true }, " ", "[", state.filterInfo.selected, " of ", state.filterInfo.total, " services]"), /* @__PURE__ */ React6.createElement(Text6, { dimColor: true }, " (Ctrl+C to stop)")), /* @__PURE__ */ React6.createElement(Text6, null, " "), (config.postgres.length > 0 || config.redis.length > 0 || config.storage.length > 0 || config.temporal.length > 0) && /* @__PURE__ */ React6.createElement(React6.Fragment, null, /* @__PURE__ */ React6.createElement(Text6, { bold: true }, "Resources:"), config.postgres.map((pg) => {
|
|
376564
376698
|
const instance = resources.get(pg.name);
|
|
376565
376699
|
const resStatus = state.resourceStatus.get(pg.name);
|
|
376566
376700
|
const isReady = !!instance;
|
|
@@ -376625,7 +376759,7 @@ function DevUI({ instanceKey, tunnelEnabled }) {
|
|
|
376625
376759
|
const staticItems = [
|
|
376626
376760
|
{
|
|
376627
376761
|
key: "title",
|
|
376628
|
-
content: /* @__PURE__ */ React6.createElement(Text6, null, /* @__PURE__ */ React6.createElement(Text6, { bold: true, color: "cyan" }, "Specific dev server"), tunnelEnabled && /* @__PURE__ */ React6.createElement(Text6, { color: "magenta" }, " [tunnel]"), instanceKey !== "default" && /* @__PURE__ */ React6.createElement(Text6, { color: "yellow" }, " [", instanceKey, "]"), /* @__PURE__ */ React6.createElement(Text6, { dimColor: true }, " (Ctrl+C to stop)"))
|
|
376762
|
+
content: /* @__PURE__ */ React6.createElement(Text6, null, /* @__PURE__ */ React6.createElement(Text6, { bold: true, color: "cyan" }, "Specific dev server"), tunnelEnabled && /* @__PURE__ */ React6.createElement(Text6, { color: "magenta" }, " [tunnel]"), instanceKey !== "default" && /* @__PURE__ */ React6.createElement(Text6, { color: "yellow" }, " [", instanceKey, "]"), state.filterInfo && /* @__PURE__ */ React6.createElement(Text6, { dimColor: true }, " ", "[", state.filterInfo.selected, " of ", state.filterInfo.total, " services]"), /* @__PURE__ */ React6.createElement(Text6, { dimColor: true }, " (Ctrl+C to stop)"))
|
|
376629
376763
|
},
|
|
376630
376764
|
{ key: "space1", content: /* @__PURE__ */ React6.createElement(Text6, null, " ") },
|
|
376631
376765
|
// Show admin UI URL
|
|
@@ -376735,10 +376869,20 @@ function DevUI({ instanceKey, tunnelEnabled }) {
|
|
|
376735
376869
|
];
|
|
376736
376870
|
return /* @__PURE__ */ React6.createElement(Box6, { flexDirection: "column" }, /* @__PURE__ */ React6.createElement(Static, { items: staticItems }, (item) => /* @__PURE__ */ React6.createElement(Box6, { key: item.key }, item.content)));
|
|
376737
376871
|
}
|
|
376738
|
-
function devCommand(instanceKey, tunnelEnabled = false) {
|
|
376872
|
+
function devCommand(instanceKey, tunnelEnabled = false, services = []) {
|
|
376739
376873
|
const options2 = isInteractive() ? {} : { stdin: new Readable2({ read() {
|
|
376740
376874
|
} }) };
|
|
376741
|
-
render4(
|
|
376875
|
+
render4(
|
|
376876
|
+
/* @__PURE__ */ React6.createElement(
|
|
376877
|
+
DevUI,
|
|
376878
|
+
{
|
|
376879
|
+
instanceKey,
|
|
376880
|
+
tunnelEnabled,
|
|
376881
|
+
services
|
|
376882
|
+
}
|
|
376883
|
+
),
|
|
376884
|
+
options2
|
|
376885
|
+
);
|
|
376742
376886
|
}
|
|
376743
376887
|
|
|
376744
376888
|
// src/commands/deploy.tsx
|
|
@@ -379157,7 +379301,7 @@ function compareVersions(a, b) {
|
|
|
379157
379301
|
return 0;
|
|
379158
379302
|
}
|
|
379159
379303
|
async function checkForUpdate() {
|
|
379160
|
-
const currentVersion = "0.1.
|
|
379304
|
+
const currentVersion = "0.1.185";
|
|
379161
379305
|
const response = await fetch(`${BINARIES_BASE_URL}/latest?t=${Date.now()}`);
|
|
379162
379306
|
if (!response.ok) {
|
|
379163
379307
|
throw new Error(`Failed to check for updates: HTTP ${response.status}`);
|
|
@@ -380074,7 +380218,7 @@ async function domainAddCommand(service, domain, options2) {
|
|
|
380074
380218
|
var program = new Command();
|
|
380075
380219
|
var env = "production";
|
|
380076
380220
|
var envLabel = env !== "production" ? `[${env.toUpperCase()}] ` : "";
|
|
380077
|
-
program.name("specific").description(`${envLabel}Infrastructure-as-code for coding agents`).version("0.1.
|
|
380221
|
+
program.name("specific").description(`${envLabel}Infrastructure-as-code for coding agents`).version("0.1.185").enablePositionalOptions();
|
|
380078
380222
|
program.command("init").description("Initialize project for use with a coding agent").option("--agent <name...>", "Agents to configure (cursor, claude, codex, other)").addHelpText("after", `
|
|
380079
380223
|
Examples:
|
|
380080
380224
|
$ specific init
|
|
@@ -380087,13 +380231,14 @@ Examples:
|
|
|
380087
380231
|
program.command("check").description("Validate specific.hcl configuration").addHelpText("after", `
|
|
380088
380232
|
Examples:
|
|
380089
380233
|
$ specific check`).action(checkCommand);
|
|
380090
|
-
program.command("dev").description("Start local development environment").option("-k, --key <key>", "Namespace for isolated dev environment (auto-detected from git worktree if not specified)").option("--tunnel", "Expose public services via public *.tunnel.spcf.app URLs").addHelpText("after", `
|
|
380234
|
+
program.command("dev").description("Start local development environment").option("-k, --key <key>", "Namespace for isolated dev environment (auto-detected from git worktree if not specified)").option("--tunnel", "Expose public services via public *.tunnel.spcf.app URLs").option("--services <services...>", "Only start these services and everything they depend on").addHelpText("after", `
|
|
380091
380235
|
Examples:
|
|
380092
380236
|
$ specific dev
|
|
380237
|
+
$ specific dev --services api worker
|
|
380093
380238
|
$ specific dev --tunnel
|
|
380094
380239
|
$ specific dev --key feature-branch`).action((options2) => {
|
|
380095
380240
|
const key = options2.key ?? getDefaultKey();
|
|
380096
|
-
devCommand(key, options2.tunnel ?? false);
|
|
380241
|
+
devCommand(key, options2.tunnel ?? false, options2.services ?? []);
|
|
380097
380242
|
});
|
|
380098
380243
|
program.command("deploy").description("Deploy to Specific infrastructure").option("--project <id>", "Project ID to deploy to (overrides .projectid file)").option("-e, --environment <name|id>", "Target environment (auto-selected if only one exists)").option("--secret <key=value...>", "Secret values (repeatable)").option("--config <key=value...>", "Config values (repeatable)").option("--preview", "Deploy to an ephemeral preview environment").addHelpText("after", `
|
|
380099
380244
|
Examples:
|
package/dist/docs/index.md
CHANGED
|
@@ -10,7 +10,7 @@ This documentation is nested and references to other docs are in Markdown link f
|
|
|
10
10
|
2. ALWAYS run `specific check` to validate configuration and fix any issues
|
|
11
11
|
3. Run yourself or instruct the user to run `specific exec [SERVICE] -- [COMMAND]` for one-off commands that need to be run in development, like database migrations or seeding. The command will be run with databases and other services started and env vars injected.
|
|
12
12
|
|
|
13
|
-
A full development environment can be started with `specific dev`. To deploy any changes, the user can run `specific deploy`.
|
|
13
|
+
A full development environment can be started with `specific dev`. To only run part of the system, pass `--services [SERVICE...]` — the named services start along with everything they depend on (referenced services and resources, transitively). To deploy any changes, the user can run `specific deploy`.
|
|
14
14
|
|
|
15
15
|
If the project is connected to GitHub in the Specific Dashboard, pushes to the configured branch can deploy automatically. Run `specific status` to inspect the project and environment state. If a deploy fails — including GitHub-triggered deploys — run `specific deployment show` to see why, and add `--output` for the failed build's log.
|
|
16
16
|
|
package/dist/docs/postgres.md
CHANGED
|
@@ -45,6 +45,7 @@ The following PostgreSQL extensions are available in both development and produc
|
|
|
45
45
|
- **PostGIS** (`postgis`) - Geospatial queries and indexing over geometry and geography types. Enable with `CREATE EXTENSION postgis;` in a migration. See https://postgis.net/documentation/ for full documentation.
|
|
46
46
|
- **pg_trgm** (`pg_trgm`) - Trigram-based fuzzy text matching and `gin_trgm_ops` / `gist_trgm_ops` indexes for `LIKE`/`ILIKE`/similarity queries. Enable with `CREATE EXTENSION pg_trgm;` in a migration. See https://www.postgresql.org/docs/current/pgtrgm.html for full documentation.
|
|
47
47
|
- **ltree** (`ltree`) - Hierarchical tree-like structures for categories, org charts, and taxonomies. Enable with `CREATE EXTENSION ltree;` in a migration. See https://www.postgresql.org/docs/current/ltree.html for full documentation.
|
|
48
|
+
- **btree_gist** (`btree_gist`) - GiST operator classes for B-tree types (int, text, timestamp, uuid, ...), enabling exclusion constraints (e.g. preventing overlapping time ranges per resource) and multicolumn GiST indexes that mix scalar and GiST-native types. Enable with `CREATE EXTENSION btree_gist;` in a migration. See https://www.postgresql.org/docs/current/btree-gist.html for full documentation.
|
|
48
49
|
- **pgRouting** (`pgrouting`) - Geospatial routing and network analysis for transportation networks, logistics, and path-finding. Requires PostGIS. Enable with `CREATE EXTENSION pgrouting CASCADE;` in a migration. See https://docs.pgrouting.org/ for full documentation.
|
|
49
50
|
|
|
50
51
|
## Graph data
|
package/dist/docs/previews.md
CHANGED
|
@@ -29,11 +29,18 @@ The preview gets its own isolated database, buckets, and volumes; writing to the
|
|
|
29
29
|
|
|
30
30
|
A volume clone is an exact replica of the parent's on-disk data — uploads, caches, database files, and search indexes alike. This needs no extra configuration: the service boots on the cloned data exactly as it would after a restart, and writes to the preview's copy never affect the parent.
|
|
31
31
|
|
|
32
|
+
## Secrets and config
|
|
33
|
+
|
|
34
|
+
A preview also inherits the parent environment's secrets and config values, so its services start with the same configuration the parent runs with — nothing to re-enter.
|
|
35
|
+
|
|
36
|
+
To give previews a different value for a specific secret or config — say a sandbox API key or a test-only endpoint — set a **preview override** on the parent environment's Secrets and config page in the Specific Dashboard. Every preview branched from that environment then uses the override for that value while still inheriting everything else. Overrides are managed entirely from the dashboard.
|
|
37
|
+
|
|
32
38
|
---
|
|
33
39
|
|
|
34
40
|
Related topics:
|
|
35
41
|
|
|
36
42
|
- Run `specific docs volumes` for declaring persistent volumes
|
|
37
43
|
- Run `specific docs services` for service configuration
|
|
44
|
+
- Run `specific docs secrets-config` for setting preview secret and config overrides
|
|
38
45
|
- Run `specific docs observability` for querying a preview's logs and metrics
|
|
39
46
|
- Run `specific docs deployments` for debugging a failed preview deploy (`specific deployment show -e <preview>` prints the failure; add `--output` for the build log)
|
|
@@ -135,6 +135,12 @@ This file is automatically gitignored and not included in deployments. Productio
|
|
|
135
135
|
|
|
136
136
|
In production, the user can update secrets and config values directly from the Specific Dashboard at https://dashboard.specific.dev — no `specific.hcl` change needed, but the new values only take effect on the next deployment. Point them there when they want to rotate a secret or change a config value in a deployed environment.
|
|
137
137
|
|
|
138
|
+
## Preview environments
|
|
139
|
+
|
|
140
|
+
Preview environments inherit their secrets and config values from the environment they branch from, so a preview starts with the parent's configuration without re-entering anything.
|
|
141
|
+
|
|
142
|
+
To give previews a different value for a specific secret or config, set a **preview override** on the parent environment's Secrets and config page in the Specific Dashboard. Every preview branched from that environment then uses the override for that value while still inheriting the rest — handy for pointing previews at a sandbox API key or a test-only endpoint. Run `specific docs previews` for more on preview environments.
|
|
143
|
+
|
|
138
144
|
## Example
|
|
139
145
|
|
|
140
146
|
```hcl
|
package/package.json
CHANGED
/package/dist/admin/_next/static/{NNVAA5krd_olwtUDgk5Bo → gLcFBPDPi0InHgZzN22am}/_buildManifest.js
RENAMED
|
File without changes
|
|
File without changes
|
/package/dist/admin/_next/static/{NNVAA5krd_olwtUDgk5Bo → gLcFBPDPi0InHgZzN22am}/_ssgManifest.js
RENAMED
|
File without changes
|