@specific.dev/cli 0.1.183 → 0.1.184

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.
Files changed (97) hide show
  1. package/dist/admin/404/index.html +1 -1
  2. package/dist/admin/404.html +1 -1
  3. package/dist/admin/__next.!KGRlZmF1bHQp.__PAGE__.txt +1 -1
  4. package/dist/admin/__next.!KGRlZmF1bHQp.txt +1 -1
  5. package/dist/admin/__next._full.txt +1 -1
  6. package/dist/admin/__next._head.txt +1 -1
  7. package/dist/admin/__next._index.txt +1 -1
  8. package/dist/admin/__next._tree.txt +1 -1
  9. package/dist/admin/_not-found/__next._full.txt +1 -1
  10. package/dist/admin/_not-found/__next._head.txt +1 -1
  11. package/dist/admin/_not-found/__next._index.txt +1 -1
  12. package/dist/admin/_not-found/__next._not-found.__PAGE__.txt +1 -1
  13. package/dist/admin/_not-found/__next._not-found.txt +1 -1
  14. package/dist/admin/_not-found/__next._tree.txt +1 -1
  15. package/dist/admin/_not-found/index.html +1 -1
  16. package/dist/admin/_not-found/index.txt +1 -1
  17. package/dist/admin/crons/__next.!KGRlZmF1bHQp.crons.__PAGE__.txt +1 -1
  18. package/dist/admin/crons/__next.!KGRlZmF1bHQp.crons.txt +1 -1
  19. package/dist/admin/crons/__next.!KGRlZmF1bHQp.txt +1 -1
  20. package/dist/admin/crons/__next._full.txt +1 -1
  21. package/dist/admin/crons/__next._head.txt +1 -1
  22. package/dist/admin/crons/__next._index.txt +1 -1
  23. package/dist/admin/crons/__next._tree.txt +1 -1
  24. package/dist/admin/crons/index.html +1 -1
  25. package/dist/admin/crons/index.txt +1 -1
  26. package/dist/admin/databases/__next.!KGRlZmF1bHQp.databases.__PAGE__.txt +1 -1
  27. package/dist/admin/databases/__next.!KGRlZmF1bHQp.databases.txt +1 -1
  28. package/dist/admin/databases/__next.!KGRlZmF1bHQp.txt +1 -1
  29. package/dist/admin/databases/__next._full.txt +1 -1
  30. package/dist/admin/databases/__next._head.txt +1 -1
  31. package/dist/admin/databases/__next._index.txt +1 -1
  32. package/dist/admin/databases/__next._tree.txt +1 -1
  33. package/dist/admin/databases/index.html +1 -1
  34. package/dist/admin/databases/index.txt +1 -1
  35. package/dist/admin/fullscreen/__next._full.txt +1 -1
  36. package/dist/admin/fullscreen/__next._head.txt +1 -1
  37. package/dist/admin/fullscreen/__next._index.txt +1 -1
  38. package/dist/admin/fullscreen/__next._tree.txt +1 -1
  39. package/dist/admin/fullscreen/__next.fullscreen.__PAGE__.txt +1 -1
  40. package/dist/admin/fullscreen/__next.fullscreen.txt +1 -1
  41. package/dist/admin/fullscreen/databases/__next._full.txt +1 -1
  42. package/dist/admin/fullscreen/databases/__next._head.txt +1 -1
  43. package/dist/admin/fullscreen/databases/__next._index.txt +1 -1
  44. package/dist/admin/fullscreen/databases/__next._tree.txt +1 -1
  45. package/dist/admin/fullscreen/databases/__next.fullscreen.databases.__PAGE__.txt +1 -1
  46. package/dist/admin/fullscreen/databases/__next.fullscreen.databases.txt +1 -1
  47. package/dist/admin/fullscreen/databases/__next.fullscreen.txt +1 -1
  48. package/dist/admin/fullscreen/databases/index.html +1 -1
  49. package/dist/admin/fullscreen/databases/index.txt +1 -1
  50. package/dist/admin/fullscreen/index.html +1 -1
  51. package/dist/admin/fullscreen/index.txt +1 -1
  52. package/dist/admin/index.html +1 -1
  53. package/dist/admin/index.txt +1 -1
  54. package/dist/admin/mail/__next.!KGRlZmF1bHQp.mail.__PAGE__.txt +1 -1
  55. package/dist/admin/mail/__next.!KGRlZmF1bHQp.mail.txt +1 -1
  56. package/dist/admin/mail/__next.!KGRlZmF1bHQp.txt +1 -1
  57. package/dist/admin/mail/__next._full.txt +1 -1
  58. package/dist/admin/mail/__next._head.txt +1 -1
  59. package/dist/admin/mail/__next._index.txt +1 -1
  60. package/dist/admin/mail/__next._tree.txt +1 -1
  61. package/dist/admin/mail/index.html +1 -1
  62. package/dist/admin/mail/index.txt +1 -1
  63. package/dist/admin/services/__next.!KGRlZmF1bHQp.services.__PAGE__.txt +1 -1
  64. package/dist/admin/services/__next.!KGRlZmF1bHQp.services.txt +1 -1
  65. package/dist/admin/services/__next.!KGRlZmF1bHQp.txt +1 -1
  66. package/dist/admin/services/__next._full.txt +1 -1
  67. package/dist/admin/services/__next._head.txt +1 -1
  68. package/dist/admin/services/__next._index.txt +1 -1
  69. package/dist/admin/services/__next._tree.txt +1 -1
  70. package/dist/admin/services/index.html +1 -1
  71. package/dist/admin/services/index.txt +1 -1
  72. package/dist/admin/storage/__next.!KGRlZmF1bHQp.storage.__PAGE__.txt +1 -1
  73. package/dist/admin/storage/__next.!KGRlZmF1bHQp.storage.txt +1 -1
  74. package/dist/admin/storage/__next.!KGRlZmF1bHQp.txt +1 -1
  75. package/dist/admin/storage/__next._full.txt +1 -1
  76. package/dist/admin/storage/__next._head.txt +1 -1
  77. package/dist/admin/storage/__next._index.txt +1 -1
  78. package/dist/admin/storage/__next._tree.txt +1 -1
  79. package/dist/admin/storage/index.html +1 -1
  80. package/dist/admin/storage/index.txt +1 -1
  81. package/dist/admin/workflows/__next.!KGRlZmF1bHQp.txt +1 -1
  82. package/dist/admin/workflows/__next.!KGRlZmF1bHQp.workflows.__PAGE__.txt +1 -1
  83. package/dist/admin/workflows/__next.!KGRlZmF1bHQp.workflows.txt +1 -1
  84. package/dist/admin/workflows/__next._full.txt +1 -1
  85. package/dist/admin/workflows/__next._head.txt +1 -1
  86. package/dist/admin/workflows/__next._index.txt +1 -1
  87. package/dist/admin/workflows/__next._tree.txt +1 -1
  88. package/dist/admin/workflows/index.html +1 -1
  89. package/dist/admin/workflows/index.txt +1 -1
  90. package/dist/cli.js +179 -34
  91. package/dist/docs/index.md +1 -1
  92. package/dist/docs/previews.md +7 -0
  93. package/dist/docs/secrets-config.md +6 -0
  94. package/package.json +1 -1
  95. /package/dist/admin/_next/static/{NNVAA5krd_olwtUDgk5Bo → SWOCW_-G5yh3FBHTMSXpq}/_buildManifest.js +0 -0
  96. /package/dist/admin/_next/static/{NNVAA5krd_olwtUDgk5Bo → SWOCW_-G5yh3FBHTMSXpq}/_clientMiddlewareManifest.json +0 -0
  97. /package/dist/admin/_next/static/{NNVAA5krd_olwtUDgk5Bo → SWOCW_-G5yh3FBHTMSXpq}/_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 envValueReferences(value)) {
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 = envValueReferences(value);
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: { mode: "all" },
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(config.services, true);
373724
- const usedConfigs = findUsedConfigs(config.services, true);
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 config.services) {
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 config.services) {
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 config.services) {
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 config.services) {
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.183",
374889
+ cli_version: "0.1.184",
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 DevUI({ instanceKey, tunnelEnabled }) {
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
- currentColorMap = buildColorMap(config2);
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: config2,
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(/* @__PURE__ */ React6.createElement(DevUI, { instanceKey, tunnelEnabled }), options2);
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.183";
379304
+ const currentVersion = "0.1.184";
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.183").enablePositionalOptions();
380221
+ program.name("specific").description(`${envLabel}Infrastructure-as-code for coding agents`).version("0.1.184").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:
@@ -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
 
@@ -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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@specific.dev/cli",
3
- "version": "0.1.183",
3
+ "version": "0.1.184",
4
4
  "description": "CLI for Specific infrastructure-as-code",
5
5
  "type": "module",
6
6
  "main": "dist/cli.js",