rastack 0.0.34 → 0.0.36

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.
@@ -1,16 +1,27 @@
1
1
  #!/usr/bin/env node
2
2
  "use strict";
3
3
  /**
4
- * `rastack init` — template the base GitHub Actions + deploy config into a repo.
4
+ * `rastack init` — scaffold a repo's deploy config + GitHub Actions AND stand the
5
+ * serverless backend up.
5
6
  *
6
7
  * rastack init [--app <slug>] [--owner <o>] [--repo <r>] [--branch <b>]
7
- * [--region <r>] [--force] [--no-workflows] [--yes]
8
+ * [--region <r>] [--prefix <p>] [--namespace <n>]
9
+ * [--force] [--no-workflows] [--no-deploy] [--print] [--yes]
8
10
  *
9
- * Scaffolds a new project (or reconfigures an existing one) so it is ready to
10
- * deploy: `.rastack/deploy.json` plus `.github/workflows/{ci,deploy-backend}.yml`,
11
- * both of which run `rastack ci`. GitHub coordinates default from the repo's
12
- * `origin` remote; missing fields are prompted for on a TTY. Existing files are
13
- * left untouched unless `--force`.
11
+ * This is the one command that takes you from nothing to deployed. It:
12
+ * 1. writes `.rastack/deploy.json` + `.github/workflows/{ci,deploy-backend}.yml`
13
+ * (GitHub coordinates default from the `origin` remote; missing fields are
14
+ * prompted for on a TTY);
15
+ * 2. resolves the tier-0 bootstrap stack's artifacts bucket — and CREATES the
16
+ * bootstrap stack if it does not exist yet;
17
+ * 3. uploads the Lambda placeholder bundle to that bucket;
18
+ * 4. deploys the per-app backend (app.yaml) with the resolved ArtifactsBucket;
19
+ * 5. reads the stack Outputs back into the config and prints the two GitHub repo
20
+ * Variables CI needs.
21
+ *
22
+ * Thereafter the recurring code deploy is CI's job — `rastack ci deploy`, which
23
+ * `rastack deploy` also runs. `--no-deploy` scaffolds only; `--print` shows the
24
+ * exact single-line `aws cloudformation deploy` commands without running them.
14
25
  */
15
26
  var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
16
27
  if (k2 === undefined) k2 = k;
@@ -55,6 +66,8 @@ function parseFlags(argv) {
55
66
  const patch = { github: {} };
56
67
  let force = false;
57
68
  let noWorkflows = false;
69
+ let noDeploy = false;
70
+ let print = false;
58
71
  let nonInteractive = false;
59
72
  for (let i = 0; i < argv.length; i++) {
60
73
  const a = argv[i];
@@ -78,12 +91,22 @@ function parseFlags(argv) {
78
91
  case "--prefix":
79
92
  patch.namePrefix = next();
80
93
  break;
94
+ case "--namespace":
95
+ patch.namespace = next();
96
+ break;
81
97
  case "--force":
82
98
  force = true;
83
99
  break;
84
100
  case "--no-workflows":
85
101
  noWorkflows = true;
86
102
  break;
103
+ case "--no-deploy":
104
+ noDeploy = true;
105
+ break;
106
+ case "--print":
107
+ case "--dry-run":
108
+ print = true;
109
+ break;
87
110
  case "--yes":
88
111
  case "-y":
89
112
  case "--non-interactive":
@@ -96,7 +119,7 @@ function parseFlags(argv) {
96
119
  }
97
120
  }
98
121
  }
99
- return { patch, force, noWorkflows, nonInteractive };
122
+ return { patch, force, noWorkflows, noDeploy, print, nonInteractive };
100
123
  }
101
124
  /** Seed a config: existing file → flags → git remote → defaults. */
102
125
  function seedConfig(flags) {
@@ -133,25 +156,156 @@ async function promptMissing(cfg) {
133
156
  });
134
157
  });
135
158
  }
159
+ /** Yes/no confirmation with a default of "no" — gates the cloud-mutating steps. */
160
+ async function confirm(question) {
161
+ return (0, io_1.withPrompt)(async (rl) => {
162
+ const answer = await (0, io_1.ask)(rl, question, "no");
163
+ return /^y(es)?$/i.test(answer.trim());
164
+ });
165
+ }
166
+ /** Pad `Key Value` rows into an aligned block. */
167
+ function table(rows) {
168
+ const width = Math.max(0, ...rows.map((r) => r.key.length));
169
+ return rows.map((r) => ` ${r.key.padEnd(width)} ${r.value}`).join("\n");
170
+ }
171
+ /**
172
+ * Read the app.yaml stack Outputs into a `DeployOutputs`, skipping any that are
173
+ * not resolvable yet (best-effort — a freshly created stack has them all).
174
+ */
175
+ function readAppOutputs(cfg) {
176
+ const outputs = {};
177
+ for (const { key, field } of config_1.APP_STACK_OUTPUTS) {
178
+ const value = (0, io_1.stackOutput)(cfg.stackName, key, cfg.region);
179
+ if (value)
180
+ outputs[field] = value;
181
+ }
182
+ return outputs;
183
+ }
184
+ /**
185
+ * Print the exact commands `init` would run, without touching the cloud
186
+ * (`--print`). The artifacts bucket is read from the bootstrap stack when the
187
+ * AWS CLI is available, else shown as a placeholder.
188
+ */
189
+ function printPlan(cfg) {
190
+ const bucket = (0, io_1.awsAvailable)()
191
+ ? (0, io_1.stackOutput)((0, config_1.bootstrapStackName)(cfg), config_1.BOOTSTRAP_ARTIFACTS_OUTPUT, cfg.region)
192
+ : null;
193
+ const withBucket = (0, config_1.mergeConfig)(cfg, {
194
+ artifactsBucket: bucket ?? "<ArtifactsBucketName from bootstrap stack>",
195
+ });
196
+ console.log(`\n Deployment plan (${cfg.stackName} @ ${cfg.region})\n`);
197
+ console.log(` ── 1. Bootstrap stack (${(0, config_1.bootstrapStackName)(cfg)}) — run only if it does not exist ──`);
198
+ console.log("\n " + (0, config_1.bootstrapStackCommand)(cfg) + "\n");
199
+ console.log(" ── 2. Per-app backend (app.yaml) ──");
200
+ console.log("\n " + (0, config_1.deployStackCommand)(withBucket) + "\n");
201
+ console.log(" ── 3. Wire these GitHub repo Variables from the stack Outputs ──");
202
+ console.log(table((0, config_1.repoVariables)(cfg).map((v) => ({
203
+ key: v.name,
204
+ value: v.value || `<${v.from}>`,
205
+ }))) + "\n");
206
+ }
207
+ /**
208
+ * Stand the infrastructure up: ensure the bootstrap stack + artifacts bucket,
209
+ * upload the Lambda placeholder, deploy app.yaml, and read the Outputs back into
210
+ * the config. Returns the config enriched with the resolved buckets + outputs.
211
+ */
212
+ async function standUp(cfg) {
213
+ if (!(0, io_1.awsAvailable)()) {
214
+ console.error(" ✗ the AWS CLI is not on PATH. Install it and configure credentials,\n" +
215
+ " or re-run with --print to see the commands, or --no-deploy to skip.");
216
+ process.exit(1);
217
+ }
218
+ // 1. Resolve the artifacts bucket; bootstrap the tier-0 stack if it is absent.
219
+ let artifactsBucket = (0, io_1.stackOutput)((0, config_1.bootstrapStackName)(cfg), config_1.BOOTSTRAP_ARTIFACTS_OUTPUT, cfg.region);
220
+ let templatesBucket = (0, io_1.stackOutput)((0, config_1.bootstrapStackName)(cfg), config_1.BOOTSTRAP_TEMPLATES_OUTPUT, cfg.region);
221
+ if (!artifactsBucket) {
222
+ console.log(`\n▸ Bootstrap stack ${(0, config_1.bootstrapStackName)(cfg)} not found — creating it (once per account+region).`);
223
+ deployBootstrap(cfg);
224
+ artifactsBucket = (0, io_1.stackOutput)((0, config_1.bootstrapStackName)(cfg), config_1.BOOTSTRAP_ARTIFACTS_OUTPUT, cfg.region);
225
+ templatesBucket = (0, io_1.stackOutput)((0, config_1.bootstrapStackName)(cfg), config_1.BOOTSTRAP_TEMPLATES_OUTPUT, cfg.region);
226
+ if (!artifactsBucket) {
227
+ console.error(" ✗ bootstrap stack created but its ArtifactsBucket output could not be read.");
228
+ process.exit(1);
229
+ }
230
+ }
231
+ console.log(` ✓ artifacts bucket: ${artifactsBucket}`);
232
+ // 2. Upload the placeholder the function is created from.
233
+ console.log("\n▸ Uploading the Lambda placeholder bundle");
234
+ if (!(0, io_1.uploadPlaceholder)(artifactsBucket, cfg.region)) {
235
+ console.error(" ✗ deploy/cloudformation/placeholder/bootstrap not found — cannot create the function.");
236
+ process.exit(1);
237
+ }
238
+ // 3. Deploy the per-app backend with the resolved bucket wired in.
239
+ cfg = (0, config_1.mergeConfig)(cfg, {
240
+ artifactsBucket,
241
+ templatesBucket: templatesBucket ?? undefined,
242
+ });
243
+ console.log(`\n▸ Deploying ${cfg.stackName}`);
244
+ try {
245
+ (0, io_1.awsRun)((0, config_1.deployStackArgs)(cfg));
246
+ }
247
+ catch (err) {
248
+ console.error(`\n ✗ aws cloudformation deploy failed for ${cfg.stackName}.\n ${err.message}`);
249
+ process.exit(1);
250
+ }
251
+ // 4. Read the Outputs back into the config so CI has them without a live lookup.
252
+ const outputs = readAppOutputs(cfg);
253
+ cfg = (0, config_1.mergeConfig)(cfg, { outputs });
254
+ (0, io_1.saveConfig)(cfg);
255
+ return cfg;
256
+ }
257
+ /** Deploy the bootstrap stack, retrying once against an existing OIDC provider. */
258
+ function deployBootstrap(cfg) {
259
+ try {
260
+ (0, io_1.awsRun)((0, config_1.bootstrapStackArgs)(cfg));
261
+ }
262
+ catch {
263
+ // The most common cause is an account that already has a GitHub OIDC
264
+ // provider (only one is allowed per URL). Retry telling the template to
265
+ // reuse it instead of creating a second.
266
+ console.log(" • retrying bootstrap, reusing the account's existing GitHub OIDC provider");
267
+ try {
268
+ (0, io_1.awsRun)((0, config_1.bootstrapStackArgs)(cfg, true));
269
+ }
270
+ catch (err) {
271
+ console.error(`\n ✗ bootstrap stack deploy failed.\n ${err.message}`);
272
+ process.exit(1);
273
+ }
274
+ }
275
+ }
276
+ function printSummary(cfg) {
277
+ console.log(`\n ✓ ${cfg.app} is deployed (${cfg.stackName} @ ${cfg.region})`);
278
+ if (cfg.outputs?.apiUrl)
279
+ console.log(` API: ${cfg.outputs.apiUrl}`);
280
+ console.log("\n Wire these GitHub repo Variables so CI can ship code (Settings → Variables):");
281
+ console.log(table((0, config_1.repoVariables)(cfg).map((v) => ({
282
+ key: v.name,
283
+ value: v.value || `<${v.from}>`,
284
+ }))));
285
+ console.log(`\n Then every push to ${cfg.github.branch} ships code via \`rastack ci deploy\`` +
286
+ ` (also runnable locally as \`rastack deploy\`).\n`);
287
+ }
136
288
  async function main(argv) {
137
289
  const flags = parseFlags(argv);
138
290
  let cfg = seedConfig(flags);
139
291
  if ((0, io_1.canPrompt)(flags.nonInteractive)) {
140
- console.log("\n rastack init — scaffold the deploy config + GitHub Actions\n");
292
+ console.log("\n rastack init — scaffold the deploy config + stand the backend up\n");
141
293
  cfg = await promptMissing(cfg);
142
294
  }
143
295
  const errors = (0, config_1.validateConfig)(cfg);
144
- // In non-interactive mode a still-incomplete config is fatal; interactively we
145
- // still write what we have so the user can finish it in the file, but warn.
296
+ // Any deploy needs a complete config; a still-incomplete one is fatal unless we
297
+ // are only scaffolding files.
146
298
  if (errors.length) {
147
- const label = flags.nonInteractive ? "✗" : "⚠";
299
+ const fatal = !flags.noDeploy || flags.nonInteractive;
300
+ const label = fatal ? "✗" : "⚠";
148
301
  for (const e of errors)
149
302
  console.error(` ${label} ${e}`);
150
- if (flags.nonInteractive) {
151
- console.error(`\n ${errors.length} problem(s). Pass the missing values as flags.`);
303
+ if (fatal) {
304
+ console.error(`\n ${errors.length} problem(s). Pass the missing values as flags, or --no-deploy to scaffold only.`);
152
305
  process.exit(1);
153
306
  }
154
307
  }
308
+ // 1. Scaffold the config + workflows.
155
309
  const files = (0, workflows_1.scaffoldFiles)(cfg, { noWorkflows: flags.noWorkflows });
156
310
  const written = [];
157
311
  const skipped = [];
@@ -170,10 +324,25 @@ async function main(argv) {
170
324
  console.log(` ✓ wrote ${w}`);
171
325
  for (const s of skipped)
172
326
  console.log(` • skipped ${s} (exists — pass --force to overwrite)`);
173
- console.log(`\n Next:\n` +
174
- ` 1. rastack deploy — review the config and print the stack launch command\n` +
175
- ` 2. create the CloudFormation stack (once), then wire the repo Variables it prints\n` +
176
- ` 3. push to ${cfg.github.branch} — CI runs \`rastack ci deploy\`\n`);
327
+ // 2. Deploy (unless opted out).
328
+ if (flags.print) {
329
+ printPlan(cfg);
330
+ return;
331
+ }
332
+ if (flags.noDeploy) {
333
+ console.log(`\n Config scaffolded. Run \`rastack init\` again (without --no-deploy) to deploy,\n` +
334
+ ` or \`rastack init --print\` to see the exact commands.\n`);
335
+ return;
336
+ }
337
+ if ((0, io_1.canPrompt)(flags.nonInteractive)) {
338
+ const ok = await confirm(`Deploy ${cfg.stackName} to AWS ${cfg.region} now? (creates the bootstrap stack if needed) [y/N]`);
339
+ if (!ok) {
340
+ console.log("\n Skipped the deploy. Re-run to deploy, or `rastack init --print` for the commands.\n");
341
+ return;
342
+ }
343
+ }
344
+ cfg = await standUp(cfg);
345
+ printSummary(cfg);
177
346
  }
178
347
  if (require.main === module) {
179
348
  main(process.argv.slice(2)).catch((err) => {
@@ -0,0 +1,18 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * `rastack update` — update the CLI in place.
4
+ *
5
+ * rastack update [<version>] [--version <v>] [--check] [--dry-run]
6
+ *
7
+ * (no args) update to the latest published version
8
+ * <version> pin a target, e.g. `rastack update 0.0.40`
9
+ * --check report current vs. latest, install nothing
10
+ * --dry-run print the npm command that would run, don't run it
11
+ *
12
+ * `rastack` is a global npm package installed into a self-contained per-user
13
+ * prefix (`~/.rastack`, via the hosted installer). Updating re-runs
14
+ * `npm install -g rastack@<target>` against that SAME prefix — derived from where
15
+ * the running CLI lives — so it never scatters a second copy into npm's default
16
+ * global location. Pure decisions live in `update.ts`; this is the IO wrapper.
17
+ */
18
+ export {};
@@ -0,0 +1,139 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+ /**
4
+ * `rastack update` — update the CLI in place.
5
+ *
6
+ * rastack update [<version>] [--version <v>] [--check] [--dry-run]
7
+ *
8
+ * (no args) update to the latest published version
9
+ * <version> pin a target, e.g. `rastack update 0.0.40`
10
+ * --check report current vs. latest, install nothing
11
+ * --dry-run print the npm command that would run, don't run it
12
+ *
13
+ * `rastack` is a global npm package installed into a self-contained per-user
14
+ * prefix (`~/.rastack`, via the hosted installer). Updating re-runs
15
+ * `npm install -g rastack@<target>` against that SAME prefix — derived from where
16
+ * the running CLI lives — so it never scatters a second copy into npm's default
17
+ * global location. Pure decisions live in `update.ts`; this is the IO wrapper.
18
+ */
19
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
20
+ if (k2 === undefined) k2 = k;
21
+ var desc = Object.getOwnPropertyDescriptor(m, k);
22
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
23
+ desc = { enumerable: true, get: function() { return m[k]; } };
24
+ }
25
+ Object.defineProperty(o, k2, desc);
26
+ }) : (function(o, m, k, k2) {
27
+ if (k2 === undefined) k2 = k;
28
+ o[k2] = m[k];
29
+ }));
30
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
31
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
32
+ }) : function(o, v) {
33
+ o["default"] = v;
34
+ });
35
+ var __importStar = (this && this.__importStar) || (function () {
36
+ var ownKeys = function(o) {
37
+ ownKeys = Object.getOwnPropertyNames || function (o) {
38
+ var ar = [];
39
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
40
+ return ar;
41
+ };
42
+ return ownKeys(o);
43
+ };
44
+ return function (mod) {
45
+ if (mod && mod.__esModule) return mod;
46
+ var result = {};
47
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
48
+ __setModuleDefault(result, mod);
49
+ return result;
50
+ };
51
+ })();
52
+ Object.defineProperty(exports, "__esModule", { value: true });
53
+ const child_process_1 = require("child_process");
54
+ const fs_1 = require("fs");
55
+ const path = __importStar(require("path"));
56
+ const update_1 = require("./update");
57
+ /** The installed rastack package directory (…/rastack), one up from dist/. */
58
+ const PKG_DIR = path.join(__dirname, "..");
59
+ const PKG_JSON = path.join(PKG_DIR, "package.json");
60
+ /** Read the version from a package.json on disk, or "unknown" if unreadable. */
61
+ function readVersion(file) {
62
+ try {
63
+ return JSON.parse((0, fs_1.readFileSync)(file, "utf-8")).version ?? "unknown";
64
+ }
65
+ catch {
66
+ return "unknown";
67
+ }
68
+ }
69
+ /** Capture `npm <args>` stdout, or null when npm is absent / the call fails. */
70
+ function npmCapture(args) {
71
+ try {
72
+ return (0, child_process_1.execFileSync)("npm", args, {
73
+ stdio: ["ignore", "pipe", "pipe"],
74
+ })
75
+ .toString()
76
+ .trim();
77
+ }
78
+ catch {
79
+ return null;
80
+ }
81
+ }
82
+ function main(argv) {
83
+ const flags = (0, update_1.parseUpdateFlags)(argv);
84
+ const current = readVersion(PKG_JSON);
85
+ const prefix = (0, update_1.globalPrefixFromPackageDir)(PKG_DIR);
86
+ // The npm prefix must land the update where this CLI actually lives.
87
+ const env = { ...process.env };
88
+ if (prefix)
89
+ env.npm_config_prefix = prefix;
90
+ if (flags.check) {
91
+ const latest = npmCapture(["view", "rastack@latest", "version"]);
92
+ if (!latest) {
93
+ console.error(" ✗ could not reach the npm registry to check for updates.");
94
+ process.exit(1);
95
+ }
96
+ console.log(` rastack ${current} (installed) — latest is ${latest}`);
97
+ if ((0, update_1.updateAvailable)(current, latest)) {
98
+ console.log(` ▸ run \`rastack update\` to upgrade to ${latest}.`);
99
+ }
100
+ else {
101
+ console.log(" ✓ you are on the latest version.");
102
+ }
103
+ return;
104
+ }
105
+ const args = (0, update_1.npmInstallArgs)(flags.version);
106
+ if (flags.dryRun) {
107
+ const pfx = prefix ? `npm_config_prefix=${prefix} ` : "";
108
+ console.log(`\n ${pfx}npm ${args.join(" ")}\n`);
109
+ return;
110
+ }
111
+ if (!prefix) {
112
+ // Running from a source checkout (not a global install) — nothing to self-update.
113
+ console.error(" ✗ rastack is not running from a global install, so `update` does not apply.\n" +
114
+ " Reinstall with the hosted installer, or `npm install -g rastack` to manage it.");
115
+ process.exit(1);
116
+ }
117
+ console.log(`\n▸ Updating rastack (${current} → ${flags.version}) in ${prefix}\n`);
118
+ try {
119
+ // Re-runs the developer's own npm to update this global package — no
120
+ // untrusted input, and execFileSync (no shell) avoids any injection.
121
+ // nosemgrep: javascript.lang.security.detect-child-process.detect-child-process
122
+ (0, child_process_1.execFileSync)("npm", args, { stdio: "inherit", env });
123
+ }
124
+ catch (err) {
125
+ console.error(`\n ✗ npm failed to update rastack. Re-run for detail:\n npm install -g ${(0, update_1.npmInstallArgs)(flags.version)[2]}\n ${err.message}`);
126
+ process.exit(1);
127
+ }
128
+ // Re-read the (now replaced) package.json to report where we landed.
129
+ const updated = readVersion(PKG_JSON);
130
+ if (updated === current) {
131
+ console.log(`\n ✓ rastack is up to date (${updated}).\n`);
132
+ }
133
+ else {
134
+ console.log(`\n ✓ rastack updated ${current} → ${updated}.\n`);
135
+ }
136
+ }
137
+ if (require.main === module) {
138
+ main(process.argv.slice(2));
139
+ }
package/dist/rastack.d.ts CHANGED
@@ -15,8 +15,9 @@
15
15
  * rastack wasm Compile the API to WebAssembly (in-browser local dev)
16
16
  * rastack scan [files...] [--fail-on-pii] Scan CSVs for PII and schema info
17
17
  * rastack seed [--env dev] [--source gs://] Load CSVs into Iceberg namespace
18
- * rastack init [--app slug] [--force] Template the base GitHub Actions + deploy config
19
- * rastack deploy [--print] [--run] Configure the serverless deployment
18
+ * rastack init [--app slug] [--force] Scaffold config + GitHub Actions AND stand the backend up
19
+ * rastack deploy [--plan] Ship code to the Lambda (alias for `rastack ci deploy`)
20
20
  * rastack ci <check|build|deploy> [--plan] Run the CI pipeline (keeps Actions a one-liner)
21
+ * rastack update [<version>] [--check] Update the CLI in place (self-update)
21
22
  */
22
23
  export {};
package/dist/rastack.js CHANGED
@@ -16,9 +16,10 @@
16
16
  * rastack wasm Compile the API to WebAssembly (in-browser local dev)
17
17
  * rastack scan [files...] [--fail-on-pii] Scan CSVs for PII and schema info
18
18
  * rastack seed [--env dev] [--source gs://] Load CSVs into Iceberg namespace
19
- * rastack init [--app slug] [--force] Template the base GitHub Actions + deploy config
20
- * rastack deploy [--print] [--run] Configure the serverless deployment
19
+ * rastack init [--app slug] [--force] Scaffold config + GitHub Actions AND stand the backend up
20
+ * rastack deploy [--plan] Ship code to the Lambda (alias for `rastack ci deploy`)
21
21
  * rastack ci <check|build|deploy> [--plan] Run the CI pipeline (keeps Actions a one-liner)
22
+ * rastack update [<version>] [--check] Update the CLI in place (self-update)
22
23
  */
23
24
  var __importDefault = (this && this.__importDefault) || function (mod) {
24
25
  return (mod && mod.__esModule) ? mod : { "default": mod };
@@ -113,17 +114,23 @@ switch (command) {
113
114
  run("seed.js");
114
115
  break;
115
116
  case "init":
116
- // Template the base GitHub Actions + deploy config into a repo.
117
+ // Scaffold config + GitHub Actions and stand the serverless backend up.
117
118
  run("rastack-init.js", rest);
118
119
  break;
119
120
  case "deploy":
120
- // Configure the serverless deployment (writes .rastack/deploy.json).
121
- run("rastack-deploy.js", rest);
121
+ // Ship code to the Lambda — an alias for the CI code deploy. Infrastructure
122
+ // is stood up once by `rastack init`; `deploy` only ever ships code.
123
+ run("rastack-ci.js", ["deploy", ...rest]);
122
124
  break;
123
125
  case "ci":
124
126
  // Run a CI pipeline phase (check | build | deploy).
125
127
  run("rastack-ci.js", rest);
126
128
  break;
129
+ case "update":
130
+ case "upgrade":
131
+ // Self-update the globally-installed CLI in place.
132
+ run("rastack-update.js", rest);
133
+ break;
127
134
  case "generate":
128
135
  case "gen":
129
136
  // Proxy to existing rastack codegen pipeline
@@ -141,8 +148,9 @@ switch (command) {
141
148
  ` rastack wasm Compile the API to WebAssembly (browser local dev)\n` +
142
149
  ` rastack scan [files...] [--fail-on-pii]\n` +
143
150
  ` rastack seed [--env dev|staging] [--source gs://bucket/ | ./myapp-dev/]\n` +
144
- ` rastack init [--app slug] [--owner o] [--repo r] [--force] Template GitHub Actions + deploy config\n` +
145
- ` rastack deploy [--app slug] [--print] [--run] Configure the serverless deployment\n` +
146
- ` rastack ci <check|build|deploy> [--plan] Run the CI pipeline (cleaner Actions)\n`);
151
+ ` rastack init [--app slug] [--owner o] [--repo r] [--print] [--no-deploy] Scaffold + stand the backend up\n` +
152
+ ` rastack deploy [--plan] Ship code to the Lambda (alias for ci deploy)\n` +
153
+ ` rastack ci <check|build|deploy> [--plan] Run the CI pipeline (cleaner Actions)\n` +
154
+ ` rastack update [<version>] [--check] [--dry-run] Update the CLI in place (self-update)\n`);
147
155
  process.exit(1);
148
156
  }
@@ -0,0 +1,48 @@
1
+ /**
2
+ * Pure core of `rastack update` — the CLI's self-updater.
3
+ *
4
+ * `rastack` is installed with `npm install -g rastack` into a self-contained
5
+ * per-user prefix (`~/.rastack`, wired by web/public/setup.{sh,ps1}), so updating
6
+ * is just re-running that global install against the SAME prefix the running CLI
7
+ * lives in — then reporting the version change. Everything decidable without IO
8
+ * lives here (flag parsing, the npm argv, deriving the prefix from the install
9
+ * path, semver comparison) so it is unit-tested without npm or the network;
10
+ * `rastack-update.ts` is the thin wrapper that shells out.
11
+ */
12
+ export interface UpdateFlags {
13
+ /** `latest`, or a pinned semver like `0.0.40`. */
14
+ version: string;
15
+ /** Only report current vs. latest — do not install. */
16
+ check: boolean;
17
+ /** Print the npm command that would run, without running it. */
18
+ dryRun: boolean;
19
+ }
20
+ /**
21
+ * Parse `rastack update` args. Accepts `--check`, `--dry-run`/`--print`, and a
22
+ * version either as `--version <v>` or as a bare positional (`rastack update
23
+ * 0.0.40`). Unknown values default to `latest`.
24
+ */
25
+ export declare function parseUpdateFlags(argv: string[]): UpdateFlags;
26
+ /** The npm package spec to install for a target version. */
27
+ export declare function installSpec(version: string): string;
28
+ /** The `npm <args>` (after the `npm` binary) that installs the target globally. */
29
+ export declare function npmInstallArgs(version: string): string[];
30
+ /**
31
+ * Derive the npm global prefix from the installed rastack package directory, so
32
+ * the update lands in the SAME location the running CLI was installed to (not
33
+ * npm's default global prefix, which may differ from the per-user `~/.rastack`).
34
+ *
35
+ * Global installs put the package at `<prefix>/lib/node_modules/rastack` (POSIX)
36
+ * or `<prefix>/node_modules/rastack` (Windows). Returns null when rastack is not
37
+ * under a `node_modules` at all — e.g. running from a source checkout — where
38
+ * self-update does not apply.
39
+ */
40
+ export declare function globalPrefixFromPackageDir(pkgDir: string): string | null;
41
+ /**
42
+ * Compare two dotted versions numerically (major.minor.patch; any pre-release
43
+ * suffix is ignored). Returns -1 if a < b, 1 if a > b, 0 if equal — enough to
44
+ * tell `--check` whether an update is available.
45
+ */
46
+ export declare function compareSemver(a: string, b: string): number;
47
+ /** True when `latest` is a strictly newer version than `current`. */
48
+ export declare function updateAvailable(current: string, latest: string): boolean;
package/dist/update.js ADDED
@@ -0,0 +1,110 @@
1
+ "use strict";
2
+ /**
3
+ * Pure core of `rastack update` — the CLI's self-updater.
4
+ *
5
+ * `rastack` is installed with `npm install -g rastack` into a self-contained
6
+ * per-user prefix (`~/.rastack`, wired by web/public/setup.{sh,ps1}), so updating
7
+ * is just re-running that global install against the SAME prefix the running CLI
8
+ * lives in — then reporting the version change. Everything decidable without IO
9
+ * lives here (flag parsing, the npm argv, deriving the prefix from the install
10
+ * path, semver comparison) so it is unit-tested without npm or the network;
11
+ * `rastack-update.ts` is the thin wrapper that shells out.
12
+ */
13
+ Object.defineProperty(exports, "__esModule", { value: true });
14
+ exports.parseUpdateFlags = parseUpdateFlags;
15
+ exports.installSpec = installSpec;
16
+ exports.npmInstallArgs = npmInstallArgs;
17
+ exports.globalPrefixFromPackageDir = globalPrefixFromPackageDir;
18
+ exports.compareSemver = compareSemver;
19
+ exports.updateAvailable = updateAvailable;
20
+ /**
21
+ * Parse `rastack update` args. Accepts `--check`, `--dry-run`/`--print`, and a
22
+ * version either as `--version <v>` or as a bare positional (`rastack update
23
+ * 0.0.40`). Unknown values default to `latest`.
24
+ */
25
+ function parseUpdateFlags(argv) {
26
+ let version = "latest";
27
+ let check = false;
28
+ let dryRun = false;
29
+ for (let i = 0; i < argv.length; i++) {
30
+ const a = argv[i];
31
+ switch (a) {
32
+ case "--check":
33
+ check = true;
34
+ break;
35
+ case "--dry-run":
36
+ case "--print":
37
+ dryRun = true;
38
+ break;
39
+ case "--version":
40
+ case "--to":
41
+ version = argv[++i] ?? "latest";
42
+ break;
43
+ default:
44
+ // A bare, non-flag token is taken as the target version.
45
+ if (!a.startsWith("-"))
46
+ version = a;
47
+ }
48
+ }
49
+ return { version, check, dryRun };
50
+ }
51
+ /** The npm package spec to install for a target version. */
52
+ function installSpec(version) {
53
+ return !version || version === "latest" ? "rastack" : `rastack@${version}`;
54
+ }
55
+ /** The `npm <args>` (after the `npm` binary) that installs the target globally. */
56
+ function npmInstallArgs(version) {
57
+ return [
58
+ "install",
59
+ "-g",
60
+ installSpec(version),
61
+ "--no-fund",
62
+ "--no-audit",
63
+ "--loglevel=error",
64
+ ];
65
+ }
66
+ /**
67
+ * Derive the npm global prefix from the installed rastack package directory, so
68
+ * the update lands in the SAME location the running CLI was installed to (not
69
+ * npm's default global prefix, which may differ from the per-user `~/.rastack`).
70
+ *
71
+ * Global installs put the package at `<prefix>/lib/node_modules/rastack` (POSIX)
72
+ * or `<prefix>/node_modules/rastack` (Windows). Returns null when rastack is not
73
+ * under a `node_modules` at all — e.g. running from a source checkout — where
74
+ * self-update does not apply.
75
+ */
76
+ function globalPrefixFromPackageDir(pkgDir) {
77
+ const norm = pkgDir.replace(/\\/g, "/");
78
+ // Length-preserving normalization, so indices map back onto the original.
79
+ const posix = norm.lastIndexOf("/lib/node_modules/");
80
+ if (posix !== -1)
81
+ return pkgDir.slice(0, posix);
82
+ const win = norm.lastIndexOf("/node_modules/");
83
+ if (win !== -1)
84
+ return pkgDir.slice(0, win);
85
+ return null;
86
+ }
87
+ /**
88
+ * Compare two dotted versions numerically (major.minor.patch; any pre-release
89
+ * suffix is ignored). Returns -1 if a < b, 1 if a > b, 0 if equal — enough to
90
+ * tell `--check` whether an update is available.
91
+ */
92
+ function compareSemver(a, b) {
93
+ const parts = (v) => v
94
+ .replace(/^v/, "")
95
+ .split("-")[0]
96
+ .split(".")
97
+ .map((n) => parseInt(n, 10) || 0);
98
+ const pa = parts(a);
99
+ const pb = parts(b);
100
+ for (let i = 0; i < 3; i++) {
101
+ const d = (pa[i] ?? 0) - (pb[i] ?? 0);
102
+ if (d !== 0)
103
+ return d < 0 ? -1 : 1;
104
+ }
105
+ return 0;
106
+ }
107
+ /** True when `latest` is a strictly newer version than `current`. */
108
+ function updateAvailable(current, latest) {
109
+ return compareSemver(current, latest) < 0;
110
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "rastack",
3
- "version": "0.0.34",
3
+ "version": "0.0.36",
4
4
  "description": "",
5
5
  "main": "runtime.ts",
6
6
  "types": "runtime.ts",