rastack 0.0.35 → 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.
- package/CHANGELOG.md +2 -0
- package/dist/deploy/config.d.ts +33 -2
- package/dist/deploy/config.js +97 -18
- package/dist/deploy/index.d.ts +1 -0
- package/dist/deploy/index.js +1 -0
- package/dist/deploy/io.d.ts +19 -0
- package/dist/deploy/io.js +81 -0
- package/dist/deploy/zip.d.ts +24 -0
- package/dist/deploy/zip.js +95 -0
- package/dist/rastack-ci.js +1 -1
- package/dist/rastack-init.d.ts +18 -7
- package/dist/rastack-init.js +187 -18
- package/dist/rastack-update.d.ts +18 -0
- package/dist/rastack-update.js +139 -0
- package/dist/rastack.d.ts +3 -2
- package/dist/rastack.js +16 -8
- package/dist/update.d.ts +48 -0
- package/dist/update.js +110 -0
- package/package.json +1 -1
- package/src/deploy/config.ts +125 -17
- package/src/deploy/index.ts +1 -0
- package/src/deploy/io.ts +88 -1
- package/src/deploy/zip.ts +109 -0
- package/src/rastack-ci.ts +1 -1
- package/src/rastack-init.ts +271 -21
- package/src/rastack-update.ts +124 -0
- package/src/rastack.ts +16 -8
- package/src/update.ts +114 -0
- package/test/deploy.spec.ts +93 -0
- package/test/update.spec.ts +95 -0
- package/dist/rastack-deploy.d.ts +0 -20
- package/dist/rastack-deploy.js +0 -196
- package/src/rastack-deploy.ts +0 -257
package/src/deploy/config.ts
CHANGED
|
@@ -252,29 +252,137 @@ export function templateUrl(cfg: DeployConfig): string | null {
|
|
|
252
252
|
return `https://${cfg.templatesBucket}.s3.${cfg.region}.amazonaws.com/app.yaml`;
|
|
253
253
|
}
|
|
254
254
|
|
|
255
|
+
/** The in-repo CloudFormation templates `rastack init` deploys from. */
|
|
256
|
+
export const APP_TEMPLATE_FILE = "deploy/cloudformation/app.yaml";
|
|
257
|
+
export const BOOTSTRAP_TEMPLATE_FILE = "deploy/cloudformation/bootstrap.yaml";
|
|
258
|
+
|
|
259
|
+
/**
|
|
260
|
+
* The argv (after `aws`) for an `aws cloudformation deploy`. `init` feeds this to
|
|
261
|
+
* `execFileSync` (no shell), and the printed command string is just this joined
|
|
262
|
+
* with spaces — so what is shown is exactly what runs.
|
|
263
|
+
*/
|
|
264
|
+
function cfnDeployArgs(
|
|
265
|
+
template: { file?: string; url?: string },
|
|
266
|
+
stackName: string,
|
|
267
|
+
region: string,
|
|
268
|
+
params: StackParameter[],
|
|
269
|
+
): string[] {
|
|
270
|
+
const t = template.url
|
|
271
|
+
? ["--template-url", template.url]
|
|
272
|
+
: ["--template-file", template.file!];
|
|
273
|
+
return [
|
|
274
|
+
"cloudformation",
|
|
275
|
+
"deploy",
|
|
276
|
+
...t,
|
|
277
|
+
"--stack-name",
|
|
278
|
+
stackName,
|
|
279
|
+
"--capabilities",
|
|
280
|
+
"CAPABILITY_NAMED_IAM",
|
|
281
|
+
"--region",
|
|
282
|
+
region,
|
|
283
|
+
"--parameter-overrides",
|
|
284
|
+
...params.map((p) => `${p.key}=${p.value}`),
|
|
285
|
+
];
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
/**
|
|
289
|
+
* Render an argv as a single-line command string. Deliberately ONE line: a
|
|
290
|
+
* backslash-continued command pasted into Windows PowerShell breaks — there `\`
|
|
291
|
+
* is not a line-continuation, so every `--flag` is parsed as its own statement
|
|
292
|
+
* (`Missing expression after unary operator '--'`). A single line pastes cleanly
|
|
293
|
+
* into bash, zsh and PowerShell alike.
|
|
294
|
+
*/
|
|
295
|
+
function awsCommand(args: string[]): string {
|
|
296
|
+
return `aws ${args.join(" ")}`;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
/** The argv that creates the app's infrastructure (app.yaml). */
|
|
300
|
+
export function deployStackArgs(cfg: DeployConfig): string[] {
|
|
301
|
+
const src = templateUrl(cfg);
|
|
302
|
+
return cfnDeployArgs(
|
|
303
|
+
src ? { url: src } : { file: APP_TEMPLATE_FILE },
|
|
304
|
+
cfg.stackName,
|
|
305
|
+
cfg.region,
|
|
306
|
+
stackParameters(cfg),
|
|
307
|
+
);
|
|
308
|
+
}
|
|
309
|
+
|
|
255
310
|
/**
|
|
256
311
|
* The one-time `aws cloudformation deploy` command that creates the app's
|
|
257
|
-
* infrastructure. Launched from the hosted template URL when the
|
|
258
|
-
* bucket is known, else from the in-repo `
|
|
312
|
+
* infrastructure (app.yaml). Launched from the hosted template URL when the
|
|
313
|
+
* templates bucket is known, else from the in-repo `app.yaml`. `rastack init`
|
|
314
|
+
* runs this for you; it is also printed by `--print` so it can be run by hand.
|
|
259
315
|
*/
|
|
260
316
|
export function deployStackCommand(cfg: DeployConfig): string {
|
|
261
|
-
|
|
262
|
-
const templateArg = src
|
|
263
|
-
? `--template-url ${src}`
|
|
264
|
-
: "--template-file deploy/cloudformation/app.yaml";
|
|
265
|
-
const overrides = stackParameters(cfg)
|
|
266
|
-
.map((p) => `${p.key}=${p.value}`)
|
|
267
|
-
.join(" ");
|
|
268
|
-
return [
|
|
269
|
-
"aws cloudformation deploy",
|
|
270
|
-
templateArg,
|
|
271
|
-
`--stack-name ${cfg.stackName}`,
|
|
272
|
-
"--capabilities CAPABILITY_NAMED_IAM",
|
|
273
|
-
`--region ${cfg.region}`,
|
|
274
|
-
`--parameter-overrides ${overrides}`,
|
|
275
|
-
].join(" \\\n ");
|
|
317
|
+
return awsCommand(deployStackArgs(cfg));
|
|
276
318
|
}
|
|
277
319
|
|
|
320
|
+
/**
|
|
321
|
+
* The tier-0 bootstrap stack (bootstrap.yaml) — the S3 buckets that hold the
|
|
322
|
+
* Lambda placeholder + hosted templates, and the GitHub OIDC provider. One per
|
|
323
|
+
* account **and region**; its name is derived from the prefix so `init` can find
|
|
324
|
+
* it (and its `ArtifactsBucket`) without extra config.
|
|
325
|
+
*/
|
|
326
|
+
export function bootstrapStackName(cfg: DeployConfig): string {
|
|
327
|
+
return `${cfg.namePrefix}-bootstrap`;
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
/**
|
|
331
|
+
* `--parameter-overrides` for bootstrap.yaml. `reuseOidc` re-runs against an
|
|
332
|
+
* account that already has a GitHub OIDC provider (AWS allows only one per URL):
|
|
333
|
+
* the template then derives the existing provider's ARN itself.
|
|
334
|
+
*/
|
|
335
|
+
export function bootstrapStackParameters(
|
|
336
|
+
cfg: DeployConfig,
|
|
337
|
+
reuseOidc = false,
|
|
338
|
+
): StackParameter[] {
|
|
339
|
+
const params: StackParameter[] = [
|
|
340
|
+
{ key: "NamePrefix", value: cfg.namePrefix },
|
|
341
|
+
{ key: "GitHubOwner", value: cfg.github.owner },
|
|
342
|
+
{ key: "PublisherRepo", value: cfg.github.repo },
|
|
343
|
+
{ key: "PublisherBranch", value: cfg.github.branch },
|
|
344
|
+
];
|
|
345
|
+
if (reuseOidc) params.push({ key: "CreateOIDCProvider", value: "false" });
|
|
346
|
+
return params;
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
/** The argv that creates the bootstrap stack (bootstrap.yaml). */
|
|
350
|
+
export function bootstrapStackArgs(
|
|
351
|
+
cfg: DeployConfig,
|
|
352
|
+
reuseOidc = false,
|
|
353
|
+
): string[] {
|
|
354
|
+
return cfnDeployArgs(
|
|
355
|
+
{ file: BOOTSTRAP_TEMPLATE_FILE },
|
|
356
|
+
bootstrapStackName(cfg),
|
|
357
|
+
cfg.region,
|
|
358
|
+
bootstrapStackParameters(cfg, reuseOidc),
|
|
359
|
+
);
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
/** The one-time `aws cloudformation deploy` command that creates the bootstrap stack. */
|
|
363
|
+
export function bootstrapStackCommand(
|
|
364
|
+
cfg: DeployConfig,
|
|
365
|
+
reuseOidc = false,
|
|
366
|
+
): string {
|
|
367
|
+
return awsCommand(bootstrapStackArgs(cfg, reuseOidc));
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
/** Stack Output keys `init` reads back off the bootstrap stack. */
|
|
371
|
+
export const BOOTSTRAP_ARTIFACTS_OUTPUT = "ArtifactsBucketName";
|
|
372
|
+
export const BOOTSTRAP_TEMPLATES_OUTPUT = "TemplatesBucketName";
|
|
373
|
+
|
|
374
|
+
/** app.yaml stack Output key → the `DeployOutputs` field it populates. */
|
|
375
|
+
export const APP_STACK_OUTPUTS: { key: string; field: keyof DeployOutputs }[] =
|
|
376
|
+
[
|
|
377
|
+
{ key: "FunctionName", field: "functionName" },
|
|
378
|
+
{ key: "ApiUrl", field: "apiUrl" },
|
|
379
|
+
{ key: "DeployRoleArn", field: "deployRoleArn" },
|
|
380
|
+
{ key: "UserPoolId", field: "userPoolId" },
|
|
381
|
+
{ key: "UserPoolClientId", field: "userPoolClientId" },
|
|
382
|
+
{ key: "JwksUrl", field: "jwksUrl" },
|
|
383
|
+
{ key: "WarehouseBucketName", field: "warehouseBucket" },
|
|
384
|
+
];
|
|
385
|
+
|
|
278
386
|
/** GitHub repo Variable to wire (name + resolved value, if known from outputs). */
|
|
279
387
|
export interface RepoVariable {
|
|
280
388
|
name: string;
|
package/src/deploy/index.ts
CHANGED
package/src/deploy/io.ts
CHANGED
|
@@ -4,12 +4,14 @@
|
|
|
4
4
|
* IO boundary they wrap.
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
-
import { execSync } from "child_process";
|
|
7
|
+
import { execFileSync, execSync } from "child_process";
|
|
8
8
|
import * as fs from "fs";
|
|
9
|
+
import * as os from "os";
|
|
9
10
|
import * as path from "path";
|
|
10
11
|
import * as readline from "readline";
|
|
11
12
|
import { CONFIG_PATH } from "./workflows";
|
|
12
13
|
import { DeployConfig, PartialDeployConfig, normalizeConfig } from "./config";
|
|
14
|
+
import { zipStore } from "./zip";
|
|
13
15
|
|
|
14
16
|
/** Absolute path to the deploy config for a project root. */
|
|
15
17
|
export function configPath(cwd = process.cwd()): string {
|
|
@@ -57,6 +59,91 @@ export function canPrompt(nonInteractive: boolean): boolean {
|
|
|
57
59
|
return !nonInteractive && Boolean(process.stdin.isTTY);
|
|
58
60
|
}
|
|
59
61
|
|
|
62
|
+
// ── AWS CLI boundary ─────────────────────────────────────────────────────────
|
|
63
|
+
// `init` shells out to the developer's own AWS CLI (execFileSync — no shell, so
|
|
64
|
+
// no injection surface) to stand the infrastructure up. Read helpers capture
|
|
65
|
+
// output and return null on failure; mutating helpers stream to the console and
|
|
66
|
+
// throw so the caller can report which step failed.
|
|
67
|
+
|
|
68
|
+
/** Capture `aws <args>` stdout, or null when the CLI is absent / the call fails. */
|
|
69
|
+
export function awsCapture(args: string[], cwd = process.cwd()): string | null {
|
|
70
|
+
try {
|
|
71
|
+
const out = execFileSync("aws", args, {
|
|
72
|
+
cwd,
|
|
73
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
74
|
+
});
|
|
75
|
+
return out.toString().trim();
|
|
76
|
+
} catch {
|
|
77
|
+
return null;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/** Run `aws <args>` streaming output; throws on a non-zero exit. */
|
|
82
|
+
export function awsRun(args: string[], cwd = process.cwd()): void {
|
|
83
|
+
// nosemgrep: javascript.lang.security.detect-child-process.detect-child-process
|
|
84
|
+
execFileSync("aws", args, { cwd, stdio: "inherit" });
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/** True when the AWS CLI is on PATH. */
|
|
88
|
+
export function awsAvailable(): boolean {
|
|
89
|
+
return awsCapture(["--version"]) !== null;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* A single Output value of a CloudFormation stack, or null when the stack does
|
|
94
|
+
* not exist or the key is unset. `describe-stacks` prints `None` for a missing
|
|
95
|
+
* key with `--output text`, which we normalize to null.
|
|
96
|
+
*/
|
|
97
|
+
export function stackOutput(
|
|
98
|
+
stackName: string,
|
|
99
|
+
key: string,
|
|
100
|
+
region: string,
|
|
101
|
+
): string | null {
|
|
102
|
+
const val = awsCapture([
|
|
103
|
+
"cloudformation",
|
|
104
|
+
"describe-stacks",
|
|
105
|
+
"--stack-name",
|
|
106
|
+
stackName,
|
|
107
|
+
"--region",
|
|
108
|
+
region,
|
|
109
|
+
"--query",
|
|
110
|
+
`Stacks[0].Outputs[?OutputKey=='${key}'].OutputValue`,
|
|
111
|
+
"--output",
|
|
112
|
+
"text",
|
|
113
|
+
]);
|
|
114
|
+
if (!val || val === "None") return null;
|
|
115
|
+
return val;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Package the in-repo Lambda placeholder and upload it to the bootstrap
|
|
120
|
+
* artifacts bucket at `placeholders/bootstrap.zip` — the object app.yaml's
|
|
121
|
+
* function is *created from*. Idempotent (the zip is deterministic); safe to run
|
|
122
|
+
* on every `init`. Returns false when the placeholder file is missing.
|
|
123
|
+
*/
|
|
124
|
+
export function uploadPlaceholder(
|
|
125
|
+
artifactsBucket: string,
|
|
126
|
+
region: string,
|
|
127
|
+
cwd = process.cwd(),
|
|
128
|
+
): boolean {
|
|
129
|
+
const src = path.join(cwd, "deploy/cloudformation/placeholder/bootstrap");
|
|
130
|
+
if (!fs.existsSync(src)) return false;
|
|
131
|
+
const zip = zipStore([
|
|
132
|
+
{ name: "bootstrap", data: fs.readFileSync(src), mode: 0o755 },
|
|
133
|
+
]);
|
|
134
|
+
const tmp = path.join(os.tmpdir(), "rastack-placeholder-bootstrap.zip");
|
|
135
|
+
fs.writeFileSync(tmp, zip);
|
|
136
|
+
awsRun([
|
|
137
|
+
"s3",
|
|
138
|
+
"cp",
|
|
139
|
+
tmp,
|
|
140
|
+
`s3://${artifactsBucket}/placeholders/bootstrap.zip`,
|
|
141
|
+
"--region",
|
|
142
|
+
region,
|
|
143
|
+
]);
|
|
144
|
+
return true;
|
|
145
|
+
}
|
|
146
|
+
|
|
60
147
|
/**
|
|
61
148
|
* Ask a single question with a default, echoed as `label [default]: `. Returns
|
|
62
149
|
* the trimmed answer, or the default on an empty line.
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A tiny, dependency-free ZIP writer used by `rastack init` to package the Lambda
|
|
3
|
+
* placeholder bundle (`placeholders/bootstrap.zip`) before uploading it to the
|
|
4
|
+
* bootstrap artifacts bucket.
|
|
5
|
+
*
|
|
6
|
+
* Why hand-roll it: `init` runs on the developer's machine — including Windows
|
|
7
|
+
* PowerShell, which has no `zip` binary — so shelling out to `zip` is not
|
|
8
|
+
* portable. Entries are STORED (no compression); the placeholder is ~1 KB, so
|
|
9
|
+
* compression buys nothing and STORE keeps this pure and trivial to verify. The
|
|
10
|
+
* one detail that matters for Lambda is the Unix executable bit on `bootstrap`,
|
|
11
|
+
* which is carried in the central-directory external attributes.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
/** A file to place in the archive. `mode` defaults to `0o644` (or `0o755` via helper). */
|
|
15
|
+
export interface ZipEntry {
|
|
16
|
+
name: string;
|
|
17
|
+
data: Buffer;
|
|
18
|
+
mode?: number;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const CRC_TABLE = (() => {
|
|
22
|
+
const table = new Int32Array(256);
|
|
23
|
+
for (let n = 0; n < 256; n++) {
|
|
24
|
+
let c = n;
|
|
25
|
+
for (let k = 0; k < 8; k++) c = c & 1 ? 0xedb88320 ^ (c >>> 1) : c >>> 1;
|
|
26
|
+
table[n] = c;
|
|
27
|
+
}
|
|
28
|
+
return table;
|
|
29
|
+
})();
|
|
30
|
+
|
|
31
|
+
function crc32(buf: Buffer): number {
|
|
32
|
+
let c = 0xffffffff;
|
|
33
|
+
for (let i = 0; i < buf.length; i++)
|
|
34
|
+
c = CRC_TABLE[(c ^ buf[i]) & 0xff] ^ (c >>> 8);
|
|
35
|
+
return (c ^ 0xffffffff) >>> 0;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// Fixed 1980-01-01 00:00 DOS timestamp — deterministic output (no wall-clock).
|
|
39
|
+
const DOS_TIME = 0;
|
|
40
|
+
const DOS_DATE = 0x0021;
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Build a ZIP archive from `entries` and return its bytes. Deterministic: the
|
|
44
|
+
* same inputs always produce the same buffer (a fixed DOS timestamp, no
|
|
45
|
+
* compression), so the placeholder upload is idempotent.
|
|
46
|
+
*/
|
|
47
|
+
export function zipStore(entries: ZipEntry[]): Buffer {
|
|
48
|
+
const locals: Buffer[] = [];
|
|
49
|
+
const centrals: Buffer[] = [];
|
|
50
|
+
let offset = 0;
|
|
51
|
+
|
|
52
|
+
for (const entry of entries) {
|
|
53
|
+
const name = Buffer.from(entry.name, "utf-8");
|
|
54
|
+
const crc = crc32(entry.data);
|
|
55
|
+
const size = entry.data.length;
|
|
56
|
+
const mode = entry.mode ?? 0o644;
|
|
57
|
+
|
|
58
|
+
const local = Buffer.alloc(30);
|
|
59
|
+
local.writeUInt32LE(0x04034b50, 0); // local file header signature
|
|
60
|
+
local.writeUInt16LE(20, 4); // version needed
|
|
61
|
+
local.writeUInt16LE(0, 6); // flags
|
|
62
|
+
local.writeUInt16LE(0, 8); // method: STORE
|
|
63
|
+
local.writeUInt16LE(DOS_TIME, 10);
|
|
64
|
+
local.writeUInt16LE(DOS_DATE, 12);
|
|
65
|
+
local.writeUInt32LE(crc, 14);
|
|
66
|
+
local.writeUInt32LE(size, 18); // compressed size
|
|
67
|
+
local.writeUInt32LE(size, 22); // uncompressed size
|
|
68
|
+
local.writeUInt16LE(name.length, 26);
|
|
69
|
+
local.writeUInt16LE(0, 28); // extra length
|
|
70
|
+
locals.push(local, name, entry.data);
|
|
71
|
+
|
|
72
|
+
const central = Buffer.alloc(46);
|
|
73
|
+
central.writeUInt32LE(0x02014b50, 0); // central directory signature
|
|
74
|
+
central.writeUInt16LE((3 << 8) | 20, 4); // version made by: Unix host
|
|
75
|
+
central.writeUInt16LE(20, 6); // version needed
|
|
76
|
+
central.writeUInt16LE(0, 8); // flags
|
|
77
|
+
central.writeUInt16LE(0, 10); // method: STORE
|
|
78
|
+
central.writeUInt16LE(DOS_TIME, 12);
|
|
79
|
+
central.writeUInt16LE(DOS_DATE, 14);
|
|
80
|
+
central.writeUInt32LE(crc, 16);
|
|
81
|
+
central.writeUInt32LE(size, 20);
|
|
82
|
+
central.writeUInt32LE(size, 24);
|
|
83
|
+
central.writeUInt16LE(name.length, 28);
|
|
84
|
+
central.writeUInt16LE(0, 30); // extra length
|
|
85
|
+
central.writeUInt16LE(0, 32); // comment length
|
|
86
|
+
central.writeUInt16LE(0, 34); // disk number
|
|
87
|
+
central.writeUInt16LE(0, 36); // internal attributes
|
|
88
|
+
central.writeUInt32LE((mode & 0xffff) << 16, 38); // external attrs: Unix mode
|
|
89
|
+
central.writeUInt32LE(offset, 42); // local header offset
|
|
90
|
+
centrals.push(central, name);
|
|
91
|
+
|
|
92
|
+
offset += local.length + name.length + entry.data.length;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const localBytes = Buffer.concat(locals);
|
|
96
|
+
const centralBytes = Buffer.concat(centrals);
|
|
97
|
+
|
|
98
|
+
const eocd = Buffer.alloc(22);
|
|
99
|
+
eocd.writeUInt32LE(0x06054b50, 0); // end of central directory signature
|
|
100
|
+
eocd.writeUInt16LE(0, 4); // disk number
|
|
101
|
+
eocd.writeUInt16LE(0, 6); // central dir start disk
|
|
102
|
+
eocd.writeUInt16LE(entries.length, 8); // entries on this disk
|
|
103
|
+
eocd.writeUInt16LE(entries.length, 10); // total entries
|
|
104
|
+
eocd.writeUInt32LE(centralBytes.length, 12); // central dir size
|
|
105
|
+
eocd.writeUInt32LE(localBytes.length, 16); // central dir offset
|
|
106
|
+
eocd.writeUInt16LE(0, 20); // comment length
|
|
107
|
+
|
|
108
|
+
return Buffer.concat([localBytes, centralBytes, eocd]);
|
|
109
|
+
}
|
package/src/rastack-ci.ts
CHANGED
|
@@ -116,7 +116,7 @@ function main(argv: string[]): void {
|
|
|
116
116
|
const cfg = loadConfig();
|
|
117
117
|
if (!cfg) {
|
|
118
118
|
console.error(
|
|
119
|
-
" ✗ no .rastack/deploy.json found. Run `rastack init`
|
|
119
|
+
" ✗ no .rastack/deploy.json found. Run `rastack init` first.",
|
|
120
120
|
);
|
|
121
121
|
process.exit(1);
|
|
122
122
|
return;
|