vibecarbon 0.1.7 → 0.2.0

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 (102) hide show
  1. package/carbon/.env.example +3 -2
  2. package/carbon/DEVELOPMENT.md +18 -3
  3. package/carbon/PRODUCTION.md +1 -1
  4. package/carbon/content/docs/cli.mdx +3 -3
  5. package/carbon/content/docs/deployment.mdx +1 -1
  6. package/carbon/content/docs/development.mdx +1 -1
  7. package/carbon/content/docs/getting-started.mdx +0 -1
  8. package/carbon/scripts/dev-init.js +1 -1
  9. package/carbon/scripts/generate-dev-configs.sh +5 -7
  10. package/carbon/scripts/validate-dev-configs.sh +5 -7
  11. package/carbon/src/client/components/TechStackSection.tsx +0 -68
  12. package/carbon/src/client/index.css +0 -14
  13. package/carbon/src/client/locales/de.json +0 -18
  14. package/carbon/src/client/locales/en.json +86 -36
  15. package/carbon/src/client/locales/es.json +0 -18
  16. package/carbon/src/client/locales/fr.json +0 -18
  17. package/carbon/src/client/locales/pt.json +0 -18
  18. package/carbon/src/client/pages/Home.tsx +0 -3
  19. package/package.json +4 -2
  20. package/services/index.json +29 -0
  21. package/services/metabase/compose/docker-compose.override.yml +14 -0
  22. package/services/metabase/compose/docker-compose.prod.yml +29 -0
  23. package/services/metabase/compose/docker-compose.yml +68 -0
  24. package/services/metabase/db/init.sh +29 -0
  25. package/services/metabase/docs/integration.md +191 -0
  26. package/services/metabase/k8s/deployment.yaml +88 -0
  27. package/services/metabase/k8s/ingressroute.yaml +22 -0
  28. package/services/metabase/k8s/kustomization.yaml +9 -0
  29. package/services/metabase/k8s/network-policy.yaml +74 -0
  30. package/services/metabase/k8s/pvc.yaml +12 -0
  31. package/services/metabase/k8s/service.yaml +15 -0
  32. package/services/metabase/manifest.json +34 -0
  33. package/services/metabase/scripts/setup.sh +152 -0
  34. package/services/n8n/compose/docker-compose.override.yml +14 -0
  35. package/services/n8n/compose/docker-compose.prod.yml +32 -0
  36. package/services/n8n/compose/docker-compose.yml +99 -0
  37. package/services/n8n/db/init.sh +25 -0
  38. package/services/n8n/docs/SSO.md +124 -0
  39. package/services/n8n/docs/integration.md +155 -0
  40. package/services/n8n/hooks/hooks.js +131 -0
  41. package/services/n8n/k8s/deployment.yaml +116 -0
  42. package/services/n8n/k8s/ingressroute.yaml +24 -0
  43. package/services/n8n/k8s/kustomization.yaml +9 -0
  44. package/services/n8n/k8s/network-policy.yaml +82 -0
  45. package/services/n8n/k8s/pvc.yaml +14 -0
  46. package/services/n8n/k8s/service.yaml +14 -0
  47. package/services/n8n/manifest.json +36 -0
  48. package/services/n8n/scripts/setup.sh +42 -0
  49. package/{carbon/docker-compose.observability.prod.yml → services/observability/compose/docker-compose.prod.yml} +8 -0
  50. package/services/observability/docs/integration.md +31 -0
  51. package/services/observability/k8s/grafana-deployment.yaml +107 -0
  52. package/services/observability/k8s/grafana-provisioning-configmap.yaml +65 -0
  53. package/services/observability/k8s/grafana-pvc.yaml +14 -0
  54. package/services/observability/k8s/grafana-service.yaml +16 -0
  55. package/services/observability/k8s/ingressroute.yaml +22 -0
  56. package/services/observability/k8s/kustomization.yaml +24 -0
  57. package/services/observability/k8s/loki-configmap.yaml +74 -0
  58. package/services/observability/k8s/loki-deployment.yaml +72 -0
  59. package/services/observability/k8s/loki-pvc.yaml +14 -0
  60. package/services/observability/k8s/loki-service.yaml +16 -0
  61. package/services/observability/k8s/network-policy.yaml +141 -0
  62. package/services/observability/k8s/prometheus-configmap.yaml +80 -0
  63. package/services/observability/k8s/prometheus-deployment.yaml +78 -0
  64. package/services/observability/k8s/prometheus-pvc.yaml +14 -0
  65. package/services/observability/k8s/prometheus-rbac.yaml +55 -0
  66. package/services/observability/k8s/prometheus-service.yaml +16 -0
  67. package/services/observability/manifest.json +28 -0
  68. package/services/redis/compose/docker-compose.prod.yml +10 -0
  69. package/services/redis/compose/docker-compose.yml +24 -0
  70. package/services/redis/docs/integration.md +148 -0
  71. package/services/redis/k8s/deployment.yaml +70 -0
  72. package/services/redis/k8s/kustomization.yaml +8 -0
  73. package/services/redis/k8s/network-policy.yaml +20 -0
  74. package/services/redis/k8s/pvc.yaml +12 -0
  75. package/services/redis/k8s/service.yaml +15 -0
  76. package/services/redis/manifest.json +30 -0
  77. package/services/s3/compose/docker-compose.s3-ha.yml +86 -0
  78. package/services/s3/compose/docker-compose.s3.yml +35 -0
  79. package/services/s3/manifest.json +87 -0
  80. package/src/add.js +57 -89
  81. package/src/cli.js +1 -1
  82. package/src/create.js +3 -87
  83. package/src/lib/deploy/bundle.js +4 -2
  84. package/src/lib/deploy/compose/index.js +45 -17
  85. package/src/lib/deploy/orchestrator.js +13 -1
  86. package/src/remove.js +9 -6
  87. package/carbon/content/docs/optional-services.mdx +0 -160
  88. package/carbon/src/client/components/MetricsStrip.tsx +0 -48
  89. /package/{carbon/docker-compose.observability.override.yml → services/observability/compose/docker-compose.override.yml} +0 -0
  90. /package/{carbon/docker-compose.observability.yml → services/observability/compose/docker-compose.yml} +0 -0
  91. /package/{carbon → services/observability}/volumes/grafana/dashboards/logs.json +0 -0
  92. /package/{carbon → services/observability}/volumes/grafana/dashboards/overview.json +0 -0
  93. /package/{carbon → services/observability}/volumes/grafana/dashboards/postgresql.json +0 -0
  94. /package/{carbon → services/observability}/volumes/grafana/dashboards.dev/logs.json +0 -0
  95. /package/{carbon → services/observability}/volumes/grafana/dashboards.dev/overview.json +0 -0
  96. /package/{carbon → services/observability}/volumes/grafana/dashboards.dev/postgresql.json +0 -0
  97. /package/{carbon → services/observability}/volumes/grafana/provisioning/dashboards/dashboards.dev.yml +0 -0
  98. /package/{carbon → services/observability}/volumes/grafana/provisioning/dashboards/dashboards.yml +0 -0
  99. /package/{carbon → services/observability}/volumes/grafana/provisioning/datasources/datasources.yml +0 -0
  100. /package/{carbon → services/observability}/volumes/loki/loki-config.yml +0 -0
  101. /package/{carbon → services/observability}/volumes/prometheus/prometheus.yml +0 -0
  102. /package/{carbon → services/observability}/volumes/promtail/promtail-config.yml +0 -0
package/src/add.js CHANGED
@@ -1,10 +1,13 @@
1
1
  /**
2
2
  * Vibecarbon Add Command
3
3
  *
4
- * Adds optional services to an existing Vibecarbon project. Services
5
- * are fetched from GitHub at runtime so users always get the latest
6
- * templates regardless of their CLI version. -offline falls back to
7
- * bundled templates for air-gapped environments.
4
+ * Adds optional services to an existing Vibecarbon project. Service
5
+ * bundles ship with the CLI and are installed from the packaged
6
+ * `services/` directory by default the same way `create` scaffolds
7
+ * from the packaged `carbon/` template, so an `add` is version-pinned
8
+ * and works offline. `-online` opts into fetching the latest bundles
9
+ * from GitHub (single-file assets only; directory assets such as `k8s/`
10
+ * still come from the packaged copy).
8
11
  *
9
12
  * Interactive-by-default: bare `vibecarbon add` prompts for a feature
10
13
  * to add. Positional features (`vibecarbon add observability redis`)
@@ -53,7 +56,9 @@ const GITHUB_BRANCH = 'main';
53
56
  const GITHUB_RAW_BASE = `https://raw.githubusercontent.com/${GITHUB_REPO}/${GITHUB_BRANCH}`;
54
57
  const SERVICES_PATH = 'services';
55
58
 
56
- // Local services directory (for --offline mode and development)
59
+ // Packaged services directory the default install source (shipped in the
60
+ // npm package `files`, alongside `carbon/`). `-online` overrides this to fetch
61
+ // from GitHub.
57
62
  const LOCAL_SERVICES_DIR = join(__dirname, '..', SERVICES_PATH);
58
63
 
59
64
  // Template variable placeholders
@@ -92,7 +97,7 @@ function buildSpec() {
92
97
  summary: 'Add a feature to a Vibecarbon project',
93
98
  description: [
94
99
  'Available features:',
95
- ...LOCAL_FEATURES.map((f) => ` ${f.name.padEnd(16)} ${f.description}`),
100
+ ...AVAILABLE_FEATURES.map((f) => ` ${f.name.padEnd(16)} ${f.description}`),
96
101
  '',
97
102
  'External services (CI/CD, Stripe, OAuth, etc.) live in `vibecarbon configure`.',
98
103
  ].join('\n'),
@@ -109,9 +114,9 @@ function buildSpec() {
109
114
  { name: 'v', boolean: true, description: 'Show version' },
110
115
  { name: 'y', boolean: true, description: 'Skip confirmation prompts' },
111
116
  {
112
- name: 'offline',
117
+ name: 'online',
113
118
  boolean: true,
114
- description: 'Use bundled templates (for air-gapped environments)',
119
+ description: 'Fetch the latest bundles from GitHub instead of the packaged copy',
115
120
  },
116
121
  ],
117
122
  examples: [
@@ -227,9 +232,13 @@ async function installServiceDirectory(serviceName, srcDir, destDir, variables,
227
232
  }
228
233
  }
229
234
  } else {
230
- // For online mode, the manifest should list all files
231
- // We'll handle this in the main install function
232
- throw new Error('Directory installation requires file listing in manifest');
235
+ // Online mode can't enumerate a remote directory (GitHub raw has no
236
+ // listing and we don't hit the contents API). Directory assets such as
237
+ // `k8s/` therefore only install from the packaged copy.
238
+ throw new Error(
239
+ `Cannot fetch directory "${srcDir}" over -online (remote directory listing is not supported). ` +
240
+ 'Drop -online to install this feature from the packaged template.',
241
+ );
233
242
  }
234
243
  }
235
244
 
@@ -290,88 +299,27 @@ function removeFromBaseKustomization(kustomizationEntry) {
290
299
  writeFileSync(kustomizationPath, content);
291
300
  }
292
301
 
293
- // ============================================================================
294
- // SPECIALIZED FEATURE HANDLERS
295
- // ============================================================================
296
-
297
- // Features with specialized handlers (not manifest-based). CI/CD used to be
298
- // here too; it moved to `vibecarbon configure` (CI/CD is an external service,
299
- // not a local container, so it lives with the other configure features).
300
- const SPECIALIZED_FEATURES = ['observability'];
301
-
302
- async function handleSpecializedFeature(featureName, options) {
303
- const { project } = options;
304
-
305
- switch (featureName) {
306
- case 'observability': {
307
- return handleObservabilityFeature(project);
308
- }
309
-
310
- default:
311
- throw new Error(`Unknown specialized feature: ${featureName}`);
312
- }
313
- }
314
-
315
- /**
316
- * Enable observability stack (Prometheus, Grafana, Loki)
317
- * Files are already included in the project - this just enables them
318
- */
319
- async function handleObservabilityFeature(_project) {
320
- const manifest = loadManifest();
321
-
322
- // Check if already enabled
323
- if (manifest.services?.observability) {
324
- p.log.info('Observability is already enabled.');
325
- return true;
326
- }
327
-
328
- // Check that the observability compose file exists
329
- const observabilityCompose = join(process.cwd(), 'docker-compose.observability.yml');
330
- if (!existsSync(observabilityCompose)) {
331
- p.log.error('Observability files not found. Please ensure your project is up to date.');
332
- return false;
333
- }
334
-
335
- const s = p.spinner();
336
- s.start('Enabling observability stack');
337
-
338
- // Set env flags for dashboard visibility (client + server)
339
- setEnvVar('VITE_OBSERVABILITY_ENABLED', 'true');
340
- setEnvVar('OBSERVABILITY_ENABLED', 'true');
341
-
342
- // Update manifest
343
- if (!manifest.services) manifest.services = {};
344
- manifest.services.observability = { addedAt: new Date().toISOString() };
345
- saveManifest(manifest);
346
-
347
- s.stop('Observability enabled');
348
-
349
- p.note(
350
- [
351
- 'Observability stack enabled:',
352
- ` ${c.info('Prometheus')} http://prometheus.localhost`,
353
- ` ${c.info('Grafana')} http://grafana.localhost`,
354
- ` ${c.info('Loki')} http://loki.localhost`,
355
- ].join('\n'),
356
- 'Observability',
357
- );
358
-
359
- return true;
360
- }
361
-
362
302
  // ============================================================================
363
303
  // ADD SERVICE
364
304
  // ============================================================================
365
305
 
366
306
  async function addService(serviceName, options) {
367
307
  const { project } = options;
368
- let { offline } = options;
369
308
 
370
- // Check if this is a specialized feature with a custom handler
371
- if (SPECIALIZED_FEATURES.includes(serviceName)) {
372
- return handleSpecializedFeature(serviceName, options);
309
+ // Parked features ship in `services/` but are turned off for this release.
310
+ // Refuse here — before any manifest fetch — so `add <parked>` can't install
311
+ // something we've hidden from the offering. `remove` is intentionally not
312
+ // guarded, so an existing install can still be cleaned up.
313
+ if (isParked(serviceName)) {
314
+ p.log.warn(`"${serviceName}" is not available in this release.`);
315
+ return false;
373
316
  }
374
317
 
318
+ // Default install source is the packaged `services/` dir (mirrors how
319
+ // `create` uses the packaged `carbon/` template). `-online` opts into the
320
+ // GitHub fetch; on network failure we fall back to the packaged copy below.
321
+ let offline = !options.online;
322
+
375
323
  let manifest;
376
324
  try {
377
325
  manifest = await fetchServiceManifest(serviceName, offline);
@@ -528,11 +476,18 @@ async function addService(serviceName, options) {
528
476
  // LIST SERVICES
529
477
  // ============================================================================
530
478
 
531
- // Features that work locally without external dependencies
479
+ // Features that work locally without external dependencies.
480
+ //
481
+ // `status: 'parked'` turns a feature off for this release: it's hidden from the
482
+ // offering (help body + interactive prompt) and `add` refuses to install it,
483
+ // but its packaged bundle under `services/` stays in place so un-parking is a
484
+ // one-line flip. Existing installs can still be `vibecarbon remove`d. Broad
485
+ // add-on discovery will move to a marketplace later — this list is not the
486
+ // permanent catalog.
532
487
  const LOCAL_FEATURES = [
533
488
  { name: 'observability', description: 'Prometheus, Grafana, Loki monitoring stack' },
534
- { name: 'n8n', description: 'Workflow automation platform' },
535
- { name: 'metabase', description: 'Business intelligence and analytics' },
489
+ { name: 'n8n', description: 'Workflow automation platform', status: 'parked' },
490
+ { name: 'metabase', description: 'Business intelligence and analytics', status: 'parked' },
536
491
  { name: 'redis', description: 'In-memory cache and session store' },
537
492
  ];
538
493
 
@@ -542,6 +497,19 @@ const LOCAL_FEATURES = [
542
497
 
543
498
  const ALL_FEATURES = [...LOCAL_FEATURES];
544
499
 
500
+ // Features actually offered to users. Parked features are excluded from the
501
+ // help body and the interactive prompt, but remain in ALL_FEATURES so typo
502
+ // suggestions and the parked guard can still resolve their names.
503
+ const AVAILABLE_FEATURES = LOCAL_FEATURES.filter((f) => f.status !== 'parked');
504
+
505
+ /**
506
+ * Whether a feature ships but is turned off for this release.
507
+ * @param {string} name
508
+ */
509
+ function isParked(name) {
510
+ return LOCAL_FEATURES.some((f) => f.name === name && f.status === 'parked');
511
+ }
512
+
545
513
  /**
546
514
  * Calculate Levenshtein distance between two strings
547
515
  */
@@ -652,7 +620,7 @@ async function main(cliArgs) {
652
620
  if (services.length === 0) {
653
621
  const choice = await p.select({
654
622
  message: 'Which feature do you want to add?',
655
- options: LOCAL_FEATURES.map((f) => ({
623
+ options: AVAILABLE_FEATURES.map((f) => ({
656
624
  value: f.name,
657
625
  label: f.name,
658
626
  hint: f.description,
@@ -668,7 +636,7 @@ async function main(cliArgs) {
668
636
  let success = true;
669
637
  for (const serviceName of services) {
670
638
  const result = await addService(serviceName, {
671
- offline: !!values.offline,
639
+ online: !!values.online,
672
640
  project,
673
641
  cliArgs: cliArgs, // Pass raw CLI args for infrastructure services
674
642
  });
package/src/cli.js CHANGED
@@ -95,7 +95,7 @@ ${c.bold('DEV COMMANDS')}
95
95
  ${c.info('down')} Stop local development environment
96
96
  ${c.info('status')} Show project and deployment status
97
97
  ${c.info('reset')} Reset local environment (removes all data)
98
- ${c.info('add')} [feature] Add features (cicd, observability, n8n, metabase, redis)
98
+ ${c.info('add')} [feature] Add features (observability, redis)
99
99
  ${c.info('remove')} [feature] Remove features from a project
100
100
  ${c.info('configure')} Configure external services (billing, OAuth, SMTP, etc.)
101
101
  ${c.info('upgrade')} Upgrade infrastructure files to latest template
package/src/create.js CHANGED
@@ -188,8 +188,8 @@ const SPEC = {
188
188
  description: 'non-interactive (CI/CD)',
189
189
  },
190
190
  {
191
- command: 'vibecarbon create my-saas -git -install',
192
- description: 'create + initialize git + run pm install',
191
+ command: 'vibecarbon create my-saas -install',
192
+ description: 'create + run pm install (git is initialized automatically)',
193
193
  },
194
194
  ],
195
195
  };
@@ -697,13 +697,6 @@ async function bootstrap(cliArgs) {
697
697
  'volumes/kong',
698
698
  'volumes/db',
699
699
  'volumes/traefik',
700
- 'volumes/prometheus',
701
- 'volumes/loki',
702
- 'volumes/promtail',
703
- 'volumes/grafana/provisioning/dashboards',
704
- 'volumes/grafana/provisioning/datasources',
705
- 'volumes/grafana/dashboards',
706
- 'volumes/grafana/dashboards.dev',
707
700
  '.github/workflows',
708
701
  'k8s/base/app',
709
702
  'k8s/base/backup',
@@ -758,21 +751,11 @@ async function bootstrap(cliArgs) {
758
751
  copyTemplate('.dockerignore', join(projectDir, '.dockerignore'), variables);
759
752
  copyTemplate('docker-compose.yml', join(projectDir, 'docker-compose.yml'), variables);
760
753
  copyTemplate('docker-compose.prod.yml', join(projectDir, 'docker-compose.prod.yml'), variables);
761
- copyTemplate(
762
- 'docker-compose.observability.yml',
763
- join(projectDir, 'docker-compose.observability.yml'),
764
- variables,
765
- );
766
754
  copyTemplate(
767
755
  'docker-compose.override.yml',
768
756
  join(projectDir, 'docker-compose.override.yml'),
769
757
  variables,
770
758
  );
771
- copyTemplate(
772
- 'docker-compose.observability.override.yml',
773
- join(projectDir, 'docker-compose.observability.override.yml'),
774
- variables,
775
- );
776
759
  copyTemplate('PRODUCTION.md', join(projectDir, 'PRODUCTION.md'), variables);
777
760
  copyTemplate('DEVELOPMENT.md', join(projectDir, 'DEVELOPMENT.md'), variables);
778
761
  copyTemplate(
@@ -780,11 +763,6 @@ async function bootstrap(cliArgs) {
780
763
  join(projectDir, 'docker-compose.dev-init.yml'),
781
764
  variables,
782
765
  );
783
- copyTemplate(
784
- 'docker-compose.observability.prod.yml',
785
- join(projectDir, 'docker-compose.observability.prod.yml'),
786
- variables,
787
- );
788
766
  chmodSync(join(projectDir, 'docker-entrypoint.sh'), 0o755);
789
767
 
790
768
  await tick();
@@ -830,68 +808,6 @@ async function bootstrap(cliArgs) {
830
808
  variables,
831
809
  );
832
810
 
833
- // Observability config files (used by deploy --observability)
834
- copyTemplate(
835
- 'volumes/prometheus/prometheus.yml',
836
- join(projectDir, 'volumes/prometheus/prometheus.yml'),
837
- variables,
838
- );
839
- copyTemplate(
840
- 'volumes/loki/loki-config.yml',
841
- join(projectDir, 'volumes/loki/loki-config.yml'),
842
- variables,
843
- );
844
- copyTemplate(
845
- 'volumes/promtail/promtail-config.yml',
846
- join(projectDir, 'volumes/promtail/promtail-config.yml'),
847
- variables,
848
- );
849
- copyTemplate(
850
- 'volumes/grafana/provisioning/dashboards/dashboards.yml',
851
- join(projectDir, 'volumes/grafana/provisioning/dashboards/dashboards.yml'),
852
- variables,
853
- );
854
- copyTemplate(
855
- 'volumes/grafana/provisioning/dashboards/dashboards.dev.yml',
856
- join(projectDir, 'volumes/grafana/provisioning/dashboards/dashboards.dev.yml'),
857
- variables,
858
- );
859
- copyTemplate(
860
- 'volumes/grafana/provisioning/datasources/datasources.yml',
861
- join(projectDir, 'volumes/grafana/provisioning/datasources/datasources.yml'),
862
- variables,
863
- );
864
- copyTemplate(
865
- 'volumes/grafana/dashboards/logs.json',
866
- join(projectDir, 'volumes/grafana/dashboards/logs.json'),
867
- variables,
868
- );
869
- copyTemplate(
870
- 'volumes/grafana/dashboards/overview.json',
871
- join(projectDir, 'volumes/grafana/dashboards/overview.json'),
872
- variables,
873
- );
874
- copyTemplate(
875
- 'volumes/grafana/dashboards/postgresql.json',
876
- join(projectDir, 'volumes/grafana/dashboards/postgresql.json'),
877
- variables,
878
- );
879
- copyTemplate(
880
- 'volumes/grafana/dashboards.dev/logs.json',
881
- join(projectDir, 'volumes/grafana/dashboards.dev/logs.json'),
882
- variables,
883
- );
884
- copyTemplate(
885
- 'volumes/grafana/dashboards.dev/overview.json',
886
- join(projectDir, 'volumes/grafana/dashboards.dev/overview.json'),
887
- variables,
888
- );
889
- copyTemplate(
890
- 'volumes/grafana/dashboards.dev/postgresql.json',
891
- join(projectDir, 'volumes/grafana/dashboards.dev/postgresql.json'),
892
- variables,
893
- );
894
-
895
811
  copyTemplate('backup/backup.sh', join(projectDir, 'backup/backup.sh'), variables);
896
812
  copyTemplate('backup/compose-backup.sh', join(projectDir, 'backup/compose-backup.sh'), variables);
897
813
  copyTemplate('backup/Dockerfile', join(projectDir, 'backup/Dockerfile'), variables);
@@ -1180,7 +1096,7 @@ fi
1180
1096
  # Runs lint + unit + component + integration tests.
1181
1097
  #
1182
1098
  # No-ops gracefully if node_modules isn't present yet — a fresh
1183
- # \`vibecarbon create -git\` lands you on a commit that's pushable
1099
+ # \`vibecarbon create\` lands you on a commit that's pushable
1184
1100
  # before you've run \`pnpm install\`. Once deps land the hook gates
1185
1101
  # every subsequent push. Bypass at will with \`git push --no-verify\`.
1186
1102
 
@@ -96,7 +96,9 @@ function composeFileFlags(options = {}) {
96
96
  if (options.metabase) {
97
97
  files.push('-f', 'docker-compose.metabase.yml', '-f', 'docker-compose.metabase.prod.yml');
98
98
  }
99
- if (options.redis) files.push('-f', 'docker-compose.redis.yml');
99
+ if (options.redis) {
100
+ files.push('-f', 'docker-compose.redis.yml', '-f', 'docker-compose.redis.prod.yml');
101
+ }
100
102
  return files.join(' ');
101
103
  }
102
104
 
@@ -126,7 +128,7 @@ export function renderBundle(projectName, options = {}) {
126
128
  if (options.metabase) {
127
129
  rootFiles.push('docker-compose.metabase.yml', 'docker-compose.metabase.prod.yml');
128
130
  }
129
- if (options.redis) rootFiles.push('docker-compose.redis.yml');
131
+ if (options.redis) rootFiles.push('docker-compose.redis.yml', 'docker-compose.redis.prod.yml');
130
132
 
131
133
  for (const file of rootFiles) {
132
134
  const src = join(cwd, file);
@@ -493,7 +493,9 @@ function composeFileFlags(options = {}) {
493
493
  if (options.metabase) {
494
494
  files.push('-f', 'docker-compose.metabase.yml', '-f', 'docker-compose.metabase.prod.yml');
495
495
  }
496
- if (options.redis) files.push('-f', 'docker-compose.redis.yml');
496
+ if (options.redis) {
497
+ files.push('-f', 'docker-compose.redis.yml', '-f', 'docker-compose.redis.prod.yml');
498
+ }
497
499
  return files.join(' ');
498
500
  }
499
501
 
@@ -756,6 +758,27 @@ export async function runMigrations(ip, sshKeyPath, projectName) {
756
758
  { timeout: 240_000 },
757
759
  );
758
760
 
761
+ // Also wait for the storage service to finish ITS first-boot migrations
762
+ // before applying app migrations. supabase-storage adds columns to the
763
+ // `storage` schema on startup (notably storage.buckets.public); our
764
+ // 00001_init.sql seeds buckets referencing them. Racing ahead fails with
765
+ // `column "public" of relation "buckets" does not exist`, which aborts the
766
+ // whole migration — the core public.* tables get created first, but the
767
+ // deploy still fails (deployComposeHA only ever masked this by swallowing the
768
+ // error in a try/catch). The supabase_admin gate above is insufficient: that
769
+ // role exists before storage has migrated. Gate on storage.buckets.public
770
+ // being queryable as the proxy for "storage migrations done", polling ~3 min.
771
+ await sshRunAsync(
772
+ ip,
773
+ sshKeyPath,
774
+ `cd ${remoteDir} && ` +
775
+ `for i in $(seq 1 60); do ` +
776
+ `docker compose exec -T db psql -U supabase_admin -d postgres -c 'SELECT public FROM storage.buckets LIMIT 0' >/dev/null 2>&1 && exit 0; ` +
777
+ `echo "[migrate] waiting for storage schema (buckets.public) to be ready (attempt $i/60)"; sleep 3; ` +
778
+ `done; echo "[migrate] storage schema never became ready" >&2; exit 1`,
779
+ { timeout: 240_000 },
780
+ );
781
+
759
782
  // Apply each migration with ON_ERROR_STOP=1 and abort on the first failure.
760
783
  // A failed migration MUST fail the deploy — silently shipping an empty schema
761
784
  // is far worse than a deploy the operator can see failed and retry. Errors
@@ -771,6 +794,25 @@ export async function runMigrations(ip, sshKeyPath, projectName) {
771
794
  `done`,
772
795
  { timeout: 300_000 },
773
796
  );
797
+
798
+ // Reload PostgREST schema cache so it sees newly created tables. The old
799
+ // `docker compose exec rest kill -s SIGUSR1 1` never worked — the
800
+ // postgrest/postgrest image has no `kill` binary ("exec: kill: executable
801
+ // file not found in $PATH"), so the reload silently no-op'd and fresh tables
802
+ // stayed invisible (PGRST205) until rest happened to restart. RCA prod-1
803
+ // 2026-05-26. Use the canonical SQL NOTIFY on the pgrst channel instead
804
+ // (db-channel-enabled defaults on); fall back to restarting rest if NOTIFY
805
+ // doesn't land. Best-effort: a missed reload self-heals on the next deploy.
806
+ // Lives here (not just in deployCompose) so every caller — deployCompose,
807
+ // deployComposeHA, and the orchestrator's inline single-compose path — gets
808
+ // a queryable schema instead of PGRST205.
809
+ await sshRunAsync(
810
+ ip,
811
+ sshKeyPath,
812
+ `cd ${remoteDir} && ` +
813
+ `docker compose exec -T db psql -U postgres -d postgres -c "NOTIFY pgrst, 'reload schema'" 2>/dev/null || ` +
814
+ `docker compose restart rest 2>/dev/null || true`,
815
+ );
774
816
  }
775
817
 
776
818
  /**
@@ -837,23 +879,9 @@ export async function deployCompose(options) {
837
879
  // swallowing it (RCA prod-1 2026-05-26: the previous blanket try/catch hid a
838
880
  // fully-failed migration and shipped a schema-less prod that reported success).
839
881
  onProgress('Running database migrations...');
840
- const remoteDir = `/opt/${projectName}`;
882
+ // runMigrations applies supabase/migrations/* AND reloads PostgREST's schema
883
+ // cache, so the new tables are immediately queryable (no PGRST205).
841
884
  await runMigrations(ip, sshKeyPath, projectName);
842
- // Reload PostgREST schema cache so it sees newly created tables. The old
843
- // `docker compose exec rest kill -s SIGUSR1 1` never worked — the
844
- // postgrest/postgrest image has no `kill` binary ("exec: kill: executable
845
- // file not found in $PATH"), so the reload silently no-op'd and fresh tables
846
- // stayed invisible (PGRST205) until rest happened to restart. RCA prod-1
847
- // 2026-05-26. Use the canonical SQL NOTIFY on the pgrst channel instead
848
- // (db-channel-enabled defaults on); fall back to restarting rest if NOTIFY
849
- // doesn't land. Best-effort: a missed reload self-heals on the next deploy.
850
- await sshRunAsync(
851
- ip,
852
- sshKeyPath,
853
- `cd ${remoteDir} && ` +
854
- `docker compose exec -T db psql -U postgres -d postgres -c "NOTIFY pgrst, 'reload schema'" 2>/dev/null || ` +
855
- `docker compose restart rest 2>/dev/null || true`,
856
- );
857
885
 
858
886
  return {
859
887
  success: true,
@@ -542,7 +542,7 @@ export async function executeDeployment(args, gatheredConfig) {
542
542
  );
543
543
  s.stop('Compose HA deployment complete');
544
544
  } else {
545
- const { setupServer, dockerLoginOnServer, startComposeStack } = await import(
545
+ const { setupServer, dockerLoginOnServer, startComposeStack, runMigrations } = await import(
546
546
  './compose/index.js'
547
547
  );
548
548
  const { setupServerFiles, waitForSSH: waitForComposeSSH } = await import(
@@ -782,6 +782,18 @@ export async function executeDeployment(args, gatheredConfig) {
782
782
  startComposeStack(serverIp, sshKeyPath, projectConfig.projectName, services),
783
783
  );
784
784
 
785
+ // Apply app migrations + reload PostgREST. The orchestrator's inline
786
+ // single-compose path previously skipped this (only deployComposeHA ran
787
+ // runMigrations), so compose-single shipped an EMPTY app schema — 0 public
788
+ // tables, every DB-backed feature 500'd, and db_schema verify failed with
789
+ // PGRST205. runMigrations waits for supabase_admin, applies each
790
+ // supabase/migrations/* with ON_ERROR_STOP=1 (a real failure aborts the
791
+ // deploy rather than silently shipping a schema-less prod), then reloads
792
+ // PostgREST's schema cache. Mirrors the HA path.
793
+ await perfAsync('deploy.compose.migrations', () =>
794
+ runMigrations(serverIp, sshKeyPath, projectConfig.projectName),
795
+ );
796
+
785
797
  // --- Verifiable success gate ---
786
798
  // Probe the app's own /api/health endpoint on the server itself (via
787
799
  // localhost + Host header), bypassing DNS/TLS. A successful deploy
package/src/remove.js CHANGED
@@ -49,15 +49,15 @@ const SPEC = {
49
49
  { name: 'y', boolean: true, description: 'Skip confirmation prompts' },
50
50
  { name: 'force', boolean: true, description: 'Skip confirmation prompt (alias for -y)' },
51
51
  {
52
- name: 'offline',
52
+ name: 'online',
53
53
  boolean: true,
54
- description: 'Use bundled service definitions (for air-gapped environments)',
54
+ description: 'Fetch the latest service definitions from GitHub instead of the packaged copy',
55
55
  },
56
56
  ],
57
57
  examples: [
58
58
  { command: 'vibecarbon remove', description: 'prompts for a feature to remove' },
59
- { command: 'vibecarbon remove n8n', description: 'remove a specific feature' },
60
- { command: 'vibecarbon remove n8n -force', description: 'remove without confirmation' },
59
+ { command: 'vibecarbon remove redis', description: 'remove a specific feature' },
60
+ { command: 'vibecarbon remove redis -force', description: 'remove without confirmation' },
61
61
  ],
62
62
  };
63
63
 
@@ -94,7 +94,10 @@ function removeDirectory(dirPath) {
94
94
  // ============================================================================
95
95
 
96
96
  async function removeService(serviceName, options) {
97
- const { force, offline } = options;
97
+ const { force } = options;
98
+ // Default to the packaged service definitions (mirrors `add`); `-online`
99
+ // opts into fetching the latest from GitHub.
100
+ const offline = !options.online;
98
101
 
99
102
  const projectManifest = loadManifest();
100
103
 
@@ -262,7 +265,7 @@ async function main(cliArgs) {
262
265
  for (const serviceName of services) {
263
266
  const result = await removeService(serviceName, {
264
267
  force,
265
- offline: !!values.offline,
268
+ online: !!values.online,
266
269
  });
267
270
  if (!result) {
268
271
  success = false;