rastack 0.0.63 → 0.0.64

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.
@@ -172,7 +172,17 @@ function analyzeField(name, initializer, checker) {
172
172
  const app = literalTypeProp(fieldType, "__app", checker, initializer);
173
173
  const model = literalTypeProp(fieldType, "__model", checker, initializer);
174
174
  if (app && model) {
175
- return { name, type: "fk", relation: { app, model } };
175
+ const fk = { name, type: "fk", relation: { app, model } };
176
+ // `s.ref(() => X, { null: true })` — a nullable relation (a shared secret
177
+ // with no project, an unassigned task). The options object is the ref
178
+ // call's second argument, read the same way scalar constraints are.
179
+ if (ts.isCallExpression(initializer)) {
180
+ const opts = literalToValue(initializer.arguments[1]);
181
+ if (opts && typeof opts === "object" && opts.null === true) {
182
+ fk.null = true;
183
+ }
184
+ }
185
+ return fk;
176
186
  }
177
187
  return undefined;
178
188
  }
@@ -64,6 +64,11 @@ export interface Ref<App extends string, Model extends string> {
64
64
  readonly __model: Model;
65
65
  /** The authored `() => Resource` thunk — carried at runtime. */
66
66
  readonly target?: () => ResourceType<App, Model, any>;
67
+ /** Optional FK constraints — `null: true` makes the relation nullable
68
+ * (e.g. a shared secret with no project, or an unassigned task). */
69
+ readonly options?: {
70
+ null?: boolean;
71
+ };
67
72
  }
68
73
  export interface StringOptions {
69
74
  maxLength?: number;
@@ -105,7 +110,9 @@ export declare const s: {
105
110
  * the relation from the *type* `Ref<App, Model>` this returns, and the
106
111
  * runtime keeps the thunk so `manifestFromResources()` can resolve it too.
107
112
  */
108
- ref<A extends string, M extends string>(thunk: () => ResourceType<A, M, any>): Ref<A, M>;
113
+ ref<A extends string, M extends string>(thunk: () => ResourceType<A, M, any>, options?: {
114
+ null?: boolean;
115
+ }): Ref<A, M>;
109
116
  };
110
117
  export type AnyField = Scalar<string> | Ref<string, string>;
111
118
  /** Local-first sync configuration (mirrors the old Python `class Sync:`). */
@@ -56,8 +56,8 @@ exports.s = {
56
56
  * the relation from the *type* `Ref<App, Model>` this returns, and the
57
57
  * runtime keeps the thunk so `manifestFromResources()` can resolve it too.
58
58
  */
59
- ref(thunk) {
60
- return { __rastackRef: true, target: thunk };
59
+ ref(thunk, options) {
60
+ return { __rastackRef: true, target: thunk, options };
61
61
  },
62
62
  };
63
63
  /** Define a resource. Capture app/model (and options) as literal types. */
@@ -89,11 +89,17 @@ function runtimeField(res, name, value) {
89
89
  if (!isResourceValue(target)) {
90
90
  throw new Error(`manifestFromResources: ${res.app}.${res.model}.${name} is an s.ref(...) whose target thunk did not return a resource() value`);
91
91
  }
92
- return {
92
+ const fk = {
93
93
  name,
94
94
  type: "fk",
95
95
  relation: { app: target.app, model: target.model },
96
96
  };
97
+ // A nullable FK (`s.ref(() => X, { null: true })`) — e.g. a shared secret
98
+ // with no project — so the engine allows and stores a null relation.
99
+ if (field.options?.null === true) {
100
+ fk.null = true;
101
+ }
102
+ return fk;
97
103
  }
98
104
  if (typeof field.__rastackScalar !== "string")
99
105
  return undefined;
@@ -22,6 +22,27 @@ export declare function configPath(cwd?: string): string;
22
22
  * repo itself still works. Returns null when no candidate exists.
23
23
  */
24
24
  export declare function resolveDeployAsset(relPath: string, cwd?: string): string | null;
25
+ /**
26
+ * Base URL the framework's CloudFormation templates are published at. The same
27
+ * site that hosts the installer (`reactapistack.com/setup.sh`) serves the base
28
+ * templates, so a CLI whose bundled copy is missing (a partial install, or a
29
+ * globally-linked dev build) can still fetch them.
30
+ */
31
+ export declare const TEMPLATE_BASE_URL = "https://reactapistack.com/cloudformation";
32
+ /**
33
+ * Download a published CloudFormation template to a temp file, returning its
34
+ * path — or null on any failure (offline, 404, write error). Used as the
35
+ * fallback when the template is not bundled with the install.
36
+ */
37
+ export declare function downloadTemplate(name: string, base?: string): Promise<string | null>;
38
+ /**
39
+ * Resolve a CloudFormation template to a local file path for `aws cloudformation
40
+ * deploy` (which accepts only `--template-file`). Prefers the copy bundled with
41
+ * the `rastack` install; when that is missing, downloads it from the React API
42
+ * Stack website. Returns null only when the template is neither bundled nor
43
+ * fetchable.
44
+ */
45
+ export declare function resolveTemplateFile(name: string, cwd?: string): Promise<string | null>;
25
46
  /** Read + normalize the deploy config, or null when it does not exist yet. */
26
47
  export declare function loadConfig(cwd?: string): DeployConfig | null;
27
48
  /** Write the deploy config, creating `.rastack/` if needed. */
@@ -39,6 +60,14 @@ export declare function awsCapture(args: string[], cwd?: string): string | null;
39
60
  export declare function awsRun(args: string[], cwd?: string): void;
40
61
  /** True when the AWS CLI is on PATH. */
41
62
  export declare function awsAvailable(): boolean;
63
+ /**
64
+ * The AWS account id the CLI's active credentials resolve to, or null when the
65
+ * CLI has no usable credentials (not logged in, an expired SSO session, or no
66
+ * configured profile). `init`/`bootstrap` call this to fail fast with an
67
+ * actionable "log in" message before attempting a stack deploy, instead of
68
+ * letting the first `cloudformation deploy` fail with a cryptic AWS error.
69
+ */
70
+ export declare function awsIdentity(region?: string): string | null;
42
71
  /**
43
72
  * A single Output value of a CloudFormation stack, or null when the stack does
44
73
  * not exist or the key is unset. `describe-stacks` prints `None` for a missing
@@ -52,6 +81,27 @@ export declare function stackOutput(stackName: string, key: string, region: stri
52
81
  * on every `init`. Returns false when the placeholder file is missing.
53
82
  */
54
83
  export declare function uploadPlaceholder(artifactsBucket: string, region: string, cwd?: string): boolean;
84
+ /**
85
+ * Deploy the tier-0 bootstrap stack, retrying once against an account that
86
+ * already has a GitHub OIDC provider — AWS allows only one provider per URL, so
87
+ * a second create is rejected and the retry tells the template to reuse the
88
+ * existing one. Throws on failure (after the retry). Shared by `rastack init`
89
+ * and `rastack bootstrap` so both stand the stack up identically.
90
+ */
91
+ export declare function deployBootstrap(cfg: DeployConfig, reuseOidc: boolean, template: string): void;
92
+ /**
93
+ * Ensure the tier-0 bootstrap stack exists for this account+region, **creating
94
+ * it when missing**, and return its artifacts/templates buckets. This is why
95
+ * `rastack init` is a single command that stands up *everything* — the
96
+ * account-level bootstrap stack and the per-app backend — with no separate
97
+ * `rastack bootstrap` step. Idempotent: an existing stack is reconciled in
98
+ * place. Throws with an actionable message when the template cannot be resolved,
99
+ * the deploy fails, or the buckets can't be read back.
100
+ */
101
+ export declare function ensureBootstrapStack(cfg: DeployConfig, reuseOidc?: boolean): Promise<{
102
+ artifactsBucket: string;
103
+ templatesBucket: string | null;
104
+ }>;
55
105
  /** True when the GitHub CLI is on PATH. */
56
106
  export declare function ghAvailable(): boolean;
57
107
  /**
package/dist/deploy/io.js CHANGED
@@ -38,8 +38,11 @@ var __importStar = (this && this.__importStar) || (function () {
38
38
  };
39
39
  })();
40
40
  Object.defineProperty(exports, "__esModule", { value: true });
41
+ exports.TEMPLATE_BASE_URL = void 0;
41
42
  exports.configPath = configPath;
42
43
  exports.resolveDeployAsset = resolveDeployAsset;
44
+ exports.downloadTemplate = downloadTemplate;
45
+ exports.resolveTemplateFile = resolveTemplateFile;
43
46
  exports.loadConfig = loadConfig;
44
47
  exports.saveConfig = saveConfig;
45
48
  exports.gitRemoteUrl = gitRemoteUrl;
@@ -47,8 +50,11 @@ exports.canPrompt = canPrompt;
47
50
  exports.awsCapture = awsCapture;
48
51
  exports.awsRun = awsRun;
49
52
  exports.awsAvailable = awsAvailable;
53
+ exports.awsIdentity = awsIdentity;
50
54
  exports.stackOutput = stackOutput;
51
55
  exports.uploadPlaceholder = uploadPlaceholder;
56
+ exports.deployBootstrap = deployBootstrap;
57
+ exports.ensureBootstrapStack = ensureBootstrapStack;
52
58
  exports.ghAvailable = ghAvailable;
53
59
  exports.ghAuthenticated = ghAuthenticated;
54
60
  exports.ghAuthLogin = ghAuthLogin;
@@ -58,6 +64,7 @@ exports.ask = ask;
58
64
  exports.withPrompt = withPrompt;
59
65
  const child_process_1 = require("child_process");
60
66
  const fs = __importStar(require("fs"));
67
+ const https = __importStar(require("https"));
61
68
  const os = __importStar(require("os"));
62
69
  const path = __importStar(require("path"));
63
70
  const readline = __importStar(require("readline"));
@@ -97,6 +104,54 @@ function resolveDeployAsset(relPath, cwd = process.cwd()) {
97
104
  }
98
105
  return null;
99
106
  }
107
+ /**
108
+ * Base URL the framework's CloudFormation templates are published at. The same
109
+ * site that hosts the installer (`reactapistack.com/setup.sh`) serves the base
110
+ * templates, so a CLI whose bundled copy is missing (a partial install, or a
111
+ * globally-linked dev build) can still fetch them.
112
+ */
113
+ exports.TEMPLATE_BASE_URL = "https://reactapistack.com/cloudformation";
114
+ /**
115
+ * Download a published CloudFormation template to a temp file, returning its
116
+ * path — or null on any failure (offline, 404, write error). Used as the
117
+ * fallback when the template is not bundled with the install.
118
+ */
119
+ function downloadTemplate(name, base = exports.TEMPLATE_BASE_URL) {
120
+ const url = `${base}/${name}`;
121
+ return new Promise((resolve) => {
122
+ https
123
+ .get(url, (res) => {
124
+ if (res.statusCode !== 200) {
125
+ res.resume();
126
+ resolve(null);
127
+ return;
128
+ }
129
+ const chunks = [];
130
+ res.on("data", (c) => chunks.push(c));
131
+ res.on("end", () => {
132
+ try {
133
+ const dest = path.join(os.tmpdir(), `rastack-${path.basename(name)}`);
134
+ fs.writeFileSync(dest, Buffer.concat(chunks));
135
+ resolve(dest);
136
+ }
137
+ catch {
138
+ resolve(null);
139
+ }
140
+ });
141
+ })
142
+ .on("error", () => resolve(null));
143
+ });
144
+ }
145
+ /**
146
+ * Resolve a CloudFormation template to a local file path for `aws cloudformation
147
+ * deploy` (which accepts only `--template-file`). Prefers the copy bundled with
148
+ * the `rastack` install; when that is missing, downloads it from the React API
149
+ * Stack website. Returns null only when the template is neither bundled nor
150
+ * fetchable.
151
+ */
152
+ async function resolveTemplateFile(name, cwd = process.cwd()) {
153
+ return resolveDeployAsset(name, cwd) ?? (await downloadTemplate(name));
154
+ }
100
155
  /** Read + normalize the deploy config, or null when it does not exist yet. */
101
156
  function loadConfig(cwd = process.cwd()) {
102
157
  const file = configPath(cwd);
@@ -163,6 +218,26 @@ function awsRun(args, cwd = process.cwd()) {
163
218
  function awsAvailable() {
164
219
  return awsCapture(["--version"]) !== null;
165
220
  }
221
+ /**
222
+ * The AWS account id the CLI's active credentials resolve to, or null when the
223
+ * CLI has no usable credentials (not logged in, an expired SSO session, or no
224
+ * configured profile). `init`/`bootstrap` call this to fail fast with an
225
+ * actionable "log in" message before attempting a stack deploy, instead of
226
+ * letting the first `cloudformation deploy` fail with a cryptic AWS error.
227
+ */
228
+ function awsIdentity(region) {
229
+ const args = [
230
+ "sts",
231
+ "get-caller-identity",
232
+ "--query",
233
+ "Account",
234
+ "--output",
235
+ "text",
236
+ ];
237
+ if (region)
238
+ args.push("--region", region);
239
+ return awsCapture(args);
240
+ }
166
241
  /**
167
242
  * A single Output value of a CloudFormation stack, or null when the stack does
168
243
  * not exist or the key is unset. `describe-stacks` prints `None` for a missing
@@ -210,6 +285,55 @@ function uploadPlaceholder(artifactsBucket, region, cwd = process.cwd()) {
210
285
  ]);
211
286
  return true;
212
287
  }
288
+ /**
289
+ * Deploy the tier-0 bootstrap stack, retrying once against an account that
290
+ * already has a GitHub OIDC provider — AWS allows only one provider per URL, so
291
+ * a second create is rejected and the retry tells the template to reuse the
292
+ * existing one. Throws on failure (after the retry). Shared by `rastack init`
293
+ * and `rastack bootstrap` so both stand the stack up identically.
294
+ */
295
+ function deployBootstrap(cfg, reuseOidc, template) {
296
+ try {
297
+ awsRun((0, config_1.bootstrapStackArgs)(cfg, reuseOidc, template));
298
+ }
299
+ catch (err) {
300
+ if (reuseOidc)
301
+ throw err;
302
+ console.log(" • retrying bootstrap, reusing the account's existing GitHub OIDC provider");
303
+ awsRun((0, config_1.bootstrapStackArgs)(cfg, true, template));
304
+ }
305
+ }
306
+ /**
307
+ * Ensure the tier-0 bootstrap stack exists for this account+region, **creating
308
+ * it when missing**, and return its artifacts/templates buckets. This is why
309
+ * `rastack init` is a single command that stands up *everything* — the
310
+ * account-level bootstrap stack and the per-app backend — with no separate
311
+ * `rastack bootstrap` step. Idempotent: an existing stack is reconciled in
312
+ * place. Throws with an actionable message when the template cannot be resolved,
313
+ * the deploy fails, or the buckets can't be read back.
314
+ */
315
+ async function ensureBootstrapStack(cfg, reuseOidc = false) {
316
+ const stack = (0, config_1.bootstrapStackName)(cfg);
317
+ const existing = stackOutput(stack, config_1.BOOTSTRAP_ARTIFACTS_OUTPUT, cfg.region);
318
+ if (existing) {
319
+ console.log(` ✓ bootstrap stack ${stack} already exists — reconciling`);
320
+ }
321
+ else {
322
+ console.log(`\n▸ Creating the tier-0 bootstrap stack ${stack} (@ ${cfg.region})`);
323
+ }
324
+ const template = await resolveTemplateFile("bootstrap.yaml");
325
+ if (!template) {
326
+ throw new Error("bootstrap.yaml template not found in the rastack package and could not be\n" +
327
+ " downloaded from reactapistack.com. Reinstall rastack (`npm i -g rastack`).");
328
+ }
329
+ deployBootstrap(cfg, reuseOidc, template);
330
+ const artifactsBucket = stackOutput(stack, config_1.BOOTSTRAP_ARTIFACTS_OUTPUT, cfg.region);
331
+ const templatesBucket = stackOutput(stack, config_1.BOOTSTRAP_TEMPLATES_OUTPUT, cfg.region);
332
+ if (!artifactsBucket) {
333
+ throw new Error(`bootstrap stack ${stack} deployed but its ArtifactsBucket output could not be read.`);
334
+ }
335
+ return { artifactsBucket, templatesBucket };
336
+ }
213
337
  // ── GitHub CLI boundary ──────────────────────────────────────────────────────
214
338
  // After the stack is up `init` wires the two repo Variables CI reads
215
339
  // (AWS_DEPLOY_ROLE_ARN / AWS_REGION) through the developer's own `gh` — already
@@ -124,60 +124,31 @@ function printPlan(cfg, reuseOidc) {
124
124
  " ArtifactsBucket to create app backends. Nothing here is app-specific.\n");
125
125
  }
126
126
  /**
127
- * Deploy the bootstrap stack, retrying once against an account that already has
128
- * a GitHub OIDC provider (AWS allows only one per URL) when the caller did not
129
- * already ask to reuse it.
127
+ * Stand the tier-0 bootstrap stack up and upload the Lambda placeholder. The
128
+ * deploy (with the GitHub-OIDC-reuse retry) and template resolution are shared
129
+ * with `rastack init` via `ensureBootstrapStack`, so both stand the stack up
130
+ * identically.
130
131
  */
131
- function deployBootstrap(cfg, reuseOidc) {
132
- const template = (0, io_1.resolveDeployAsset)("bootstrap.yaml");
133
- if (!template) {
134
- console.error(" ✗ bootstrap.yaml template not found in the rastack package. Reinstall\n" +
135
- " rastack (`npm i -g rastack`) or run from the framework repo.");
136
- process.exit(1);
137
- }
138
- try {
139
- (0, io_1.awsRun)((0, config_1.bootstrapStackArgs)(cfg, reuseOidc, template));
140
- }
141
- catch (err) {
142
- if (reuseOidc) {
143
- console.error(`\n ✗ bootstrap stack deploy failed.\n ${err.message}`);
144
- process.exit(1);
145
- }
146
- // The most common cause is an account that already has a GitHub OIDC
147
- // provider. Retry telling the template to reuse it instead of creating a
148
- // second (which CloudFormation rejects).
149
- console.log(" • retrying bootstrap, reusing the account's existing GitHub OIDC provider");
150
- try {
151
- (0, io_1.awsRun)((0, config_1.bootstrapStackArgs)(cfg, true, template));
152
- }
153
- catch (retryErr) {
154
- console.error(`\n ✗ bootstrap stack deploy failed.\n ${retryErr.message}`);
155
- process.exit(1);
156
- }
157
- }
158
- }
159
- /** Stand the tier-0 bootstrap stack up and upload the Lambda placeholder. */
160
- function standUp(cfg, reuseOidc) {
132
+ async function standUp(cfg, reuseOidc) {
161
133
  if (!(0, io_1.awsAvailable)()) {
162
134
  console.error(" ✗ the AWS CLI is not on PATH. Install it and configure credentials,\n" +
163
135
  " or re-run with --print to see the command.");
164
136
  process.exit(1);
165
137
  }
166
- const stack = (0, config_1.bootstrapStackName)(cfg);
167
- const existing = (0, io_1.stackOutput)(stack, config_1.BOOTSTRAP_ARTIFACTS_OUTPUT, cfg.region);
168
- if (existing) {
169
- console.log(`\n▸ Bootstrap stack ${stack} already exists — reconciling (idempotent).`);
170
- }
171
- else {
172
- console.log(`\n▸ Creating bootstrap stack ${stack} (@ ${cfg.region})`);
173
- }
174
- deployBootstrap(cfg, reuseOidc);
175
- const artifactsBucket = (0, io_1.stackOutput)(stack, config_1.BOOTSTRAP_ARTIFACTS_OUTPUT, cfg.region);
176
- const templatesBucket = (0, io_1.stackOutput)(stack, config_1.BOOTSTRAP_TEMPLATES_OUTPUT, cfg.region);
177
- if (!artifactsBucket) {
178
- console.error(" ✗ bootstrap stack deployed but its ArtifactsBucket output could not be read.");
138
+ // Verify the AWS CLI is logged in before attempting a stack deploy.
139
+ const account = (0, io_1.awsIdentity)(cfg.region);
140
+ if (!account) {
141
+ console.error(" ✗ the AWS CLI has no usable credentials — it is not logged in.\n" +
142
+ " Run `aws configure` (or `aws sso login`), then re-run `rastack bootstrap`.\n" +
143
+ " Or re-run with --print to see the command.");
179
144
  process.exit(1);
180
145
  }
146
+ console.log(` ✓ AWS account ${account} (${cfg.region})`);
147
+ const stack = (0, config_1.bootstrapStackName)(cfg);
148
+ const { artifactsBucket, templatesBucket } = await (0, io_1.ensureBootstrapStack)(cfg, reuseOidc).catch((err) => {
149
+ console.error(`\n ✗ ${err.message}`);
150
+ process.exit(1);
151
+ });
181
152
  console.log(` ✓ artifacts bucket: ${artifactsBucket}`);
182
153
  if (templatesBucket)
183
154
  console.log(` ✓ templates bucket: ${templatesBucket}`);
@@ -220,7 +191,7 @@ async function main(argv) {
220
191
  return;
221
192
  }
222
193
  }
223
- standUp(cfg, flags.reuseOidc);
194
+ await standUp(cfg, flags.reuseOidc);
224
195
  }
225
196
  if (require.main === module) {
226
197
  main(process.argv.slice(2)).catch((err) => {
@@ -11,14 +11,17 @@
11
11
  * 1. writes `.rastack/deploy.json` + `.github/workflows/{ci,deploy-backend}.yml`
12
12
  * (GitHub coordinates default from the `origin` remote; missing fields are
13
13
  * prompted for on a TTY);
14
- * 2. resolves the tier-0 bootstrap stack's artifacts bucket — erroring with a
15
- * pointer to `rastack bootstrap` when that account+region stack is missing
16
- * (init is per-app and never creates account-level infrastructure);
14
+ * 2. verifies the AWS CLI is logged in, then ensures the tier-0 bootstrap stack
15
+ * exists — creating it (the S3 template/placeholder buckets + GitHub OIDC
16
+ * provider) when missing, reconciling it when it already does;
17
17
  * 3. uploads the Lambda placeholder bundle to that bucket;
18
18
  * 4. deploys the per-app backend (app.yaml) with the resolved ArtifactsBucket;
19
19
  * 5. reads the stack Outputs back into the config and prints the two GitHub repo
20
20
  * Variables CI needs.
21
21
  *
22
+ * The CloudFormation templates ship inside the `rastack` package; a partial
23
+ * install falls back to downloading them from reactapistack.com.
24
+ *
22
25
  * Thereafter the recurring code deploy is CI's job — `rastack ci deploy`, which
23
26
  * `rastack deploy` also runs. `--no-deploy` scaffolds only; `--print` shows the
24
27
  * exact single-line `aws cloudformation deploy` commands without running them.
@@ -12,14 +12,17 @@
12
12
  * 1. writes `.rastack/deploy.json` + `.github/workflows/{ci,deploy-backend}.yml`
13
13
  * (GitHub coordinates default from the `origin` remote; missing fields are
14
14
  * prompted for on a TTY);
15
- * 2. resolves the tier-0 bootstrap stack's artifacts bucket — erroring with a
16
- * pointer to `rastack bootstrap` when that account+region stack is missing
17
- * (init is per-app and never creates account-level infrastructure);
15
+ * 2. verifies the AWS CLI is logged in, then ensures the tier-0 bootstrap stack
16
+ * exists — creating it (the S3 template/placeholder buckets + GitHub OIDC
17
+ * provider) when missing, reconciling it when it already does;
18
18
  * 3. uploads the Lambda placeholder bundle to that bucket;
19
19
  * 4. deploys the per-app backend (app.yaml) with the resolved ArtifactsBucket;
20
20
  * 5. reads the stack Outputs back into the config and prints the two GitHub repo
21
21
  * Variables CI needs.
22
22
  *
23
+ * The CloudFormation templates ship inside the `rastack` package; a partial
24
+ * install falls back to downloading them from reactapistack.com.
25
+ *
23
26
  * Thereafter the recurring code deploy is CI's job — `rastack ci deploy`, which
24
27
  * `rastack deploy` also runs. `--no-deploy` scaffolds only; `--print` shows the
25
28
  * exact single-line `aws cloudformation deploy` commands without running them.
@@ -244,8 +247,10 @@ function envLabel(cfg, env) {
244
247
  }
245
248
  /**
246
249
  * Print the exact commands `init` would run, without touching the cloud
247
- * (`--print`). The artifacts bucket is read from the bootstrap stack when the
248
- * AWS CLI is available, else shown as a placeholder.
250
+ * (`--print`). init stands up *both* stacks, so both `aws cloudformation deploy`
251
+ * commands are shown: the tier-0 bootstrap stack (created only when missing) and
252
+ * the per-app backend. The artifacts bucket is read from an existing bootstrap
253
+ * stack when the AWS CLI is available, else shown as a placeholder.
249
254
  */
250
255
  function printPlan(cfg, envs) {
251
256
  const bucket = (0, io_1.awsAvailable)()
@@ -254,11 +259,12 @@ function printPlan(cfg, envs) {
254
259
  const withBucket = (0, config_1.mergeConfig)(cfg, {
255
260
  artifactsBucket: bucket ?? "<ArtifactsBucketName from bootstrap stack>",
256
261
  });
262
+ const bootstrapTemplate = (0, io_1.resolveDeployAsset)("bootstrap.yaml") ?? undefined;
257
263
  const appTemplate = (0, io_1.resolveDeployAsset)("app.yaml") ?? undefined;
258
264
  console.log(`\n Deployment plan (${envs.join(" + ")} @ ${cfg.region})\n`);
259
- console.log(` ── Prerequisite: the tier-0 bootstrap stack (${(0, config_1.bootstrapStackName)(cfg)}) must exist ──`);
260
- console.log(` Create it once per account+region (init does not): rastack bootstrap --region ${cfg.region}\n`);
261
- console.log(" ── 1. Per-app backend (app.yaml) — one stack per environment ──");
265
+ console.log(` ── 1. Tier-0 bootstrap stack (${(0, config_1.bootstrapStackName)(cfg)}) — created only when missing ──`);
266
+ console.log("\n " + (0, config_1.bootstrapStackCommand)(cfg, false, bootstrapTemplate) + "\n");
267
+ console.log(" ── 2. Per-app backend (app.yaml) — one stack per environment ──");
262
268
  for (const env of envs) {
263
269
  const e = (0, config_1.resolveEnvironment)(withBucket, env);
264
270
  console.log(`\n # ${envLabel(withBucket, env)} — stack ${e.stackName}`);
@@ -276,9 +282,12 @@ function printPlan(cfg, envs) {
276
282
  console.log("");
277
283
  }
278
284
  /**
279
- * Stand the infrastructure up: ensure the bootstrap stack + artifacts bucket,
280
- * upload the Lambda placeholder, deploy app.yaml, and read the Outputs back into
281
- * the config. Returns the config enriched with the resolved buckets + outputs.
285
+ * Stand the infrastructure up: verify the AWS CLI is logged in, ensure the
286
+ * tier-0 bootstrap stack (creating it when missing), upload the Lambda
287
+ * placeholder, deploy app.yaml, and read the Outputs back into the config.
288
+ * `init` is the single command that stands up *everything* — there is no
289
+ * separate manual bootstrap step. Returns the config enriched with the resolved
290
+ * buckets + outputs.
282
291
  */
283
292
  async function standUp(cfg, envs, interactive) {
284
293
  if (!(0, io_1.awsAvailable)()) {
@@ -286,23 +295,27 @@ async function standUp(cfg, envs, interactive) {
286
295
  " or re-run with --print to see the commands, or --no-deploy to skip.");
287
296
  process.exit(1);
288
297
  }
289
- // 1. Resolve the artifacts bucket from the tier-0 bootstrap stack. That stack
290
- // is an account+region once-off — the S3 buckets that host the templates +
291
- // Lambda placeholder, and the GitHub OIDC provider. `init` is a per-app
292
- // command and deliberately does NOT create it: standing up account-level
293
- // infrastructure is `rastack bootstrap`'s job (run once by an account admin),
294
- // so a missing bootstrap stack is a clear, actionable error, not an implicit
295
- // account mutation buried inside an app deploy.
296
- const artifactsBucket = (0, io_1.stackOutput)((0, config_1.bootstrapStackName)(cfg), config_1.BOOTSTRAP_ARTIFACTS_OUTPUT, cfg.region);
297
- const templatesBucket = (0, io_1.stackOutput)((0, config_1.bootstrapStackName)(cfg), config_1.BOOTSTRAP_TEMPLATES_OUTPUT, cfg.region);
298
- if (!artifactsBucket) {
299
- console.error(`\n ✗ Bootstrap stack ${(0, config_1.bootstrapStackName)(cfg)} not found in ${cfg.region}.\n` +
300
- " It is the once-per-account+region step that hosts the CloudFormation\n" +
301
- " templates and the Lambda placeholder every app deploy reads. Create it\n" +
302
- " first (an account admin does this once), then re-run `rastack init`:\n\n" +
303
- ` rastack bootstrap --region ${cfg.region}\n`);
298
+ // Verify the AWS CLI is actually logged in before creating any stacks, so a
299
+ // missing/expired credential fails fast with an actionable message rather than
300
+ // a cryptic `cloudformation deploy` error partway through.
301
+ const account = (0, io_1.awsIdentity)(cfg.region);
302
+ if (!account) {
303
+ console.error(" ✗ the AWS CLI has no usable credentials — it is not logged in.\n" +
304
+ " Configure credentials, then re-run `rastack init`:\n" +
305
+ " aws configure # static access key / secret\n" +
306
+ " aws sso login # or an existing SSO session\n" +
307
+ " Or re-run with --print to see the commands, or --no-deploy to skip.");
304
308
  process.exit(1);
305
309
  }
310
+ console.log(` ✓ AWS account ${account} (${cfg.region})`);
311
+ // 1. Ensure the tier-0 bootstrap stack exists — the S3 buckets that host the
312
+ // templates + Lambda placeholder and the GitHub OIDC provider. init creates
313
+ // it when missing (idempotent for an existing one), so a single command
314
+ // takes an account+repo from nothing to a deployed backend.
315
+ const { artifactsBucket, templatesBucket } = await (0, io_1.ensureBootstrapStack)(cfg).catch((err) => {
316
+ console.error(`\n ✗ ${err.message}`);
317
+ process.exit(1);
318
+ });
306
319
  console.log(` ✓ artifacts bucket: ${artifactsBucket}`);
307
320
  // 2. Upload the placeholder the function is created from (shared by all envs).
308
321
  console.log("\n▸ Uploading the Lambda placeholder bundle");
@@ -314,10 +327,10 @@ async function standUp(cfg, envs, interactive) {
314
327
  artifactsBucket,
315
328
  templatesBucket: templatesBucket ?? undefined,
316
329
  });
317
- const appTemplate = (0, io_1.resolveDeployAsset)("app.yaml");
330
+ const appTemplate = await (0, io_1.resolveTemplateFile)("app.yaml");
318
331
  if (!appTemplate) {
319
- console.error(" ✗ app.yaml template not found in the rastack package. Reinstall rastack\n" +
320
- " (`npm i -g rastack`) or run from the framework repo.");
332
+ console.error(" ✗ app.yaml template not found in the rastack package and could not be\n" +
333
+ " downloaded from reactapistack.com. Reinstall rastack (`npm i -g rastack`).");
321
334
  process.exit(1);
322
335
  }
323
336
  // 3. Deploy one stack per environment — each with its own Cognito pool,
@@ -501,7 +514,7 @@ async function main(argv) {
501
514
  const stacks = flags.envs
502
515
  .map((env) => (0, config_1.resolveEnvironment)(cfg, env).stackName)
503
516
  .join(" + ");
504
- const ok = await confirm(`Deploy ${stacks} to AWS ${cfg.region} now? (requires the bootstrap stack to exist) [y/N]`);
517
+ const ok = await confirm(`Deploy ${stacks} to AWS ${cfg.region} now? (creates the bootstrap stack if missing) [y/N]`);
505
518
  if (!ok) {
506
519
  console.log("\n Skipped the deploy. Re-run to deploy, or `rastack init --print` for the commands.\n");
507
520
  return;
@@ -375,7 +375,7 @@ function __wbg_get_imports() {
375
375
  return ret;
376
376
  },
377
377
  __wbindgen_cast_0000000000000001: function(arg0, arg1) {
378
- // Cast intrinsic for `Closure(Closure { owned: true, function: Function { arguments: [Externref], shim_idx: 88, ret: Result(Unit), inner_ret: Some(Result(Unit)) }, mutable: true }) -> Externref`.
378
+ // Cast intrinsic for `Closure(Closure { owned: true, function: Function { arguments: [Externref], shim_idx: 90, ret: Result(Unit), inner_ret: Some(Result(Unit)) }, mutable: true }) -> Externref`.
379
379
  const ret = makeMutClosure(arg0, arg1, wasm_bindgen_ac0f9960a408a037___convert__closures_____invoke___wasm_bindgen_ac0f9960a408a037___JsValue__core_9b3796e30d99ddb7___result__Result_____wasm_bindgen_ac0f9960a408a037___JsError___true_);
380
380
  return ret;
381
381
  },
Binary file
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "rastack",
3
- "version": "0.0.63",
3
+ "version": "0.0.64",
4
4
  "description": "",
5
5
  "main": "runtime.ts",
6
6
  "types": "runtime.ts",
@@ -170,6 +170,11 @@ export function RAStackProvider({
170
170
  const existing = await persistence.load();
171
171
  if (existing) {
172
172
  await engine.importWarehouse(existing);
173
+ } else if (config.seedWarehouse) {
174
+ // Seed from a bundled warehouse blob (exportWarehouse format) rather
175
+ // than loose Iceberg files.
176
+ const seed = await config.seedWarehouse();
177
+ if (seed) await engine.importWarehouse(seed);
173
178
  } else {
174
179
  const base = config.warehouseBaseUrl ?? "/data/warehouse/";
175
180
  const resources = manifestResources(manifest);
@@ -208,6 +213,24 @@ export function RAStackProvider({
208
213
  }, 400);
209
214
  persistRef.current = persist;
210
215
 
216
+ // Persist at the *engine* level, not just in the axios adapter. Consumers
217
+ // that talk to the engine directly (the admin console calls
218
+ // `engine.handle("DELETE", …)` straight, bypassing the generated hooks /
219
+ // axios seam) must still have their writes saved to IndexedDB — otherwise
220
+ // every reload re-seeds from the committed warehouse and silently
221
+ // resurrects deleted rows. Wrapping `handle` here makes persistence fire
222
+ // for both paths (the adapter's `onWrite` below then coalesces into the
223
+ // same debounce, so an app-hook write persists exactly once).
224
+ const MUTATING = new Set(["POST", "PUT", "PATCH", "DELETE"]);
225
+ const rawHandle = engine.handle.bind(engine);
226
+ engine.handle = ((method: string, path: string, query?: any, body?: any) =>
227
+ Promise.resolve(rawHandle(method, path, query, body)).then((res: any) => {
228
+ if (MUTATING.has(String(method).toUpperCase()) && res && res.status < 300) {
229
+ persist();
230
+ }
231
+ return res;
232
+ })) as typeof engine.handle;
233
+
211
234
  engineRef.current = engine;
212
235
  configureApi({
213
236
  baseURL: "",
package/provider/types.ts CHANGED
@@ -152,6 +152,15 @@ export interface RAStackProviderConfig {
152
152
  fileSource?: import("../cache").WarehouseFileSource;
153
153
  /** `s3` mode: how to load/save the warehouse blob from object storage. */
154
154
  warehouse?: WarehouseSource;
155
+ /**
156
+ * Seed `local` mode from a **bundled warehouse blob** — the format
157
+ * `exportWarehouse()` produces and `importWarehouse()` consumes — instead of
158
+ * walking loose Iceberg files under `warehouseBaseUrl`. Use this when the app
159
+ * commits a single warehouse blob asset (e.g. bundled JSON) rather than a
160
+ * directory of parquet files. Takes precedence over `warehouseBaseUrl` /
161
+ * `fileSource` seeding, and is ignored once a persisted local copy exists.
162
+ */
163
+ seedWarehouse?: () => Uint8Array | null | Promise<Uint8Array | null>;
155
164
  /**
156
165
  * Override how the WASM module is loaded. Supply this from app code (where
157
166
  * your bundler can resolve the generated bundle) if the default loader can't