@specific.dev/cli 0.1.186 → 0.1.187

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 (96) 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 +2 -2
  4. package/dist/admin/__next.!KGRlZmF1bHQp.txt +1 -1
  5. package/dist/admin/__next._full.txt +2 -2
  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/_next/static/chunks/{cbb47779f2b2b95e.js → dc8ca40fc1cc95ef.js} +2 -2
  10. package/dist/admin/_not-found/__next._full.txt +1 -1
  11. package/dist/admin/_not-found/__next._head.txt +1 -1
  12. package/dist/admin/_not-found/__next._index.txt +1 -1
  13. package/dist/admin/_not-found/__next._not-found.__PAGE__.txt +1 -1
  14. package/dist/admin/_not-found/__next._not-found.txt +1 -1
  15. package/dist/admin/_not-found/__next._tree.txt +1 -1
  16. package/dist/admin/_not-found/index.html +1 -1
  17. package/dist/admin/_not-found/index.txt +1 -1
  18. package/dist/admin/crons/__next.!KGRlZmF1bHQp.crons.__PAGE__.txt +2 -2
  19. package/dist/admin/crons/__next.!KGRlZmF1bHQp.crons.txt +1 -1
  20. package/dist/admin/crons/__next.!KGRlZmF1bHQp.txt +1 -1
  21. package/dist/admin/crons/__next._full.txt +2 -2
  22. package/dist/admin/crons/__next._head.txt +1 -1
  23. package/dist/admin/crons/__next._index.txt +1 -1
  24. package/dist/admin/crons/__next._tree.txt +1 -1
  25. package/dist/admin/crons/index.html +1 -1
  26. package/dist/admin/crons/index.txt +2 -2
  27. package/dist/admin/databases/__next.!KGRlZmF1bHQp.databases.__PAGE__.txt +1 -1
  28. package/dist/admin/databases/__next.!KGRlZmF1bHQp.databases.txt +1 -1
  29. package/dist/admin/databases/__next.!KGRlZmF1bHQp.txt +1 -1
  30. package/dist/admin/databases/__next._full.txt +1 -1
  31. package/dist/admin/databases/__next._head.txt +1 -1
  32. package/dist/admin/databases/__next._index.txt +1 -1
  33. package/dist/admin/databases/__next._tree.txt +1 -1
  34. package/dist/admin/databases/index.html +1 -1
  35. package/dist/admin/databases/index.txt +1 -1
  36. package/dist/admin/fullscreen/__next._full.txt +2 -2
  37. package/dist/admin/fullscreen/__next._head.txt +1 -1
  38. package/dist/admin/fullscreen/__next._index.txt +1 -1
  39. package/dist/admin/fullscreen/__next._tree.txt +1 -1
  40. package/dist/admin/fullscreen/__next.fullscreen.__PAGE__.txt +2 -2
  41. package/dist/admin/fullscreen/__next.fullscreen.txt +1 -1
  42. package/dist/admin/fullscreen/databases/__next._full.txt +1 -1
  43. package/dist/admin/fullscreen/databases/__next._head.txt +1 -1
  44. package/dist/admin/fullscreen/databases/__next._index.txt +1 -1
  45. package/dist/admin/fullscreen/databases/__next._tree.txt +1 -1
  46. package/dist/admin/fullscreen/databases/__next.fullscreen.databases.__PAGE__.txt +1 -1
  47. package/dist/admin/fullscreen/databases/__next.fullscreen.databases.txt +1 -1
  48. package/dist/admin/fullscreen/databases/__next.fullscreen.txt +1 -1
  49. package/dist/admin/fullscreen/databases/index.html +1 -1
  50. package/dist/admin/fullscreen/databases/index.txt +1 -1
  51. package/dist/admin/fullscreen/index.html +1 -1
  52. package/dist/admin/fullscreen/index.txt +2 -2
  53. package/dist/admin/index.html +1 -1
  54. package/dist/admin/index.txt +2 -2
  55. package/dist/admin/mail/__next.!KGRlZmF1bHQp.mail.__PAGE__.txt +1 -1
  56. package/dist/admin/mail/__next.!KGRlZmF1bHQp.mail.txt +1 -1
  57. package/dist/admin/mail/__next.!KGRlZmF1bHQp.txt +1 -1
  58. package/dist/admin/mail/__next._full.txt +1 -1
  59. package/dist/admin/mail/__next._head.txt +1 -1
  60. package/dist/admin/mail/__next._index.txt +1 -1
  61. package/dist/admin/mail/__next._tree.txt +1 -1
  62. package/dist/admin/mail/index.html +1 -1
  63. package/dist/admin/mail/index.txt +1 -1
  64. package/dist/admin/services/__next.!KGRlZmF1bHQp.services.__PAGE__.txt +1 -1
  65. package/dist/admin/services/__next.!KGRlZmF1bHQp.services.txt +1 -1
  66. package/dist/admin/services/__next.!KGRlZmF1bHQp.txt +1 -1
  67. package/dist/admin/services/__next._full.txt +1 -1
  68. package/dist/admin/services/__next._head.txt +1 -1
  69. package/dist/admin/services/__next._index.txt +1 -1
  70. package/dist/admin/services/__next._tree.txt +1 -1
  71. package/dist/admin/services/index.html +1 -1
  72. package/dist/admin/services/index.txt +1 -1
  73. package/dist/admin/storage/__next.!KGRlZmF1bHQp.storage.__PAGE__.txt +1 -1
  74. package/dist/admin/storage/__next.!KGRlZmF1bHQp.storage.txt +1 -1
  75. package/dist/admin/storage/__next.!KGRlZmF1bHQp.txt +1 -1
  76. package/dist/admin/storage/__next._full.txt +1 -1
  77. package/dist/admin/storage/__next._head.txt +1 -1
  78. package/dist/admin/storage/__next._index.txt +1 -1
  79. package/dist/admin/storage/__next._tree.txt +1 -1
  80. package/dist/admin/storage/index.html +1 -1
  81. package/dist/admin/storage/index.txt +1 -1
  82. package/dist/admin/workflows/__next.!KGRlZmF1bHQp.txt +1 -1
  83. package/dist/admin/workflows/__next.!KGRlZmF1bHQp.workflows.__PAGE__.txt +1 -1
  84. package/dist/admin/workflows/__next.!KGRlZmF1bHQp.workflows.txt +1 -1
  85. package/dist/admin/workflows/__next._full.txt +1 -1
  86. package/dist/admin/workflows/__next._head.txt +1 -1
  87. package/dist/admin/workflows/__next._index.txt +1 -1
  88. package/dist/admin/workflows/__next._tree.txt +1 -1
  89. package/dist/admin/workflows/index.html +1 -1
  90. package/dist/admin/workflows/index.txt +1 -1
  91. package/dist/cli.js +322 -69
  92. package/dist/docs/storage.md +30 -0
  93. package/package.json +1 -1
  94. /package/dist/admin/_next/static/{6oKX20WXYrqT7PTdniaYQ → aJUGfHUUdbbGy0TJq5kAc}/_buildManifest.js +0 -0
  95. /package/dist/admin/_next/static/{6oKX20WXYrqT7PTdniaYQ → aJUGfHUUdbbGy0TJq5kAc}/_clientMiddlewareManifest.json +0 -0
  96. /package/dist/admin/_next/static/{6oKX20WXYrqT7PTdniaYQ → aJUGfHUUdbbGy0TJq5kAc}/_ssgManifest.js +0 -0
package/dist/cli.js CHANGED
@@ -368324,7 +368324,7 @@ function parseReferenceString(str) {
368324
368324
  const storageMatch = str.match(new RegExp(`^storage\\.(${id})\\.(\\w+)$`));
368325
368325
  if (storageMatch && storageMatch[1] && storageMatch[2]) {
368326
368326
  const attr = storageMatch[2];
368327
- if (["endpoint", "access_key", "secret_key", "bucket"].includes(attr)) {
368327
+ if (["endpoint", "access_key", "secret_key", "bucket", "public_url"].includes(attr)) {
368328
368328
  return {
368329
368329
  type: "storage",
368330
368330
  name: storageMatch[1],
@@ -368860,8 +368860,28 @@ function parseStorage(data) {
368860
368860
  return [];
368861
368861
  }
368862
368862
  const result = [];
368863
- for (const [name] of Object.entries(data)) {
368864
- result.push({ name });
368863
+ for (const [name, fieldsArray] of Object.entries(data)) {
368864
+ if (Array.isArray(fieldsArray) && fieldsArray.length > 1) {
368865
+ throw new Error(`Storage "${name}" is defined more than once. Merge the blocks into a single storage "${name}" block.`);
368866
+ }
368867
+ const fields = Array.isArray(fieldsArray) ? fieldsArray[0] : fieldsArray;
368868
+ if (!fields || typeof fields !== "object" || Array.isArray(fields)) {
368869
+ result.push({ name });
368870
+ continue;
368871
+ }
368872
+ const fieldObj = fields;
368873
+ const unknownFields = Object.keys(fieldObj).filter((field) => field !== "public");
368874
+ if (unknownFields.length > 0) {
368875
+ throw new Error(`Storage "${name}" has unsupported field "${unknownFields[0]}". Supported fields: public.`);
368876
+ }
368877
+ const publicValue = Array.isArray(fieldObj.public) ? fieldObj.public[0] : fieldObj.public;
368878
+ if (publicValue !== void 0 && typeof publicValue !== "boolean") {
368879
+ throw new Error(`Storage "${name}" has invalid "public" value. Must be a boolean.`);
368880
+ }
368881
+ result.push({
368882
+ name,
368883
+ ...publicValue !== void 0 ? { public: publicValue } : {}
368884
+ });
368865
368885
  }
368866
368886
  return result;
368867
368887
  }
@@ -369021,82 +369041,127 @@ function validateEndpointReferences(config) {
369021
369041
  for (const service of config.services) {
369022
369042
  serviceEndpointsMap.set(service.name, service.endpoints);
369023
369043
  }
369044
+ const storageByName = new Map(config.storage.map((storage) => [storage.name, storage]));
369045
+ const validateStorageReference = (ownerType, ownerName, key, ref, envLabel2 = "env var") => {
369046
+ const target = storageByName.get(ref.name);
369047
+ if (!target) {
369048
+ errors.push({
369049
+ service: ownerName,
369050
+ message: `${ownerType} "${ownerName}" references unknown storage "${ref.name}" in ${envLabel2} "${key}".`
369051
+ });
369052
+ return;
369053
+ }
369054
+ if (ref.attribute === "public_url" && target.public !== true) {
369055
+ errors.push({
369056
+ service: ownerName,
369057
+ message: `${ownerType} "${ownerName}" references public_url of storage "${ref.name}" in ${envLabel2} "${key}", but that storage is not public. Add \`public = true\` to the storage block.`
369058
+ });
369059
+ }
369060
+ };
369024
369061
  for (const service of config.services) {
369025
- if (!service.env)
369026
- continue;
369027
- for (const [key, value] of Object.entries(service.env)) {
369028
- const refs = extractEnvReferences(value);
369029
- for (const ref of refs) {
369030
- if (service.endpoints.length > 1 && ref.type === "port") {
369031
- errors.push({
369032
- service: service.name,
369033
- message: `Service "${service.name}" has multiple endpoints but uses \${port} for env var "${key}". Use \${endpoint.<name>.port} instead.`
369034
- });
369035
- }
369036
- if (ref.type === "service") {
369037
- const serviceRef = ref;
369038
- const targetEndpoints = serviceEndpointsMap.get(serviceRef.serviceName);
369039
- if (!targetEndpoints) {
369040
- errors.push({
369041
- service: service.name,
369042
- message: `Service "${service.name}" references unknown service "${serviceRef.serviceName}" in env var "${key}".`
369043
- });
369044
- continue;
369045
- }
369046
- if (targetEndpoints.length > 1 && !serviceRef.endpointName) {
369062
+ if (service.env) {
369063
+ for (const [key, value] of Object.entries(service.env)) {
369064
+ const refs = extractEnvReferences(value);
369065
+ for (const ref of refs) {
369066
+ if (ref.type === "storage") {
369067
+ validateStorageReference("Service", service.name, key, ref);
369068
+ }
369069
+ if (service.endpoints.length > 1 && ref.type === "port") {
369047
369070
  errors.push({
369048
369071
  service: service.name,
369049
- message: `Service "${service.name}" references service "${serviceRef.serviceName}" which has multiple endpoints, but no endpoint name specified in env var "${key}". Use \${service.${serviceRef.serviceName}.endpoint.<name>.${serviceRef.attribute}} instead.`
369072
+ message: `Service "${service.name}" has multiple endpoints but uses \${port} for env var "${key}". Use \${endpoint.<name>.port} instead.`
369050
369073
  });
369051
369074
  }
369052
- if (serviceRef.endpointName) {
369053
- const endpointExists = targetEndpoints.some((e) => e.name === serviceRef.endpointName);
369054
- if (!endpointExists) {
369075
+ if (ref.type === "service") {
369076
+ const serviceRef = ref;
369077
+ const targetEndpoints = serviceEndpointsMap.get(serviceRef.serviceName);
369078
+ if (!targetEndpoints) {
369055
369079
  errors.push({
369056
369080
  service: service.name,
369057
- message: `Service "${service.name}" references endpoint "${serviceRef.endpointName}" on service "${serviceRef.serviceName}" in env var "${key}", but that endpoint does not exist.`
369081
+ message: `Service "${service.name}" references unknown service "${serviceRef.serviceName}" in env var "${key}".`
369058
369082
  });
369083
+ continue;
369059
369084
  }
369060
- }
369061
- if (serviceRef.attribute === "public_url") {
369062
- const endpointName = serviceRef.endpointName;
369063
- if (endpointName) {
369064
- const targetEndpoint = targetEndpoints.find((e) => e.name === endpointName);
369065
- if (targetEndpoint && !targetEndpoint.public) {
369085
+ if (targetEndpoints.length > 1 && !serviceRef.endpointName) {
369086
+ errors.push({
369087
+ service: service.name,
369088
+ message: `Service "${service.name}" references service "${serviceRef.serviceName}" which has multiple endpoints, but no endpoint name specified in env var "${key}". Use \${service.${serviceRef.serviceName}.endpoint.<name>.${serviceRef.attribute}} instead.`
369089
+ });
369090
+ }
369091
+ if (serviceRef.endpointName) {
369092
+ const endpointExists = targetEndpoints.some((e) => e.name === serviceRef.endpointName);
369093
+ if (!endpointExists) {
369066
369094
  errors.push({
369067
369095
  service: service.name,
369068
- message: `Service "${service.name}" references public_url of endpoint "${endpointName}" on service "${serviceRef.serviceName}" in env var "${key}", but that endpoint is not public. Add \`public = true\` to the endpoint.`
369096
+ message: `Service "${service.name}" references endpoint "${serviceRef.endpointName}" on service "${serviceRef.serviceName}" in env var "${key}", but that endpoint does not exist.`
369069
369097
  });
369070
369098
  }
369071
- } else {
369072
- const defaultEndpoint = targetEndpoints.length === 1 ? targetEndpoints[0] : targetEndpoints.find((e) => e.name === "default");
369073
- if (defaultEndpoint && !defaultEndpoint.public) {
369074
- errors.push({
369075
- service: service.name,
369076
- message: `Service "${service.name}" references public_url of service "${serviceRef.serviceName}" in env var "${key}", but its endpoint is not public. Add \`public = true\` to the endpoint.`
369077
- });
369099
+ }
369100
+ if (serviceRef.attribute === "public_url") {
369101
+ const endpointName = serviceRef.endpointName;
369102
+ if (endpointName) {
369103
+ const targetEndpoint = targetEndpoints.find((e) => e.name === endpointName);
369104
+ if (targetEndpoint && !targetEndpoint.public) {
369105
+ errors.push({
369106
+ service: service.name,
369107
+ message: `Service "${service.name}" references public_url of endpoint "${endpointName}" on service "${serviceRef.serviceName}" in env var "${key}", but that endpoint is not public. Add \`public = true\` to the endpoint.`
369108
+ });
369109
+ }
369110
+ } else {
369111
+ const defaultEndpoint = targetEndpoints.length === 1 ? targetEndpoints[0] : targetEndpoints.find((e) => e.name === "default");
369112
+ if (defaultEndpoint && !defaultEndpoint.public) {
369113
+ errors.push({
369114
+ service: service.name,
369115
+ message: `Service "${service.name}" references public_url of service "${serviceRef.serviceName}" in env var "${key}", but its endpoint is not public. Add \`public = true\` to the endpoint.`
369116
+ });
369117
+ }
369078
369118
  }
369079
369119
  }
369080
369120
  }
369121
+ if (ref.type === "endpoint") {
369122
+ const endpointRef = ref;
369123
+ const endpointExists = service.endpoints.some((e) => e.name === endpointRef.endpointName);
369124
+ if (!endpointExists) {
369125
+ errors.push({
369126
+ service: service.name,
369127
+ message: `Service "${service.name}" references endpoint "${endpointRef.endpointName}" in env var "${key}", but that endpoint is not defined on this service.`
369128
+ });
369129
+ }
369130
+ }
369131
+ if (ref.type === "volume") {
369132
+ const volumeRef = ref;
369133
+ const volumeExists = service.volumes?.some((v) => v.name === volumeRef.volumeName);
369134
+ if (!volumeExists) {
369135
+ errors.push({
369136
+ service: service.name,
369137
+ message: `Service "${service.name}" references volume "${volumeRef.volumeName}" in env var "${key}", but that volume is not defined on this service.`
369138
+ });
369139
+ }
369140
+ }
369081
369141
  }
369082
- if (ref.type === "endpoint") {
369083
- const endpointRef = ref;
369084
- const endpointExists = service.endpoints.some((e) => e.name === endpointRef.endpointName);
369085
- if (!endpointExists) {
369086
- errors.push({
369087
- service: service.name,
369088
- message: `Service "${service.name}" references endpoint "${endpointRef.endpointName}" in env var "${key}", but that endpoint is not defined on this service.`
369089
- });
369142
+ }
369143
+ }
369144
+ if (service.dev?.env) {
369145
+ for (const [key, value] of Object.entries(service.dev.env)) {
369146
+ for (const ref of extractEnvReferences(value)) {
369147
+ if (ref.type === "storage") {
369148
+ validateStorageReference("Service", service.name, key, ref, "dev env var");
369090
369149
  }
369091
369150
  }
369092
- if (ref.type === "volume") {
369093
- const volumeRef = ref;
369094
- const volumeExists = service.volumes?.some((v) => v.name === volumeRef.volumeName);
369095
- if (!volumeExists) {
369096
- errors.push({
369097
- service: service.name,
369098
- message: `Service "${service.name}" references volume "${volumeRef.volumeName}" in env var "${key}", but that volume is not defined on this service.`
369099
- });
369151
+ }
369152
+ }
369153
+ }
369154
+ for (const cron of config.crons) {
369155
+ for (const [envLabel2, env2] of [
369156
+ ["env var", cron.env],
369157
+ ["dev env var", cron.dev?.env]
369158
+ ]) {
369159
+ if (!env2)
369160
+ continue;
369161
+ for (const [key, value] of Object.entries(env2)) {
369162
+ for (const ref of extractEnvReferences(value)) {
369163
+ if (ref.type === "storage") {
369164
+ validateStorageReference("Cron", cron.name, key, ref, envLabel2);
369100
369165
  }
369101
369166
  }
369102
369167
  }
@@ -369945,6 +370010,7 @@ async function startStorage(storage, port, dataDir) {
369945
370010
  accessKey,
369946
370011
  secretKey,
369947
370012
  bucket: storage.name,
370013
+ ...storage.public ? { publicUrl: `${endpoint}/${encodeURIComponent(storage.name)}` } : {},
369948
370014
  async stop() {
369949
370015
  const httpServer = server.httpServer;
369950
370016
  if (httpServer) {
@@ -370350,6 +370416,11 @@ function resolveEnvValue(value, resources, secrets, configs, servicePort, servic
370350
370416
  throw new Error(`Storage "${value.name}" does not have a bucket`);
370351
370417
  }
370352
370418
  return storage.bucket;
370419
+ case "public_url":
370420
+ if (!storage.publicUrl) {
370421
+ throw new Error(`Storage "${value.name}" is not public`);
370422
+ }
370423
+ return storage.publicUrl;
370353
370424
  default:
370354
370425
  throw new Error(`Unknown storage attribute: ${String(value.attribute)}`);
370355
370426
  }
@@ -372605,6 +372676,8 @@ async function startResources(options2) {
372605
372676
  if (instance.endpoint) dbState.endpoint = instance.endpoint;
372606
372677
  if (instance.accessKey) dbState.accessKey = instance.accessKey;
372607
372678
  if (instance.secretKey) dbState.secretKey = instance.secretKey;
372679
+ if (instance.bucket) dbState.bucket = instance.bucket;
372680
+ if (instance.publicUrl) dbState.publicUrl = instance.publicUrl;
372608
372681
  await stateManager.registerDatabase(storage.name, dbState);
372609
372682
  }
372610
372683
  if (temporalConfigs.length > 0) {
@@ -374225,7 +374298,8 @@ Add them to the config block in specific.local`
374225
374298
  type: r.type,
374226
374299
  port: r.port,
374227
374300
  host: r.host,
374228
- syncEnabled: r.type === "postgres" && syncDatabases.has(name)
374301
+ syncEnabled: r.type === "postgres" && syncDatabases.has(name),
374302
+ ...r.publicUrl ? { publicUrl: r.publicUrl } : {}
374229
374303
  })),
374230
374304
  hasTemporal,
374231
374305
  hasMail,
@@ -374886,7 +374960,7 @@ function trackEvent(event, properties) {
374886
374960
  event,
374887
374961
  properties: {
374888
374962
  ...properties,
374889
- cli_version: "0.1.186",
374963
+ cli_version: "0.1.187",
374890
374964
  platform: process.platform,
374891
374965
  node_version: process.version,
374892
374966
  project_id: getProjectId()
@@ -375355,6 +375429,18 @@ import { execFile as execFile8 } from "child_process";
375355
375429
 
375356
375430
  // node_modules/.pnpm/@specific+config@file+..+config/node_modules/@specific/config/dist/parser.js
375357
375431
  var import_hcl2_json_parser3 = __toESM(require_dist2(), 1);
375432
+
375433
+ // node_modules/.pnpm/@specific+config@file+..+config/node_modules/@specific/config/dist/types.js
375434
+ function extractEnvReferences2(value) {
375435
+ if (typeof value === "string")
375436
+ return [];
375437
+ if (value.type === "interpolated") {
375438
+ return value.parts.filter((p) => p.type === "ref").map((p) => p.ref);
375439
+ }
375440
+ return [value];
375441
+ }
375442
+
375443
+ // node_modules/.pnpm/@specific+config@file+..+config/node_modules/@specific/config/dist/parser.js
375358
375444
  var { parseToObject: parseToObject3 } = import_hcl2_json_parser3.default;
375359
375445
  function parseReference2(value) {
375360
375446
  if (typeof value !== "string") {
@@ -375446,7 +375532,7 @@ function parseReferenceString2(str) {
375446
375532
  const storageMatch = str.match(new RegExp(`^storage\\.(${id})\\.(\\w+)$`));
375447
375533
  if (storageMatch && storageMatch[1] && storageMatch[2]) {
375448
375534
  const attr = storageMatch[2];
375449
- if (["endpoint", "access_key", "secret_key", "bucket"].includes(attr)) {
375535
+ if (["endpoint", "access_key", "secret_key", "bucket", "public_url"].includes(attr)) {
375450
375536
  return {
375451
375537
  type: "storage",
375452
375538
  name: storageMatch[1],
@@ -375982,8 +376068,28 @@ function parseStorage2(data) {
375982
376068
  return [];
375983
376069
  }
375984
376070
  const result = [];
375985
- for (const [name] of Object.entries(data)) {
375986
- result.push({ name });
376071
+ for (const [name, fieldsArray] of Object.entries(data)) {
376072
+ if (Array.isArray(fieldsArray) && fieldsArray.length > 1) {
376073
+ throw new Error(`Storage "${name}" is defined more than once. Merge the blocks into a single storage "${name}" block.`);
376074
+ }
376075
+ const fields = Array.isArray(fieldsArray) ? fieldsArray[0] : fieldsArray;
376076
+ if (!fields || typeof fields !== "object" || Array.isArray(fields)) {
376077
+ result.push({ name });
376078
+ continue;
376079
+ }
376080
+ const fieldObj = fields;
376081
+ const unknownFields = Object.keys(fieldObj).filter((field) => field !== "public");
376082
+ if (unknownFields.length > 0) {
376083
+ throw new Error(`Storage "${name}" has unsupported field "${unknownFields[0]}". Supported fields: public.`);
376084
+ }
376085
+ const publicValue = Array.isArray(fieldObj.public) ? fieldObj.public[0] : fieldObj.public;
376086
+ if (publicValue !== void 0 && typeof publicValue !== "boolean") {
376087
+ throw new Error(`Storage "${name}" has invalid "public" value. Must be a boolean.`);
376088
+ }
376089
+ result.push({
376090
+ name,
376091
+ ...publicValue !== void 0 ? { public: publicValue } : {}
376092
+ });
375987
376093
  }
375988
376094
  return result;
375989
376095
  }
@@ -376126,6 +376232,146 @@ async function parseConfig2(hcl) {
376126
376232
  environments: parseEnvironments2(json.environment)
376127
376233
  };
376128
376234
  }
376235
+ function validateEndpointReferences2(config) {
376236
+ const errors = [];
376237
+ const serviceEndpointsMap = /* @__PURE__ */ new Map();
376238
+ for (const service of config.services) {
376239
+ serviceEndpointsMap.set(service.name, service.endpoints);
376240
+ }
376241
+ const storageByName = new Map(config.storage.map((storage) => [storage.name, storage]));
376242
+ const validateStorageReference = (ownerType, ownerName, key, ref, envLabel2 = "env var") => {
376243
+ const target = storageByName.get(ref.name);
376244
+ if (!target) {
376245
+ errors.push({
376246
+ service: ownerName,
376247
+ message: `${ownerType} "${ownerName}" references unknown storage "${ref.name}" in ${envLabel2} "${key}".`
376248
+ });
376249
+ return;
376250
+ }
376251
+ if (ref.attribute === "public_url" && target.public !== true) {
376252
+ errors.push({
376253
+ service: ownerName,
376254
+ message: `${ownerType} "${ownerName}" references public_url of storage "${ref.name}" in ${envLabel2} "${key}", but that storage is not public. Add \`public = true\` to the storage block.`
376255
+ });
376256
+ }
376257
+ };
376258
+ for (const service of config.services) {
376259
+ if (service.env) {
376260
+ for (const [key, value] of Object.entries(service.env)) {
376261
+ const refs = extractEnvReferences2(value);
376262
+ for (const ref of refs) {
376263
+ if (ref.type === "storage") {
376264
+ validateStorageReference("Service", service.name, key, ref);
376265
+ }
376266
+ if (service.endpoints.length > 1 && ref.type === "port") {
376267
+ errors.push({
376268
+ service: service.name,
376269
+ message: `Service "${service.name}" has multiple endpoints but uses \${port} for env var "${key}". Use \${endpoint.<name>.port} instead.`
376270
+ });
376271
+ }
376272
+ if (ref.type === "service") {
376273
+ const serviceRef = ref;
376274
+ const targetEndpoints = serviceEndpointsMap.get(serviceRef.serviceName);
376275
+ if (!targetEndpoints) {
376276
+ errors.push({
376277
+ service: service.name,
376278
+ message: `Service "${service.name}" references unknown service "${serviceRef.serviceName}" in env var "${key}".`
376279
+ });
376280
+ continue;
376281
+ }
376282
+ if (targetEndpoints.length > 1 && !serviceRef.endpointName) {
376283
+ errors.push({
376284
+ service: service.name,
376285
+ message: `Service "${service.name}" references service "${serviceRef.serviceName}" which has multiple endpoints, but no endpoint name specified in env var "${key}". Use \${service.${serviceRef.serviceName}.endpoint.<name>.${serviceRef.attribute}} instead.`
376286
+ });
376287
+ }
376288
+ if (serviceRef.endpointName) {
376289
+ const endpointExists = targetEndpoints.some((e) => e.name === serviceRef.endpointName);
376290
+ if (!endpointExists) {
376291
+ errors.push({
376292
+ service: service.name,
376293
+ message: `Service "${service.name}" references endpoint "${serviceRef.endpointName}" on service "${serviceRef.serviceName}" in env var "${key}", but that endpoint does not exist.`
376294
+ });
376295
+ }
376296
+ }
376297
+ if (serviceRef.attribute === "public_url") {
376298
+ const endpointName = serviceRef.endpointName;
376299
+ if (endpointName) {
376300
+ const targetEndpoint = targetEndpoints.find((e) => e.name === endpointName);
376301
+ if (targetEndpoint && !targetEndpoint.public) {
376302
+ errors.push({
376303
+ service: service.name,
376304
+ message: `Service "${service.name}" references public_url of endpoint "${endpointName}" on service "${serviceRef.serviceName}" in env var "${key}", but that endpoint is not public. Add \`public = true\` to the endpoint.`
376305
+ });
376306
+ }
376307
+ } else {
376308
+ const defaultEndpoint = targetEndpoints.length === 1 ? targetEndpoints[0] : targetEndpoints.find((e) => e.name === "default");
376309
+ if (defaultEndpoint && !defaultEndpoint.public) {
376310
+ errors.push({
376311
+ service: service.name,
376312
+ message: `Service "${service.name}" references public_url of service "${serviceRef.serviceName}" in env var "${key}", but its endpoint is not public. Add \`public = true\` to the endpoint.`
376313
+ });
376314
+ }
376315
+ }
376316
+ }
376317
+ }
376318
+ if (ref.type === "endpoint") {
376319
+ const endpointRef = ref;
376320
+ const endpointExists = service.endpoints.some((e) => e.name === endpointRef.endpointName);
376321
+ if (!endpointExists) {
376322
+ errors.push({
376323
+ service: service.name,
376324
+ message: `Service "${service.name}" references endpoint "${endpointRef.endpointName}" in env var "${key}", but that endpoint is not defined on this service.`
376325
+ });
376326
+ }
376327
+ }
376328
+ if (ref.type === "volume") {
376329
+ const volumeRef = ref;
376330
+ const volumeExists = service.volumes?.some((v) => v.name === volumeRef.volumeName);
376331
+ if (!volumeExists) {
376332
+ errors.push({
376333
+ service: service.name,
376334
+ message: `Service "${service.name}" references volume "${volumeRef.volumeName}" in env var "${key}", but that volume is not defined on this service.`
376335
+ });
376336
+ }
376337
+ }
376338
+ }
376339
+ }
376340
+ }
376341
+ if (service.dev?.env) {
376342
+ for (const [key, value] of Object.entries(service.dev.env)) {
376343
+ for (const ref of extractEnvReferences2(value)) {
376344
+ if (ref.type === "storage") {
376345
+ validateStorageReference("Service", service.name, key, ref, "dev env var");
376346
+ }
376347
+ }
376348
+ }
376349
+ }
376350
+ }
376351
+ for (const cron of config.crons) {
376352
+ for (const [envLabel2, env2] of [
376353
+ ["env var", cron.env],
376354
+ ["dev env var", cron.dev?.env]
376355
+ ]) {
376356
+ if (!env2)
376357
+ continue;
376358
+ for (const [key, value] of Object.entries(env2)) {
376359
+ for (const ref of extractEnvReferences2(value)) {
376360
+ if (ref.type === "storage") {
376361
+ validateStorageReference("Cron", cron.name, key, ref, envLabel2);
376362
+ }
376363
+ }
376364
+ }
376365
+ }
376366
+ }
376367
+ return errors;
376368
+ }
376369
+ function assertValidReferences(config) {
376370
+ const errors = validateEndpointReferences2(config);
376371
+ if (errors.length > 0) {
376372
+ throw new Error(errors.map((error) => error.message).join("\n"));
376373
+ }
376374
+ }
376129
376375
 
376130
376376
  // src/commands/check.tsx
376131
376377
  async function runReshapeCheck(migrationsDir) {
@@ -376161,6 +376407,7 @@ function CheckUI() {
376161
376407
  const hcl = fs22.readFileSync(configPath, "utf-8");
376162
376408
  try {
376163
376409
  const config2 = await parseConfig2(hcl);
376410
+ assertValidReferences(config2);
376164
376411
  const localPath = path20.join(process.cwd(), LOCAL_FILE);
376165
376412
  if (fs22.existsSync(localPath)) {
376166
376413
  const localContent = fs22.readFileSync(localPath, "utf-8");
@@ -376236,7 +376483,7 @@ function CheckUI() {
376236
376483
  const reshapeChecks = state.reshapeChecks ?? [];
376237
376484
  const reshapeErrors = reshapeChecks.filter((r) => !r.success);
376238
376485
  const hasReshapeErrors = reshapeErrors.length > 0;
376239
- return /* @__PURE__ */ React3.createElement(Box3, { flexDirection: "column", gap: 1 }, /* @__PURE__ */ React3.createElement(Text3, { color: hasReshapeErrors ? "yellow" : "green" }, hasReshapeErrors ? "Configuration is valid but reshape migrations have errors" : "Configuration is valid"), config.builds.length > 0 && /* @__PURE__ */ React3.createElement(Box3, { flexDirection: "column" }, /* @__PURE__ */ React3.createElement(Text3, { bold: true }, "Builds (", config.builds.length, "):"), config.builds.map((b) => /* @__PURE__ */ React3.createElement(Text3, { key: b.name }, " ", "- ", b.name, b.base ? ` (${b.base})` : "", b.dockerfile ? ` (dockerfile: ${b.dockerfile})` : ""))), config.services.length > 0 && /* @__PURE__ */ React3.createElement(Box3, { flexDirection: "column" }, /* @__PURE__ */ React3.createElement(Text3, { bold: true }, "Services (", config.services.length, "):"), config.services.map((s) => /* @__PURE__ */ React3.createElement(Text3, { key: s.name }, " ", "- ", s.name, s.build ? ` (build: ${s.build.name})` : "", s.image ? ` (image: ${s.image})` : ""))), config.crons.length > 0 && /* @__PURE__ */ React3.createElement(Box3, { flexDirection: "column" }, /* @__PURE__ */ React3.createElement(Text3, { bold: true }, "Crons (", config.crons.length, "):"), config.crons.map((c) => /* @__PURE__ */ React3.createElement(Text3, { key: c.name }, " ", "- ", c.name, " (", c.schedule, ")", c.build ? ` (build: ${c.build.name})` : "", c.image ? ` (image: ${c.image})` : ""))), config.postgres.length > 0 && /* @__PURE__ */ React3.createElement(Box3, { flexDirection: "column" }, /* @__PURE__ */ React3.createElement(Text3, { bold: true }, "Postgres (", config.postgres.length, "):"), config.postgres.map((p) => /* @__PURE__ */ React3.createElement(Text3, { key: p.name }, " ", "- ", p.name, p.reshape?.enabled ? " (reshape enabled)" : ""))), reshapeChecks.length > 0 && /* @__PURE__ */ React3.createElement(Box3, { flexDirection: "column" }, /* @__PURE__ */ React3.createElement(Text3, { bold: true }, "Reshape Migrations:"), reshapeChecks.map((r) => /* @__PURE__ */ React3.createElement(Box3, { key: r.databaseName, flexDirection: "column" }, /* @__PURE__ */ React3.createElement(Text3, null, " ", /* @__PURE__ */ React3.createElement(Text3, { color: r.success ? "green" : "red" }, r.success ? "pass" : "fail"), " ", r.databaseName, " (", r.migrationsDir, ")"), r.error && /* @__PURE__ */ React3.createElement(Text3, { color: "red" }, " ", r.error)))), config.redis.length > 0 && /* @__PURE__ */ React3.createElement(Box3, { flexDirection: "column" }, /* @__PURE__ */ React3.createElement(Text3, { bold: true }, "Redis (", config.redis.length, "):"), config.redis.map((r) => /* @__PURE__ */ React3.createElement(Text3, { key: r.name }, " ", "- ", r.name))), config.storage.length > 0 && /* @__PURE__ */ React3.createElement(Box3, { flexDirection: "column" }, /* @__PURE__ */ React3.createElement(Text3, { bold: true }, "Storage (", config.storage.length, "):"), config.storage.map((s) => /* @__PURE__ */ React3.createElement(Text3, { key: s.name }, " ", "- ", s.name))), config.configs.length > 0 && /* @__PURE__ */ React3.createElement(Box3, { flexDirection: "column" }, /* @__PURE__ */ React3.createElement(Text3, { bold: true }, "Config (", config.configs.length, "):"), config.configs.map((c) => /* @__PURE__ */ React3.createElement(Text3, { key: c.name }, " ", "- ", c.name, c.default ? ` (default: ${c.default})` : ""))), config.environments.length > 0 && /* @__PURE__ */ React3.createElement(Box3, { flexDirection: "column" }, /* @__PURE__ */ React3.createElement(Text3, { bold: true }, "Environments (", config.environments.length, "):"), config.environments.map((e) => /* @__PURE__ */ React3.createElement(Text3, { key: e.name }, " ", "- ", e.name))));
376486
+ return /* @__PURE__ */ React3.createElement(Box3, { flexDirection: "column", gap: 1 }, /* @__PURE__ */ React3.createElement(Text3, { color: hasReshapeErrors ? "yellow" : "green" }, hasReshapeErrors ? "Configuration is valid but reshape migrations have errors" : "Configuration is valid"), config.builds.length > 0 && /* @__PURE__ */ React3.createElement(Box3, { flexDirection: "column" }, /* @__PURE__ */ React3.createElement(Text3, { bold: true }, "Builds (", config.builds.length, "):"), config.builds.map((b) => /* @__PURE__ */ React3.createElement(Text3, { key: b.name }, " ", "- ", b.name, b.base ? ` (${b.base})` : "", b.dockerfile ? ` (dockerfile: ${b.dockerfile})` : ""))), config.services.length > 0 && /* @__PURE__ */ React3.createElement(Box3, { flexDirection: "column" }, /* @__PURE__ */ React3.createElement(Text3, { bold: true }, "Services (", config.services.length, "):"), config.services.map((s) => /* @__PURE__ */ React3.createElement(Text3, { key: s.name }, " ", "- ", s.name, s.build ? ` (build: ${s.build.name})` : "", s.image ? ` (image: ${s.image})` : ""))), config.crons.length > 0 && /* @__PURE__ */ React3.createElement(Box3, { flexDirection: "column" }, /* @__PURE__ */ React3.createElement(Text3, { bold: true }, "Crons (", config.crons.length, "):"), config.crons.map((c) => /* @__PURE__ */ React3.createElement(Text3, { key: c.name }, " ", "- ", c.name, " (", c.schedule, ")", c.build ? ` (build: ${c.build.name})` : "", c.image ? ` (image: ${c.image})` : ""))), config.postgres.length > 0 && /* @__PURE__ */ React3.createElement(Box3, { flexDirection: "column" }, /* @__PURE__ */ React3.createElement(Text3, { bold: true }, "Postgres (", config.postgres.length, "):"), config.postgres.map((p) => /* @__PURE__ */ React3.createElement(Text3, { key: p.name }, " ", "- ", p.name, p.reshape?.enabled ? " (reshape enabled)" : ""))), reshapeChecks.length > 0 && /* @__PURE__ */ React3.createElement(Box3, { flexDirection: "column" }, /* @__PURE__ */ React3.createElement(Text3, { bold: true }, "Reshape Migrations:"), reshapeChecks.map((r) => /* @__PURE__ */ React3.createElement(Box3, { key: r.databaseName, flexDirection: "column" }, /* @__PURE__ */ React3.createElement(Text3, null, " ", /* @__PURE__ */ React3.createElement(Text3, { color: r.success ? "green" : "red" }, r.success ? "pass" : "fail"), " ", r.databaseName, " (", r.migrationsDir, ")"), r.error && /* @__PURE__ */ React3.createElement(Text3, { color: "red" }, " ", r.error)))), config.redis.length > 0 && /* @__PURE__ */ React3.createElement(Box3, { flexDirection: "column" }, /* @__PURE__ */ React3.createElement(Text3, { bold: true }, "Redis (", config.redis.length, "):"), config.redis.map((r) => /* @__PURE__ */ React3.createElement(Text3, { key: r.name }, " ", "- ", r.name))), config.storage.length > 0 && /* @__PURE__ */ React3.createElement(Box3, { flexDirection: "column" }, /* @__PURE__ */ React3.createElement(Text3, { bold: true }, "Storage (", config.storage.length, "):"), config.storage.map((s) => /* @__PURE__ */ React3.createElement(Text3, { key: s.name }, " ", "- ", s.name, s.public ? " (public)" : ""))), config.configs.length > 0 && /* @__PURE__ */ React3.createElement(Box3, { flexDirection: "column" }, /* @__PURE__ */ React3.createElement(Text3, { bold: true }, "Config (", config.configs.length, "):"), config.configs.map((c) => /* @__PURE__ */ React3.createElement(Text3, { key: c.name }, " ", "- ", c.name, c.default ? ` (default: ${c.default})` : ""))), config.environments.length > 0 && /* @__PURE__ */ React3.createElement(Box3, { flexDirection: "column" }, /* @__PURE__ */ React3.createElement(Text3, { bold: true }, "Environments (", config.environments.length, "):"), config.environments.map((e) => /* @__PURE__ */ React3.createElement(Text3, { key: e.name }, " ", "- ", e.name))));
376240
376487
  }
376241
376488
  function checkCommand() {
376242
376489
  render3(/* @__PURE__ */ React3.createElement(CheckUI, null));
@@ -378528,7 +378775,13 @@ async function execCommand(serviceName, command, instanceKey = "default", option
378528
378775
  endpoint: dbState.endpoint,
378529
378776
  accessKey: dbState.accessKey,
378530
378777
  secretKey: dbState.secretKey,
378531
- bucket: dbState.engine === "object_store" ? name : void 0,
378778
+ // State written by older SDK versions predates the bucket field;
378779
+ // object-store buckets were always named after the resource.
378780
+ bucket: dbState.bucket ?? (dbState.engine === "object_store" ? name : void 0),
378781
+ // Same legacy-state fallback for publicUrl: an older dev daemon
378782
+ // never records it, but the local URL is derivable from the
378783
+ // endpoint when the config marks the storage public.
378784
+ publicUrl: dbState.publicUrl ?? (dbState.engine === "object_store" && dbState.endpoint && config.storage.find((s) => s.name === name)?.public === true ? `${dbState.endpoint}/${encodeURIComponent(name)}` : void 0),
378532
378785
  syncUrl: dbState.syncUrl,
378533
378786
  syncSecret: dbState.syncSecret,
378534
378787
  reshapeEnabled: dbState.reshapeEnabled,
@@ -379301,7 +379554,7 @@ function compareVersions(a, b) {
379301
379554
  return 0;
379302
379555
  }
379303
379556
  async function checkForUpdate() {
379304
- const currentVersion = "0.1.186";
379557
+ const currentVersion = "0.1.187";
379305
379558
  const response = await fetch(`${BINARIES_BASE_URL}/latest?t=${Date.now()}`);
379306
379559
  if (!response.ok) {
379307
379560
  throw new Error(`Failed to check for updates: HTTP ${response.status}`);
@@ -380218,7 +380471,7 @@ async function domainAddCommand(service, domain, options2) {
380218
380471
  var program = new Command();
380219
380472
  var env = "production";
380220
380473
  var envLabel = env !== "production" ? `[${env.toUpperCase()}] ` : "";
380221
- program.name("specific").description(`${envLabel}Infrastructure-as-code for coding agents`).version("0.1.186").enablePositionalOptions();
380474
+ program.name("specific").description(`${envLabel}Infrastructure-as-code for coding agents`).version("0.1.187").enablePositionalOptions();
380222
380475
  program.command("init").description("Initialize project for use with a coding agent").option("--agent <name...>", "Agents to configure (cursor, claude, codex, other)").addHelpText("after", `
380223
380476
  Examples:
380224
380477
  $ specific init
@@ -36,6 +36,36 @@ storage "uploads" {}
36
36
  - `access_key` - Access key for authentication
37
37
  - `secret_key` - Secret key for authentication
38
38
  - `bucket` - Bucket name
39
+ - `public_url` - HTTPS origin for anonymous reads (public buckets only)
40
+
41
+ ## Public buckets
42
+
43
+ Public access is opt-in:
44
+
45
+ ```hcl
46
+ storage "assets" {
47
+ public = true
48
+ }
49
+
50
+ service "web" {
51
+ build = build.web
52
+
53
+ env = {
54
+ ASSETS_PUBLIC_URL = storage.assets.public_url
55
+ }
56
+ }
57
+ ```
58
+
59
+ `public_url` has no trailing slash. Append an object key to it, for example
60
+ `${ASSETS_PUBLIC_URL}/images/logo.png`.
61
+
62
+ Anonymous users can read objects, but uploads and deletes still require
63
+ credentials. Directory listing is disabled. CORS, cache headers, and custom
64
+ domains must be configured separately. Removing `public = true` makes the
65
+ bucket private again on the next deployment.
66
+
67
+ Preview buckets contain a fork of their parent bucket. Making one public can
68
+ expose copied parent data.
39
69
 
40
70
  ## Production management
41
71
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@specific.dev/cli",
3
- "version": "0.1.186",
3
+ "version": "0.1.187",
4
4
  "description": "CLI for Specific infrastructure-as-code",
5
5
  "type": "module",
6
6
  "main": "dist/cli.js",