@everystack/cli 0.4.6 → 0.4.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@everystack/cli",
3
- "version": "0.4.6",
3
+ "version": "0.4.8",
4
4
  "description": "CLI and OTA updates for Expo apps on everystack",
5
5
  "license": "AGPL-3.0-only",
6
6
  "author": "Scalable Technology, Inc. <licensing@scalable.technology>",
@@ -82,7 +82,7 @@
82
82
  "structured-headers": "1.0.1",
83
83
  "tsx": "4.21.0",
84
84
  "typescript": "5.9.3",
85
- "@everystack/model": "0.4.1"
85
+ "@everystack/model": "0.4.2"
86
86
  },
87
87
  "peerDependencies": {
88
88
  "@everystack/server": ">=0.1.0",
@@ -35,6 +35,7 @@ import { resolveDbSource, createUrlRunner, connectingVia, type DbSource } from '
35
35
  import { resolveConfig, opsFunction } from '../config.js';
36
36
  import { invokeAction, lambdaQueryRunner } from '../aws.js';
37
37
  import { executeApplyPlan, type ApplyPlanResult } from '../apply-execute.js';
38
+ import { pgDumpPreflightError } from './db-backup.js';
38
39
  import { step, success, fail, info, warn } from '../output.js';
39
40
 
40
41
  // The apply core lives in apply-execute.ts (shared with the ops Lambda's
@@ -116,6 +117,19 @@ async function applyPlanViaStage(
116
117
 
117
118
  info(`plan ${planHash(plan).slice(0, 12)}: ${plan.fromFingerprint.slice(0, 12)} → ${plan.toFingerprint.slice(0, 12)} (${plan.executable} statement(s)${plan.destructive ? `, ${plan.destructive} destructive` : ''})`);
118
119
 
120
+ // A destructive staged apply snapshots via db:backup INSIDE the Lambda, before the DDL. Preflight
121
+ // that capability HERE so a missing/incompatible pg_dump layer fails fast with the install remedy —
122
+ // not deep in the ceremony after the authority check, the way it once surfaced mid-apply.
123
+ if (plan.destructive > 0) {
124
+ step('Preflighting the pg_dump layer (the destructive apply snapshots before the DDL)...');
125
+ const probe: any = await invokeAction(region, fn, 'db:backup:probe', {}).catch((err: any) => ({ error: err?.message ?? String(err) }));
126
+ const remedy = pgDumpPreflightError(probe);
127
+ if (remedy) {
128
+ fail(`Cannot take the pre-apply snapshot — the destructive plan was NOT applied.\n${remedy}`);
129
+ process.exit(1);
130
+ }
131
+ }
132
+
119
133
  try {
120
134
  // Descent — the fast-forward rule needs the git checkout, so the CLI
121
135
  // decides it (the Lambda has no checkout). Read the stage's state read-only
@@ -48,7 +48,7 @@ export async function dbBackupProbeCommand(flags: Record<string, string>): Promi
48
48
 
49
49
  if (result?.error) {
50
50
  fail(result.error);
51
- info('Build + publish the layer with scripts/build-pgdump-layer.sh, export PGDUMP_LAYER_ARN, then redeploy.');
51
+ info('Install @everystack/pg-tools and redeploy — the layer attaches automatically. For a bring-your-own layer, pass pgDumpLayer(..., { layerArn }) in sst.config.ts.');
52
52
  process.exit(1);
53
53
  }
54
54
 
@@ -71,7 +71,7 @@ export async function dbBackupProbeCommand(flags: Record<string, string>): Promi
71
71
  export function pgDumpPreflightError(probe: any): string | null {
72
72
  if (!probe || probe.error) {
73
73
  const detail = probe?.error ?? 'the pg_dump layer probe returned no result';
74
- return `${detail}\nBuild + publish the layer with scripts/build-pgdump-layer.sh, export PGDUMP_LAYER_ARN, then redeploy.`;
74
+ return `${detail}\nInstall @everystack/pg-tools and redeploy — the layer attaches automatically. For a bring-your-own layer, pass pgDumpLayer(..., { layerArn }) in sst.config.ts.`;
75
75
  }
76
76
  if (probe.compatible === false) {
77
77
  return `pg_dump ${probe.pgDump ?? '(unknown)'} (major ${probe.pgDumpMajor}) is older than the server (major ${probe.serverMajor}) — it will refuse to dump (no bypass exists). Rebuild the layer with pg_dump >= ${probe.serverMajor}.`;
@@ -23,7 +23,9 @@ function resolveInstanceId(config: CliConfig, flags: Record<string, string>): st
23
23
  if (!id || id === 'placeholder') {
24
24
  fail(
25
25
  'No RDS instance id available. db:snapshot needs an RDS instance.\n' +
26
- ' - If your DB is RDS: redeploy so the `databaseInstanceId` output is present, or pass --instance <id>.\n' +
26
+ ' - If your DB is RDS: add `databaseInstanceId: database.id` to the outputs return block in\n' +
27
+ ' sst.config.ts and redeploy (run db:snapshot from the app directory so .sst/outputs.json is\n' +
28
+ ' readable), or pass --instance <id>.\n' +
27
29
  ' - If your DB is not RDS (Aurora, a container, self-hosted): use `everystack db:backup` for a portable logical backup.',
28
30
  );
29
31
  process.exit(1);
@@ -11,7 +11,7 @@
11
11
  */
12
12
 
13
13
  import { spawn } from 'node:child_process';
14
- import { step, success, fail, info } from '../output.js';
14
+ import { step, success, fail, info, warn } from '../output.js';
15
15
  import { invalidateCachedConfig } from '../discover.js';
16
16
  import { resolveConfig } from '../config.js';
17
17
  import { probeDeployedFunctions } from '../deploy-probe.js';
@@ -22,14 +22,31 @@ export function buildDeployArgs(flags: Record<string, string>): string[] {
22
22
  return ['sst', 'deploy', '--stage', stage];
23
23
  }
24
24
 
25
+ /**
26
+ * The environment the sst child receives: the FULL ambient environment plus color. The wrapper
27
+ * forwards everything so a consumer's own sst.config.ts env reads behave the same run directly or
28
+ * through `everystack deploy`. (This contract was pinned when the pg_dump layer depended on
29
+ * PGDUMP_LAYER_ARN reaching sst — now deprecated: the layer ships via @everystack/pg-tools +
30
+ * pgDumpLayer() and reads no ambient env at deploy.)
31
+ */
32
+ export function deployEnv(base: Record<string, string | undefined>): Record<string, string | undefined> {
33
+ return { ...base, FORCE_COLOR: '1' };
34
+ }
35
+
25
36
  export async function deployCommand(flags: Record<string, string>): Promise<void> {
26
37
  const stage = flags.stage || 'dev';
27
38
  const args = buildDeployArgs(flags);
28
39
 
29
40
  step(`Deploying stage "${stage}"...`);
41
+ // PGDUMP_LAYER_ARN is deprecated: the layer now ships via @everystack/pg-tools and pgDumpLayer()
42
+ // needs no env var. Still forwarded to sst for one more minor (pgDumpLayer honors it as an alias),
43
+ // then removed — surface it loudly so the operator migrates off it.
44
+ if (process.env.PGDUMP_LAYER_ARN) {
45
+ warn('PGDUMP_LAYER_ARN is DEPRECATED and will be removed next minor. The pg_dump layer now ships via @everystack/pg-tools + pgDumpLayer() with no env var. Drop it once your sst.config.ts uses pgDumpLayer().');
46
+ }
30
47
  await new Promise<void>((resolve) => {
31
48
  // `npx` resolves the consumer's local sst (the same way the export step runs expo).
32
- const child = spawn('npx', args, { stdio: 'inherit', env: { ...process.env, FORCE_COLOR: '1' } });
49
+ const child = spawn('npx', args, { stdio: 'inherit', env: deployEnv(process.env) as NodeJS.ProcessEnv });
33
50
  child.on('close', (code) => {
34
51
  if (code === 0) {
35
52
  resolve();
@@ -65,5 +82,8 @@ export async function deployCommand(flags: Record<string, string>): Promise<void
65
82
  info(`Skipped the post-deploy probe: ${err?.message ?? err}`);
66
83
  }
67
84
 
68
- info('Next: `everystack db:migrate --stage ' + stage + '` to apply migrations, `everystack update` to publish the app bundle.');
85
+ // The 0.4 flow: schema changes ride db:plan db:apply. Migration-based apps still have
86
+ // db:migrate, but the copy-paste hint must advertise the canonical path — this exact
87
+ // line got copied into consumer runbooks with the retired verb.
88
+ info('Next: schema changes via `everystack db:plan --stage ' + stage + '` → `db:apply`; `everystack update` to publish the app bundle.');
69
89
  }
package/src/cli/config.ts CHANGED
@@ -52,6 +52,35 @@ interface SstOutputs {
52
52
  backupsBucket?: string;
53
53
  }
54
54
 
55
+ /**
56
+ * Merge deploy-stable fields from a local .sst/outputs.json into a discovered config.
57
+ * Discovery finds live resources (functions, buckets) but cannot see plain stack outputs
58
+ * like `databaseInstanceId` — a consumer's fresh deploy DID add the output and
59
+ * `db:snapshot --stage` still failed, because the discovery path never carried it.
60
+ * Only fields that are stable across deploys are merged (an instance id never rotates
61
+ * the way a Lambda hash does), so a stale outputs file cannot poison the live discovery.
62
+ */
63
+ export function enrichConfigFromOutputs(
64
+ config: CliConfig,
65
+ outputs: Partial<SstOutputs> | null,
66
+ ): CliConfig {
67
+ if (!outputs) return config;
68
+ return {
69
+ ...config,
70
+ ...(config.databaseInstanceId ? {} : outputs.databaseInstanceId ? { databaseInstanceId: outputs.databaseInstanceId } : {}),
71
+ };
72
+ }
73
+
74
+ /** Best-effort read of the local .sst/outputs.json; null when absent/unreadable. */
75
+ async function readLocalOutputs(): Promise<Partial<SstOutputs> | null> {
76
+ try {
77
+ const raw = await fs.readFile(path.resolve('.sst', 'outputs.json'), 'utf8');
78
+ return JSON.parse(raw) as Partial<SstOutputs>;
79
+ } catch {
80
+ return null;
81
+ }
82
+ }
83
+
55
84
  export async function resolveConfig(stage?: string): Promise<CliConfig> {
56
85
  const region = process.env.AWS_REGION || process.env.AWS_DEFAULT_REGION || 'us-east-1';
57
86
 
@@ -66,7 +95,7 @@ export async function resolveConfig(stage?: string): Promise<CliConfig> {
66
95
 
67
96
  // Check cache first
68
97
  const cached = await getCachedConfig(stage);
69
- if (cached) return cached;
98
+ if (cached) return enrichConfigFromOutputs(cached, await readLocalOutputs());
70
99
 
71
100
  step(`Discovering resources for stage "${stage}"...`);
72
101
  const appName = await parseAppName();
@@ -74,7 +103,7 @@ export async function resolveConfig(stage?: string): Promise<CliConfig> {
74
103
 
75
104
  // Cache for future fast lookups
76
105
  await setCachedConfig(stage, config);
77
- return config;
106
+ return enrichConfigFromOutputs(config, await readLocalOutputs());
78
107
  }
79
108
 
80
109
  // Default: read .sst/outputs.json (unchanged behavior)
package/src/cli/index.ts CHANGED
@@ -339,7 +339,7 @@ Usage:
339
339
  everystack deploy [--stage <name>] Deploy infrastructure (wraps sst deploy — no need to know SST)
340
340
  everystack update --branch <name> --message <msg> [--platform ios|android|web|all] [--stage <name>] [--skip-export]
341
341
  everystack update --channel <name> --message <msg> [--platform ios|android|web|all] [--stage <name>] [--skip-export]
342
- everystack db:migrate [--stage <name>] Run database migrations on deployed Lambda
342
+ everystack db:migrate [--stage <name>] Run drizzle migrations (legacy Models apps use db:plan → db:apply)
343
343
  everystack db:seed [--stage <name>] Seed database on deployed Lambda (dev only)
344
344
  everystack db:reset [--stage <name>] Drop all schemas + re-run migrations (dev only)
345
345
  everystack db:psql --stage <name> Interactive ADMIN psql (IAM-gated; resolves the admin URL in-process)
@@ -136,7 +136,12 @@ function deploy(r: ProjectReality): Block {
136
136
  '- `everystack channels list` — see the channels that exist.',
137
137
  ];
138
138
  if (r.hasDatabase) {
139
- lines.push('', 'After a deploy that includes new migrations: `everystack db:migrate --stage <name>`.');
139
+ lines.push(
140
+ '',
141
+ r.migrationMode === 'everystack'
142
+ ? 'After a deploy, schema changes land via `everystack db:plan --stage <name>` → `db:apply`.'
143
+ : 'After a deploy that includes new migrations: `everystack db:migrate --stage <name>`.',
144
+ );
140
145
  }
141
146
  return gen('deploy', lines);
142
147
  }
@@ -222,7 +227,12 @@ function database(r: ProjectReality): Block {
222
227
  '## Database operations',
223
228
  '',
224
229
  ...flow,
225
- '- `everystack db:migrate --stage <name>` apply migrations on the deployed Lambda',
230
+ // db:migrate is the RETIRED verb for everystack-mode apps (plan → apply is the flow);
231
+ // advertising it here got copied into consumer runbooks verbatim. Migration-based
232
+ // apps still get it — it is their flow.
233
+ ...(r.migrationMode === 'everystack'
234
+ ? []
235
+ : ['- `everystack db:migrate --stage <name>` — apply migrations on the deployed Lambda']),
226
236
  '- `everystack db:seed --stage dev` — seed the database (dev only)',
227
237
  '- `everystack db:psql --stage dev` — psql into the stage database (credentials never exposed)',
228
238
  '- `everystack db:doctor` — verify the security posture (RLS, roles, search_path)',