rastack 0.0.40 → 0.0.42

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/CHANGELOG.md CHANGED
@@ -2,6 +2,15 @@
2
2
 
3
3
  All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines.
4
4
 
5
+ ### [0.0.42](https://github.com/theserverkid/reactapistack/compare/v0.0.41...v0.0.42) (2026-07-12)
6
+
7
+
8
+ ### Bug Fixes
9
+
10
+ * **deploy:** bundle CFN templates in the rastack package so init/bootstrap work from any repo ([4b45ee3](https://github.com/theserverkid/reactapistack/commit/4b45ee3440aef0355bc06c5b40bb1fa4f02e496f))
11
+
12
+ ### [0.0.41](https://github.com/theserverkid/reactapistack/compare/v0.0.40...v0.0.41) (2026-07-12)
13
+
5
14
  ### [0.0.40](https://github.com/theserverkid/reactapistack/compare/v0.0.39...v0.0.40) (2026-07-12)
6
15
 
7
16
 
@@ -136,17 +136,28 @@ export declare function cognitoAdvancedSecuritySupported(region: string): boolea
136
136
  * the template's ENFORCED default stands).
137
137
  */
138
138
  export declare function stackParameters(cfg: DeployConfig): StackParameter[];
139
- /** The in-repo CloudFormation templates `rastack init` deploys from. */
139
+ /**
140
+ * The canonical repo-relative CloudFormation template paths. These are the paths
141
+ * in the *framework* repo (theserverkid/reactapistack) and the default the argv
142
+ * builders below embed. A **consumer** repo (the demo, any app) deliberately does
143
+ * NOT carry a copy of the framework IaC — the templates ship inside the `rastack`
144
+ * package instead — so `init`/`bootstrap` resolve the real on-disk path via
145
+ * `resolveDeployAsset` (io.ts) and pass it in as the `templateFile` override.
146
+ */
140
147
  export declare const APP_TEMPLATE_FILE = "deploy/cloudformation/app.yaml";
141
148
  export declare const BOOTSTRAP_TEMPLATE_FILE = "deploy/cloudformation/bootstrap.yaml";
142
- /** The argv that creates the app's infrastructure (app.yaml). */
143
- export declare function deployStackArgs(cfg: DeployConfig): string[];
149
+ /**
150
+ * The argv that creates the app's infrastructure (app.yaml). `templateFile`
151
+ * defaults to the canonical repo-relative path; `init` passes the resolved
152
+ * package-bundled path so it works from a consumer repo that carries no IaC.
153
+ */
154
+ export declare function deployStackArgs(cfg: DeployConfig, templateFile?: string): string[];
144
155
  /**
145
156
  * The one-time `aws cloudformation deploy` command that creates the app's
146
157
  * infrastructure (app.yaml), from the in-repo template. `rastack init` runs this
147
158
  * for you; it is also printed by `--print` so it can be run by hand.
148
159
  */
149
- export declare function deployStackCommand(cfg: DeployConfig): string;
160
+ export declare function deployStackCommand(cfg: DeployConfig, templateFile?: string): string;
150
161
  /**
151
162
  * The tier-0 bootstrap stack (bootstrap.yaml) — the S3 buckets that hold the
152
163
  * Lambda placeholder + hosted templates, and the GitHub OIDC provider. One per
@@ -160,10 +171,15 @@ export declare function bootstrapStackName(cfg: DeployConfig): string;
160
171
  * the template then derives the existing provider's ARN itself.
161
172
  */
162
173
  export declare function bootstrapStackParameters(cfg: DeployConfig, reuseOidc?: boolean): StackParameter[];
163
- /** The argv that creates the bootstrap stack (bootstrap.yaml). */
164
- export declare function bootstrapStackArgs(cfg: DeployConfig, reuseOidc?: boolean): string[];
174
+ /**
175
+ * The argv that creates the bootstrap stack (bootstrap.yaml). `templateFile`
176
+ * defaults to the canonical repo-relative path; `init`/`bootstrap` pass the
177
+ * resolved package-bundled path so it works from a consumer repo that carries no
178
+ * IaC.
179
+ */
180
+ export declare function bootstrapStackArgs(cfg: DeployConfig, reuseOidc?: boolean, templateFile?: string): string[];
165
181
  /** The one-time `aws cloudformation deploy` command that creates the bootstrap stack. */
166
- export declare function bootstrapStackCommand(cfg: DeployConfig, reuseOidc?: boolean): string;
182
+ export declare function bootstrapStackCommand(cfg: DeployConfig, reuseOidc?: boolean, templateFile?: string): string;
167
183
  /** Stack Output keys `init` reads back off the bootstrap stack. */
168
184
  export declare const BOOTSTRAP_ARTIFACTS_OUTPUT = "ArtifactsBucketName";
169
185
  export declare const BOOTSTRAP_TEMPLATES_OUTPUT = "TemplatesBucketName";
@@ -232,7 +232,14 @@ function stackParameters(cfg) {
232
232
  params.push({ key: "AdvancedSecurity", value: "OFF" });
233
233
  return params;
234
234
  }
235
- /** The in-repo CloudFormation templates `rastack init` deploys from. */
235
+ /**
236
+ * The canonical repo-relative CloudFormation template paths. These are the paths
237
+ * in the *framework* repo (theserverkid/reactapistack) and the default the argv
238
+ * builders below embed. A **consumer** repo (the demo, any app) deliberately does
239
+ * NOT carry a copy of the framework IaC — the templates ship inside the `rastack`
240
+ * package instead — so `init`/`bootstrap` resolve the real on-disk path via
241
+ * `resolveDeployAsset` (io.ts) and pass it in as the `templateFile` override.
242
+ */
236
243
  exports.APP_TEMPLATE_FILE = "deploy/cloudformation/app.yaml";
237
244
  exports.BOOTSTRAP_TEMPLATE_FILE = "deploy/cloudformation/bootstrap.yaml";
238
245
  /**
@@ -244,7 +251,8 @@ exports.BOOTSTRAP_TEMPLATE_FILE = "deploy/cloudformation/bootstrap.yaml";
244
251
  * accepts *only* a local template file. `--template-url` is exclusive to the
245
252
  * low-level `create-stack`/`update-stack`/`create-change-set` commands, so
246
253
  * pairing it with `deploy` fails with `the following arguments are required:
247
- * --template-file`. `init` runs in the repo, where both templates are present.
254
+ * --template-file`. The file is resolved from the `rastack` package (see
255
+ * `resolveDeployAsset`), so a consumer repo need not carry the framework IaC.
248
256
  */
249
257
  function cfnDeployArgs(templateFile, stackName, region, params) {
250
258
  return [
@@ -272,17 +280,21 @@ function cfnDeployArgs(templateFile, stackName, region, params) {
272
280
  function awsCommand(args) {
273
281
  return `aws ${args.join(" ")}`;
274
282
  }
275
- /** The argv that creates the app's infrastructure (app.yaml). */
276
- function deployStackArgs(cfg) {
277
- return cfnDeployArgs(exports.APP_TEMPLATE_FILE, cfg.stackName, cfg.region, stackParameters(cfg));
283
+ /**
284
+ * The argv that creates the app's infrastructure (app.yaml). `templateFile`
285
+ * defaults to the canonical repo-relative path; `init` passes the resolved
286
+ * package-bundled path so it works from a consumer repo that carries no IaC.
287
+ */
288
+ function deployStackArgs(cfg, templateFile = exports.APP_TEMPLATE_FILE) {
289
+ return cfnDeployArgs(templateFile, cfg.stackName, cfg.region, stackParameters(cfg));
278
290
  }
279
291
  /**
280
292
  * The one-time `aws cloudformation deploy` command that creates the app's
281
293
  * infrastructure (app.yaml), from the in-repo template. `rastack init` runs this
282
294
  * for you; it is also printed by `--print` so it can be run by hand.
283
295
  */
284
- function deployStackCommand(cfg) {
285
- return awsCommand(deployStackArgs(cfg));
296
+ function deployStackCommand(cfg, templateFile = exports.APP_TEMPLATE_FILE) {
297
+ return awsCommand(deployStackArgs(cfg, templateFile));
286
298
  }
287
299
  /**
288
300
  * The tier-0 bootstrap stack (bootstrap.yaml) — the S3 buckets that hold the
@@ -309,13 +321,18 @@ function bootstrapStackParameters(cfg, reuseOidc = false) {
309
321
  params.push({ key: "CreateOIDCProvider", value: "false" });
310
322
  return params;
311
323
  }
312
- /** The argv that creates the bootstrap stack (bootstrap.yaml). */
313
- function bootstrapStackArgs(cfg, reuseOidc = false) {
314
- return cfnDeployArgs(exports.BOOTSTRAP_TEMPLATE_FILE, bootstrapStackName(cfg), cfg.region, bootstrapStackParameters(cfg, reuseOidc));
324
+ /**
325
+ * The argv that creates the bootstrap stack (bootstrap.yaml). `templateFile`
326
+ * defaults to the canonical repo-relative path; `init`/`bootstrap` pass the
327
+ * resolved package-bundled path so it works from a consumer repo that carries no
328
+ * IaC.
329
+ */
330
+ function bootstrapStackArgs(cfg, reuseOidc = false, templateFile = exports.BOOTSTRAP_TEMPLATE_FILE) {
331
+ return cfnDeployArgs(templateFile, bootstrapStackName(cfg), cfg.region, bootstrapStackParameters(cfg, reuseOidc));
315
332
  }
316
333
  /** The one-time `aws cloudformation deploy` command that creates the bootstrap stack. */
317
- function bootstrapStackCommand(cfg, reuseOidc = false) {
318
- return awsCommand(bootstrapStackArgs(cfg, reuseOidc));
334
+ function bootstrapStackCommand(cfg, reuseOidc = false, templateFile = exports.BOOTSTRAP_TEMPLATE_FILE) {
335
+ return awsCommand(bootstrapStackArgs(cfg, reuseOidc, templateFile));
319
336
  }
320
337
  /** Stack Output keys `init` reads back off the bootstrap stack. */
321
338
  exports.BOOTSTRAP_ARTIFACTS_OUTPUT = "ArtifactsBucketName";
@@ -7,6 +7,21 @@ import * as readline from "readline";
7
7
  import { DeployConfig } from "./config";
8
8
  /** Absolute path to the deploy config for a project root. */
9
9
  export declare function configPath(cwd?: string): string;
10
+ /**
11
+ * Resolve a CloudFormation asset (`app.yaml`, `bootstrap.yaml`, or the
12
+ * `placeholder/bootstrap` Lambda bundle) to an absolute on-disk path.
13
+ *
14
+ * The templates are the **framework's** IaC, not the consumer's — the demo repo
15
+ * and every future app repo deliberately do not carry a copy (see
16
+ * deploy/cloudformation/README.md). So `init`/`bootstrap`, which run in those
17
+ * repos, cannot assume `deploy/cloudformation/<asset>` exists relative to the
18
+ * cwd — that is exactly the `Invalid template path deploy/cloudformation/…`
19
+ * failure. Instead the assets ship *inside* the `rastack` package (copied into
20
+ * `dist/templates/cloudformation/` at build time) and are resolved from the
21
+ * install location, with fallbacks so running from source or from the framework
22
+ * repo itself still works. Returns null when no candidate exists.
23
+ */
24
+ export declare function resolveDeployAsset(relPath: string, cwd?: string): string | null;
10
25
  /** Read + normalize the deploy config, or null when it does not exist yet. */
11
26
  export declare function loadConfig(cwd?: string): DeployConfig | null;
12
27
  /** Write the deploy config, creating `.rastack/` if needed. */
package/dist/deploy/io.js CHANGED
@@ -39,6 +39,7 @@ var __importStar = (this && this.__importStar) || (function () {
39
39
  })();
40
40
  Object.defineProperty(exports, "__esModule", { value: true });
41
41
  exports.configPath = configPath;
42
+ exports.resolveDeployAsset = resolveDeployAsset;
42
43
  exports.loadConfig = loadConfig;
43
44
  exports.saveConfig = saveConfig;
44
45
  exports.gitRemoteUrl = gitRemoteUrl;
@@ -66,6 +67,35 @@ const zip_1 = require("./zip");
66
67
  function configPath(cwd = process.cwd()) {
67
68
  return path.join(cwd, workflows_1.CONFIG_PATH);
68
69
  }
70
+ /**
71
+ * Resolve a CloudFormation asset (`app.yaml`, `bootstrap.yaml`, or the
72
+ * `placeholder/bootstrap` Lambda bundle) to an absolute on-disk path.
73
+ *
74
+ * The templates are the **framework's** IaC, not the consumer's — the demo repo
75
+ * and every future app repo deliberately do not carry a copy (see
76
+ * deploy/cloudformation/README.md). So `init`/`bootstrap`, which run in those
77
+ * repos, cannot assume `deploy/cloudformation/<asset>` exists relative to the
78
+ * cwd — that is exactly the `Invalid template path deploy/cloudformation/…`
79
+ * failure. Instead the assets ship *inside* the `rastack` package (copied into
80
+ * `dist/templates/cloudformation/` at build time) and are resolved from the
81
+ * install location, with fallbacks so running from source or from the framework
82
+ * repo itself still works. Returns null when no candidate exists.
83
+ */
84
+ function resolveDeployAsset(relPath, cwd = process.cwd()) {
85
+ const candidates = [
86
+ // Installed package: dist/deploy/io.js → dist/templates/cloudformation/…
87
+ path.join(__dirname, "..", "templates", "cloudformation", relPath),
88
+ // Running from TS source: tools/src/deploy/io.ts → repo-root deploy/…
89
+ path.join(__dirname, "..", "..", "..", "deploy", "cloudformation", relPath),
90
+ // Invoked inside the framework repo itself (cwd is the repo root).
91
+ path.join(cwd, "deploy", "cloudformation", relPath),
92
+ ];
93
+ for (const candidate of candidates) {
94
+ if (fs.existsSync(candidate))
95
+ return candidate;
96
+ }
97
+ return null;
98
+ }
69
99
  /** Read + normalize the deploy config, or null when it does not exist yet. */
70
100
  function loadConfig(cwd = process.cwd()) {
71
101
  const file = configPath(cwd);
@@ -161,8 +191,8 @@ function stackOutput(stackName, key, region) {
161
191
  * on every `init`. Returns false when the placeholder file is missing.
162
192
  */
163
193
  function uploadPlaceholder(artifactsBucket, region, cwd = process.cwd()) {
164
- const src = path.join(cwd, "deploy/cloudformation/placeholder/bootstrap");
165
- if (!fs.existsSync(src))
194
+ const src = resolveDeployAsset("placeholder/bootstrap", cwd);
195
+ if (!src)
166
196
  return false;
167
197
  const zip = (0, zip_1.zipStore)([
168
198
  { name: "bootstrap", data: fs.readFileSync(src), mode: 0o755 },
@@ -8,7 +8,8 @@
8
8
  * [--region <r>] [--prefix <p>]
9
9
  * [--reuse-oidc] [--print] [--yes]
10
10
  *
11
- * It deploys `deploy/cloudformation/bootstrap.yaml`, which creates:
11
+ * It deploys the `bootstrap.yaml` template bundled with the `rastack` package
12
+ * (resolved by `resolveDeployAsset`, so it works from any repo), which creates:
12
13
  * • the S3 buckets that hold the Lambda placeholder + hosted templates, and
13
14
  * • the GitHub OIDC provider + trust that lets CI assume a deploy role.
14
15
  * It then uploads the Lambda placeholder bundle to the artifacts bucket, so the
@@ -9,7 +9,8 @@
9
9
  * [--region <r>] [--prefix <p>]
10
10
  * [--reuse-oidc] [--print] [--yes]
11
11
  *
12
- * It deploys `deploy/cloudformation/bootstrap.yaml`, which creates:
12
+ * It deploys the `bootstrap.yaml` template bundled with the `rastack` package
13
+ * (resolved by `resolveDeployAsset`, so it works from any repo), which creates:
13
14
  * • the S3 buckets that hold the Lambda placeholder + hosted templates, and
14
15
  * • the GitHub OIDC provider + trust that lets CI assume a deploy role.
15
16
  * It then uploads the Lambda placeholder bundle to the artifacts bucket, so the
@@ -116,8 +117,9 @@ async function confirm(question) {
116
117
  /** Print the exact command `bootstrap` would run, without touching the cloud. */
117
118
  function printPlan(cfg, reuseOidc) {
118
119
  console.log(`\n Bootstrap plan (${(0, config_1.bootstrapStackName)(cfg)} @ ${cfg.region})\n`);
120
+ const template = (0, io_1.resolveDeployAsset)("bootstrap.yaml") ?? undefined;
119
121
  console.log(" ── Tier-0 bootstrap stack (bootstrap.yaml) ──");
120
- console.log("\n " + (0, config_1.bootstrapStackCommand)(cfg, reuseOidc) + "\n");
122
+ console.log("\n " + (0, config_1.bootstrapStackCommand)(cfg, reuseOidc, template) + "\n");
121
123
  console.log(" Then `rastack init` (or `rastack bootstrap` again) resolves this stack's\n" +
122
124
  " ArtifactsBucket to create app backends. Nothing here is app-specific.\n");
123
125
  }
@@ -127,8 +129,14 @@ function printPlan(cfg, reuseOidc) {
127
129
  * already ask to reuse it.
128
130
  */
129
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
+ }
130
138
  try {
131
- (0, io_1.awsRun)((0, config_1.bootstrapStackArgs)(cfg, reuseOidc));
139
+ (0, io_1.awsRun)((0, config_1.bootstrapStackArgs)(cfg, reuseOidc, template));
132
140
  }
133
141
  catch (err) {
134
142
  if (reuseOidc) {
@@ -140,7 +148,7 @@ function deployBootstrap(cfg, reuseOidc) {
140
148
  // second (which CloudFormation rejects).
141
149
  console.log(" • retrying bootstrap, reusing the account's existing GitHub OIDC provider");
142
150
  try {
143
- (0, io_1.awsRun)((0, config_1.bootstrapStackArgs)(cfg, true));
151
+ (0, io_1.awsRun)((0, config_1.bootstrapStackArgs)(cfg, true, template));
144
152
  }
145
153
  catch (retryErr) {
146
154
  console.error(`\n ✗ bootstrap stack deploy failed.\n ${retryErr.message}`);
@@ -177,8 +185,8 @@ function standUp(cfg, reuseOidc) {
177
185
  // account is ready for `rastack init` without a second manual step.
178
186
  console.log("\n▸ Uploading the Lambda placeholder bundle");
179
187
  if (!(0, io_1.uploadPlaceholder)(artifactsBucket, cfg.region)) {
180
- console.error(" ✗ deploy/cloudformation/placeholder/bootstrap not found — the placeholder\n" +
181
- " was not uploaded. `rastack init` will upload it on the first app deploy.");
188
+ console.error(" ✗ Lambda placeholder bundle not found in the rastack package — the\n" +
189
+ " placeholder was not uploaded. `rastack init` will upload it on the first app deploy.");
182
190
  }
183
191
  else {
184
192
  console.log(" ✓ placeholder uploaded");
@@ -187,11 +187,13 @@ function printPlan(cfg) {
187
187
  const withBucket = (0, config_1.mergeConfig)(cfg, {
188
188
  artifactsBucket: bucket ?? "<ArtifactsBucketName from bootstrap stack>",
189
189
  });
190
+ const bootstrapTemplate = (0, io_1.resolveDeployAsset)("bootstrap.yaml") ?? undefined;
191
+ const appTemplate = (0, io_1.resolveDeployAsset)("app.yaml") ?? undefined;
190
192
  console.log(`\n Deployment plan (${cfg.stackName} @ ${cfg.region})\n`);
191
193
  console.log(` ── 1. Bootstrap stack (${(0, config_1.bootstrapStackName)(cfg)}) — run only if it does not exist ──`);
192
- console.log("\n " + (0, config_1.bootstrapStackCommand)(cfg) + "\n");
194
+ console.log("\n " + (0, config_1.bootstrapStackCommand)(cfg, false, bootstrapTemplate) + "\n");
193
195
  console.log(" ── 2. Per-app backend (app.yaml) ──");
194
- console.log("\n " + (0, config_1.deployStackCommand)(withBucket) + "\n");
196
+ console.log("\n " + (0, config_1.deployStackCommand)(withBucket, appTemplate) + "\n");
195
197
  console.log(" ── 3. Wire these GitHub repo Variables from the stack Outputs ──");
196
198
  console.log(table((0, config_1.repoVariables)(cfg).map((v) => ({
197
199
  key: v.name,
@@ -226,7 +228,7 @@ async function standUp(cfg, interactive) {
226
228
  // 2. Upload the placeholder the function is created from.
227
229
  console.log("\n▸ Uploading the Lambda placeholder bundle");
228
230
  if (!(0, io_1.uploadPlaceholder)(artifactsBucket, cfg.region)) {
229
- console.error(" ✗ deploy/cloudformation/placeholder/bootstrap not found — cannot create the function.");
231
+ console.error(" ✗ Lambda placeholder bundle not found in the rastack package — cannot create the function.");
230
232
  process.exit(1);
231
233
  }
232
234
  // 3. Deploy the per-app backend with the resolved bucket wired in.
@@ -234,9 +236,15 @@ async function standUp(cfg, interactive) {
234
236
  artifactsBucket,
235
237
  templatesBucket: templatesBucket ?? undefined,
236
238
  });
239
+ const appTemplate = (0, io_1.resolveDeployAsset)("app.yaml");
240
+ if (!appTemplate) {
241
+ console.error(" ✗ app.yaml template not found in the rastack package. Reinstall rastack\n" +
242
+ " (`npm i -g rastack`) or run from the framework repo.");
243
+ process.exit(1);
244
+ }
237
245
  console.log(`\n▸ Deploying ${cfg.stackName}`);
238
246
  try {
239
- (0, io_1.awsRun)((0, config_1.deployStackArgs)(cfg));
247
+ (0, io_1.awsRun)((0, config_1.deployStackArgs)(cfg, appTemplate));
240
248
  }
241
249
  catch (err) {
242
250
  console.error(`\n ✗ aws cloudformation deploy failed for ${cfg.stackName}.\n ${err.message}`);
@@ -299,8 +307,14 @@ function wireRepoVariables(cfg, interactive) {
299
307
  }
300
308
  /** Deploy the bootstrap stack, retrying once against an existing OIDC provider. */
301
309
  function deployBootstrap(cfg) {
310
+ const template = (0, io_1.resolveDeployAsset)("bootstrap.yaml");
311
+ if (!template) {
312
+ console.error(" ✗ bootstrap.yaml template not found in the rastack package. Reinstall\n" +
313
+ " rastack (`npm i -g rastack`) or run from the framework repo.");
314
+ process.exit(1);
315
+ }
302
316
  try {
303
- (0, io_1.awsRun)((0, config_1.bootstrapStackArgs)(cfg));
317
+ (0, io_1.awsRun)((0, config_1.bootstrapStackArgs)(cfg, false, template));
304
318
  }
305
319
  catch {
306
320
  // The most common cause is an account that already has a GitHub OIDC
@@ -308,7 +322,7 @@ function deployBootstrap(cfg) {
308
322
  // reuse it instead of creating a second.
309
323
  console.log(" • retrying bootstrap, reusing the account's existing GitHub OIDC provider");
310
324
  try {
311
- (0, io_1.awsRun)((0, config_1.bootstrapStackArgs)(cfg, true));
325
+ (0, io_1.awsRun)((0, config_1.bootstrapStackArgs)(cfg, true, template));
312
326
  }
313
327
  catch (err) {
314
328
  console.error(`\n ✗ bootstrap stack deploy failed.\n ${err.message}`);
@@ -56,6 +56,12 @@ const path = __importStar(require("path"));
56
56
  const update_1 = require("./update");
57
57
  /** The npm executable for this platform (`npm.cmd` on Windows, else `npm`). */
58
58
  const NPM = (0, update_1.npmBinary)();
59
+ /**
60
+ * Whether to launch npm through a shell. Required on Windows, where a bare
61
+ * `.cmd` spawn is rejected with `EINVAL` since the Node CVE-2024-27980 fix;
62
+ * false on POSIX so the direct spawn stays injection-free.
63
+ */
64
+ const NPM_SHELL = (0, update_1.npmSpawnShell)();
59
65
  /** The installed rastack package directory (…/rastack), one up from dist/. */
60
66
  const PKG_DIR = path.join(__dirname, "..");
61
67
  const PKG_JSON = path.join(PKG_DIR, "package.json");
@@ -73,6 +79,7 @@ function npmCapture(args) {
73
79
  try {
74
80
  return (0, child_process_1.execFileSync)(NPM, args, {
75
81
  stdio: ["ignore", "pipe", "pipe"],
82
+ shell: NPM_SHELL,
76
83
  })
77
84
  .toString()
78
85
  .trim();
@@ -118,10 +125,12 @@ function main(argv) {
118
125
  }
119
126
  console.log(`\n▸ Updating rastack (${current} → ${flags.version}) in ${prefix}\n`);
120
127
  try {
121
- // Re-runs the developer's own npm to update this global package — no
122
- // untrusted input, and execFileSync (no shell) avoids any injection.
128
+ // Re-runs the developer's own npm to update this global package. On Windows
129
+ // the `.cmd` shim is launched through a shell (EINVAL otherwise); the only
130
+ // caller-supplied token — the version — is sanitized in `parseUpdateFlags`,
131
+ // and every other arg is a fixed literal, so nothing injectable reaches it.
123
132
  // nosemgrep: javascript.lang.security.detect-child-process.detect-child-process
124
- (0, child_process_1.execFileSync)(NPM, args, { stdio: "inherit", env });
133
+ (0, child_process_1.execFileSync)(NPM, args, { stdio: "inherit", env, shell: NPM_SHELL });
125
134
  }
126
135
  catch (err) {
127
136
  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}`);