rastack 0.0.33 → 0.0.35

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.
@@ -0,0 +1,121 @@
1
+ "use strict";
2
+ /**
3
+ * Filesystem + prompt helpers shared by the `rastack deploy | ci | init`
4
+ * entrypoints. Everything pure lives in the sibling modules; this is the small
5
+ * IO boundary they wrap.
6
+ */
7
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
8
+ if (k2 === undefined) k2 = k;
9
+ var desc = Object.getOwnPropertyDescriptor(m, k);
10
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
11
+ desc = { enumerable: true, get: function() { return m[k]; } };
12
+ }
13
+ Object.defineProperty(o, k2, desc);
14
+ }) : (function(o, m, k, k2) {
15
+ if (k2 === undefined) k2 = k;
16
+ o[k2] = m[k];
17
+ }));
18
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
19
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
20
+ }) : function(o, v) {
21
+ o["default"] = v;
22
+ });
23
+ var __importStar = (this && this.__importStar) || (function () {
24
+ var ownKeys = function(o) {
25
+ ownKeys = Object.getOwnPropertyNames || function (o) {
26
+ var ar = [];
27
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
28
+ return ar;
29
+ };
30
+ return ownKeys(o);
31
+ };
32
+ return function (mod) {
33
+ if (mod && mod.__esModule) return mod;
34
+ var result = {};
35
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
36
+ __setModuleDefault(result, mod);
37
+ return result;
38
+ };
39
+ })();
40
+ Object.defineProperty(exports, "__esModule", { value: true });
41
+ exports.configPath = configPath;
42
+ exports.loadConfig = loadConfig;
43
+ exports.saveConfig = saveConfig;
44
+ exports.gitRemoteUrl = gitRemoteUrl;
45
+ exports.canPrompt = canPrompt;
46
+ exports.ask = ask;
47
+ exports.withPrompt = withPrompt;
48
+ const child_process_1 = require("child_process");
49
+ const fs = __importStar(require("fs"));
50
+ const path = __importStar(require("path"));
51
+ const readline = __importStar(require("readline"));
52
+ const workflows_1 = require("./workflows");
53
+ const config_1 = require("./config");
54
+ /** Absolute path to the deploy config for a project root. */
55
+ function configPath(cwd = process.cwd()) {
56
+ return path.join(cwd, workflows_1.CONFIG_PATH);
57
+ }
58
+ /** Read + normalize the deploy config, or null when it does not exist yet. */
59
+ function loadConfig(cwd = process.cwd()) {
60
+ const file = configPath(cwd);
61
+ if (!fs.existsSync(file))
62
+ return null;
63
+ const raw = JSON.parse(fs.readFileSync(file, "utf-8"));
64
+ return (0, config_1.normalizeConfig)(raw);
65
+ }
66
+ /** Write the deploy config, creating `.rastack/` if needed. */
67
+ function saveConfig(cfg, cwd = process.cwd()) {
68
+ const file = configPath(cwd);
69
+ fs.mkdirSync(path.dirname(file), { recursive: true });
70
+ fs.writeFileSync(file, JSON.stringify(cfg, null, 2) + "\n");
71
+ return file;
72
+ }
73
+ /**
74
+ * Best-effort GitHub `origin` remote URL, for defaulting the repo coordinates.
75
+ * Returns null when not a git repo or no origin.
76
+ */
77
+ function gitRemoteUrl(cwd = process.cwd()) {
78
+ try {
79
+ // Reads the developer's own git config with a fixed command; no untrusted
80
+ // input, so there is no command-injection surface.
81
+ // nosemgrep: javascript.lang.security.detect-child-process.detect-child-process
82
+ const out = (0, child_process_1.execSync)("git remote get-url origin", {
83
+ cwd,
84
+ stdio: ["ignore", "pipe", "ignore"],
85
+ });
86
+ const url = out.toString().trim();
87
+ return url || null;
88
+ }
89
+ catch {
90
+ return null;
91
+ }
92
+ }
93
+ /** True when we can prompt the user (a TTY, and not explicitly opted out). */
94
+ function canPrompt(nonInteractive) {
95
+ return !nonInteractive && Boolean(process.stdin.isTTY);
96
+ }
97
+ /**
98
+ * Ask a single question with a default, echoed as `label [default]: `. Returns
99
+ * the trimmed answer, or the default on an empty line.
100
+ */
101
+ function ask(rl, label, def) {
102
+ const suffix = def ? ` [${def}]` : "";
103
+ return new Promise((resolve) => {
104
+ rl.question(` ${label}${suffix}: `, (answer) => {
105
+ resolve(answer.trim() || def);
106
+ });
107
+ });
108
+ }
109
+ /** Run a callback with a readline interface, always closing it. */
110
+ async function withPrompt(fn) {
111
+ const rl = readline.createInterface({
112
+ input: process.stdin,
113
+ output: process.stdout,
114
+ });
115
+ try {
116
+ return await fn(rl);
117
+ }
118
+ finally {
119
+ rl.close();
120
+ }
121
+ }
@@ -0,0 +1,46 @@
1
+ /**
2
+ * The files `rastack init` templates into a repo — the base GitHub Actions and
3
+ * the deploy config — so starting (or reconfiguring) a project is one command.
4
+ *
5
+ * The generated workflows lean on `rastack ci`: rather than inlining the build
6
+ * and deploy shell, each job runs one `rastack ci …` line, and the CLI owns the
7
+ * steps (tools/src/deploy/ci.ts). That keeps the YAML short and keeps the
8
+ * pipeline logic versioned with the CLI, testable, and inspectable with
9
+ * `rastack ci --plan`.
10
+ *
11
+ * Pure string builders — `rastack-init.ts` is the thin wrapper that writes them.
12
+ */
13
+ import { DeployConfig } from "./config";
14
+ /** A templated file: repo-relative path + its contents. */
15
+ export interface ScaffoldFile {
16
+ path: string;
17
+ content: string;
18
+ }
19
+ /**
20
+ * `.github/workflows/ci.yml` — validate resources and compile the manifest on
21
+ * every push/PR. The clean replacement for hand-written compile/check steps.
22
+ */
23
+ export declare function renderCiWorkflow(cfg: DeployConfig): string;
24
+ /**
25
+ * `.github/workflows/deploy-backend.yml` — ship the Rust Lambda on every push to
26
+ * the deploy branch. Authenticates with GitHub OIDC (no stored keys) and then
27
+ * runs one line, `rastack ci deploy`, which cross-builds the bundle, bakes the
28
+ * JWKS, and calls `update-function-code`. That single privileged call is the
29
+ * whole of CI's blast radius (docs/serverless-deploy.md §2).
30
+ */
31
+ export declare function renderDeployWorkflow(cfg: DeployConfig): string;
32
+ /** The deploy config written to `.rastack/deploy.json`, pretty-printed. */
33
+ export declare function renderConfigFile(cfg: DeployConfig): string;
34
+ export declare const CONFIG_PATH = ".rastack/deploy.json";
35
+ export declare const CI_WORKFLOW_PATH = ".github/workflows/ci.yml";
36
+ export declare const DEPLOY_WORKFLOW_PATH = ".github/workflows/deploy-backend.yml";
37
+ export interface ScaffoldOptions {
38
+ /** Omit the GitHub Actions workflows (config only). */
39
+ noWorkflows?: boolean;
40
+ }
41
+ /**
42
+ * The full set of files `rastack init` templates: the deploy config plus the two
43
+ * base workflows. Returned as data so the caller decides what to write, skip, or
44
+ * overwrite.
45
+ */
46
+ export declare function scaffoldFiles(cfg: DeployConfig, opts?: ScaffoldOptions): ScaffoldFile[];
@@ -0,0 +1,145 @@
1
+ "use strict";
2
+ /**
3
+ * The files `rastack init` templates into a repo — the base GitHub Actions and
4
+ * the deploy config — so starting (or reconfiguring) a project is one command.
5
+ *
6
+ * The generated workflows lean on `rastack ci`: rather than inlining the build
7
+ * and deploy shell, each job runs one `rastack ci …` line, and the CLI owns the
8
+ * steps (tools/src/deploy/ci.ts). That keeps the YAML short and keeps the
9
+ * pipeline logic versioned with the CLI, testable, and inspectable with
10
+ * `rastack ci --plan`.
11
+ *
12
+ * Pure string builders — `rastack-init.ts` is the thin wrapper that writes them.
13
+ */
14
+ Object.defineProperty(exports, "__esModule", { value: true });
15
+ exports.DEPLOY_WORKFLOW_PATH = exports.CI_WORKFLOW_PATH = exports.CONFIG_PATH = void 0;
16
+ exports.renderCiWorkflow = renderCiWorkflow;
17
+ exports.renderDeployWorkflow = renderDeployWorkflow;
18
+ exports.renderConfigFile = renderConfigFile;
19
+ exports.scaffoldFiles = scaffoldFiles;
20
+ /** Node version the templated workflows pin (LTS — safe for a fresh repo). */
21
+ const NODE_VERSION = "22";
22
+ /**
23
+ * `.github/workflows/ci.yml` — validate resources and compile the manifest on
24
+ * every push/PR. The clean replacement for hand-written compile/check steps.
25
+ */
26
+ function renderCiWorkflow(cfg) {
27
+ return `name: CI
28
+
29
+ # Validate the resource graph and compile the manifest on every change.
30
+ # The heavy lifting lives in \`rastack ci check\` (see tools/src/deploy/ci.ts),
31
+ # so this workflow stays a single command.
32
+
33
+ on:
34
+ push:
35
+ branches: [${cfg.github.branch}]
36
+ pull_request:
37
+
38
+ permissions:
39
+ contents: read
40
+
41
+ jobs:
42
+ validate:
43
+ runs-on: ubuntu-latest
44
+ steps:
45
+ - uses: actions/checkout@v4
46
+ - uses: actions/setup-node@v4
47
+ with:
48
+ node-version: '${NODE_VERSION}'
49
+ - name: Install rastack
50
+ run: npm install -g rastack
51
+ - name: Validate resources & compile manifest
52
+ run: rastack ci check
53
+ `;
54
+ }
55
+ /**
56
+ * `.github/workflows/deploy-backend.yml` — ship the Rust Lambda on every push to
57
+ * the deploy branch. Authenticates with GitHub OIDC (no stored keys) and then
58
+ * runs one line, `rastack ci deploy`, which cross-builds the bundle, bakes the
59
+ * JWKS, and calls `update-function-code`. That single privileged call is the
60
+ * whole of CI's blast radius (docs/serverless-deploy.md §2).
61
+ */
62
+ function renderDeployWorkflow(cfg) {
63
+ const arch = cfg.lambda.arch === "arm64" ? "arm64" : "x86_64";
64
+ return `name: Deploy backend
65
+
66
+ # Ship the Rust API (rastack-server) to AWS Lambda. Infrastructure is created
67
+ # once by a human via CloudFormation (docs/serverless-deploy.md); this workflow
68
+ # only ever runs \`aws lambda update-function-code\`, wrapped by \`rastack ci\`.
69
+ #
70
+ # Wire these from the CloudFormation stack Outputs as GitHub repo Variables:
71
+ # AWS_DEPLOY_ROLE_ARN DeployRoleArn (the OIDC role this job assumes)
72
+ # AWS_REGION Region (${cfg.region})
73
+
74
+ on:
75
+ push:
76
+ branches: [${cfg.github.branch}]
77
+
78
+ # Least privilege: read the repo + mint an OIDC token to assume the deploy role.
79
+ permissions:
80
+ contents: read
81
+ id-token: write
82
+
83
+ concurrency:
84
+ group: deploy-backend
85
+ cancel-in-progress: true
86
+
87
+ jobs:
88
+ deploy:
89
+ name: Build & deploy the Lambda (${arch})
90
+ runs-on: ubuntu-latest
91
+ # Skip on forks / repos that never wired the deploy role, so the job does
92
+ # not fail red for contributors who cannot (and should not) deploy.
93
+ if: vars.AWS_DEPLOY_ROLE_ARN != ''
94
+ steps:
95
+ - uses: actions/checkout@v4
96
+
97
+ - uses: actions/setup-node@v4
98
+ with:
99
+ node-version: '${NODE_VERSION}'
100
+ - name: Install rastack
101
+ run: npm install -g rastack
102
+
103
+ - uses: dtolnay/rust-toolchain@stable
104
+ - name: Cache cargo
105
+ uses: Swatinem/rust-cache@v2
106
+ with:
107
+ workspaces: rust
108
+ # cargo-lambda cross-builds the custom-runtime binary; it auto-installs
109
+ # zig (via pip) for the cross-compile when needed.
110
+ - name: Install cargo-lambda
111
+ run: pip3 install cargo-lambda
112
+
113
+ - name: Configure AWS credentials (OIDC — no stored keys)
114
+ uses: aws-actions/configure-aws-credentials@v4
115
+ with:
116
+ role-to-assume: \${{ vars.AWS_DEPLOY_ROLE_ARN }}
117
+ aws-region: \${{ vars.AWS_REGION }}
118
+
119
+ # One line: compile the manifest, cross-build the bundle, bake the JWKS,
120
+ # and update-function-code. See \`rastack ci --plan deploy\`.
121
+ - name: Build & deploy
122
+ run: rastack ci deploy
123
+ `;
124
+ }
125
+ /** The deploy config written to `.rastack/deploy.json`, pretty-printed. */
126
+ function renderConfigFile(cfg) {
127
+ return JSON.stringify(cfg, null, 2) + "\n";
128
+ }
129
+ exports.CONFIG_PATH = ".rastack/deploy.json";
130
+ exports.CI_WORKFLOW_PATH = ".github/workflows/ci.yml";
131
+ exports.DEPLOY_WORKFLOW_PATH = ".github/workflows/deploy-backend.yml";
132
+ /**
133
+ * The full set of files `rastack init` templates: the deploy config plus the two
134
+ * base workflows. Returned as data so the caller decides what to write, skip, or
135
+ * overwrite.
136
+ */
137
+ function scaffoldFiles(cfg, opts = {}) {
138
+ const files = [
139
+ { path: exports.CONFIG_PATH, content: renderConfigFile(cfg) },
140
+ ];
141
+ if (!opts.noWorkflows) {
142
+ files.push({ path: exports.CI_WORKFLOW_PATH, content: renderCiWorkflow(cfg) }, { path: exports.DEPLOY_WORKFLOW_PATH, content: renderDeployWorkflow(cfg) });
143
+ }
144
+ return files;
145
+ }
@@ -0,0 +1,18 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * `rastack ci` — run the CI pipeline for a phase, so GitHub Actions stays a
4
+ * one-liner.
5
+ *
6
+ * rastack ci <check|build|deploy> [--plan] [--skip-build]
7
+ * [--function <name>] [--jwks-url <url>] [--bin <cmd>]
8
+ *
9
+ * check validate the resource graph + compile the manifest (a CI gate)
10
+ * build cross-build the Lambda bundle and bake the Cognito JWKS in
11
+ * deploy build (unless --skip-build) then `update-function-code`
12
+ *
13
+ * The steps are a pure function of `.rastack/deploy.json` (see
14
+ * tools/src/deploy/ci.ts); `--plan` prints them without running anything.
15
+ * Function name / JWKS URL come from the config's stack outputs when wired, else
16
+ * are resolved live from CloudFormation at run time.
17
+ */
18
+ export {};
@@ -0,0 +1,118 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+ /**
4
+ * `rastack ci` — run the CI pipeline for a phase, so GitHub Actions stays a
5
+ * one-liner.
6
+ *
7
+ * rastack ci <check|build|deploy> [--plan] [--skip-build]
8
+ * [--function <name>] [--jwks-url <url>] [--bin <cmd>]
9
+ *
10
+ * check validate the resource graph + compile the manifest (a CI gate)
11
+ * build cross-build the Lambda bundle and bake the Cognito JWKS in
12
+ * deploy build (unless --skip-build) then `update-function-code`
13
+ *
14
+ * The steps are a pure function of `.rastack/deploy.json` (see
15
+ * tools/src/deploy/ci.ts); `--plan` prints them without running anything.
16
+ * Function name / JWKS URL come from the config's stack outputs when wired, else
17
+ * are resolved live from CloudFormation at run time.
18
+ */
19
+ Object.defineProperty(exports, "__esModule", { value: true });
20
+ const child_process_1 = require("child_process");
21
+ const ci_1 = require("./deploy/ci");
22
+ const io_1 = require("./deploy/io");
23
+ const PHASES = ["check", "build", "deploy"];
24
+ function parseFlags(argv) {
25
+ let phase = "";
26
+ let plan = false;
27
+ const opts = {
28
+ bin: process.env.RASTACK_BIN || "rastack",
29
+ functionName: process.env.RASTACK_FUNCTION || undefined,
30
+ jwksUrl: process.env.RASTACK_JWKS_URL || undefined,
31
+ };
32
+ for (let i = 0; i < argv.length; i++) {
33
+ const a = argv[i];
34
+ const next = () => argv[++i] ?? "";
35
+ switch (a) {
36
+ case "--plan":
37
+ case "--dry-run":
38
+ plan = true;
39
+ break;
40
+ case "--skip-build":
41
+ opts.skipBuild = true;
42
+ break;
43
+ case "--function":
44
+ opts.functionName = next();
45
+ break;
46
+ case "--jwks-url":
47
+ opts.jwksUrl = next();
48
+ break;
49
+ case "--bin":
50
+ opts.bin = next();
51
+ break;
52
+ default:
53
+ if (a.startsWith("-")) {
54
+ console.error(`Unknown flag: ${a}`);
55
+ process.exit(1);
56
+ }
57
+ else if (!phase && PHASES.includes(a)) {
58
+ phase = a;
59
+ }
60
+ else if (!phase) {
61
+ console.error(`Unknown ci phase: "${a}" (expected ${PHASES.join(" | ")}).`);
62
+ process.exit(1);
63
+ }
64
+ }
65
+ }
66
+ if (!phase) {
67
+ console.error(`Usage: rastack ci <${PHASES.join(" | ")}> [--plan] [--skip-build]\n`);
68
+ process.exit(1);
69
+ }
70
+ return { phase, plan, opts };
71
+ }
72
+ function printPlan(phase, steps) {
73
+ console.log(`\n rastack ci ${phase} — ${steps.length} step(s):\n`);
74
+ steps.forEach((s, i) => {
75
+ const at = s.cwd ? ` (in ${s.cwd}/)` : "";
76
+ console.log(` ${i + 1}. ${s.name}${at}`);
77
+ console.log(` $ ${s.run}\n`);
78
+ });
79
+ }
80
+ function runSteps(phase, steps) {
81
+ for (const step of steps) {
82
+ console.log(`\n▸ ${step.name}${step.cwd ? ` (in ${step.cwd}/)` : ""}`);
83
+ console.log(` $ ${step.run}`);
84
+ try {
85
+ // Runs a CI pipeline step derived from the developer's own deploy config
86
+ // — no untrusted input. A shell is required for the `$(aws …)` output
87
+ // substitutions the plan uses to resolve stack outputs at run time.
88
+ // nosemgrep: javascript.lang.security.detect-child-process.detect-child-process
89
+ (0, child_process_1.execSync)(step.run, {
90
+ stdio: "inherit",
91
+ cwd: step.cwd,
92
+ env: process.env,
93
+ });
94
+ }
95
+ catch {
96
+ console.error(`\n ✗ step failed: ${step.name}`);
97
+ process.exit(1);
98
+ }
99
+ }
100
+ console.log(`\n ✓ rastack ci ${phase} complete.`);
101
+ }
102
+ function main(argv) {
103
+ const { phase, plan, opts } = parseFlags(argv);
104
+ const cfg = (0, io_1.loadConfig)();
105
+ if (!cfg) {
106
+ console.error(" ✗ no .rastack/deploy.json found. Run `rastack init` (or `rastack deploy`) first.");
107
+ process.exit(1);
108
+ return;
109
+ }
110
+ const steps = (0, ci_1.ciPlan)(phase, cfg, opts);
111
+ if (plan)
112
+ printPlan(phase, steps);
113
+ else
114
+ runSteps(phase, steps);
115
+ }
116
+ if (require.main === module) {
117
+ main(process.argv.slice(2));
118
+ }
@@ -0,0 +1,20 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * `rastack deploy` — configure a repo's serverless deployment.
4
+ *
5
+ * rastack deploy [--app <slug>] [--region <r>] [--owner <o>] [--repo <r>]
6
+ * [--branch <b>] [--namespace <n>] [--templates-bucket <b>]
7
+ * [--artifacts-bucket <b>] [--arch arm64|x86_64]
8
+ * [--print] [--run] [--yes]
9
+ *
10
+ * Writes/updates `.rastack/deploy.json` (creating it if `rastack init` has not
11
+ * run) and prints exactly how to stand the app up: the one-time
12
+ * `aws cloudformation deploy` that creates all infrastructure, the GitHub repo
13
+ * Variables CI reads, and the Lambda's runtime env. Infra is created once by a
14
+ * human — this command *configures* that step, it does not silently mutate the
15
+ * cloud. `--run` executes the launch command for you; `--print` is
16
+ * non-interactive (config as-is).
17
+ *
18
+ * The recurring code deploy is CI's job (`rastack ci deploy`), not this command.
19
+ */
20
+ export {};
@@ -0,0 +1,196 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+ /**
4
+ * `rastack deploy` — configure a repo's serverless deployment.
5
+ *
6
+ * rastack deploy [--app <slug>] [--region <r>] [--owner <o>] [--repo <r>]
7
+ * [--branch <b>] [--namespace <n>] [--templates-bucket <b>]
8
+ * [--artifacts-bucket <b>] [--arch arm64|x86_64]
9
+ * [--print] [--run] [--yes]
10
+ *
11
+ * Writes/updates `.rastack/deploy.json` (creating it if `rastack init` has not
12
+ * run) and prints exactly how to stand the app up: the one-time
13
+ * `aws cloudformation deploy` that creates all infrastructure, the GitHub repo
14
+ * Variables CI reads, and the Lambda's runtime env. Infra is created once by a
15
+ * human — this command *configures* that step, it does not silently mutate the
16
+ * cloud. `--run` executes the launch command for you; `--print` is
17
+ * non-interactive (config as-is).
18
+ *
19
+ * The recurring code deploy is CI's job (`rastack ci deploy`), not this command.
20
+ */
21
+ Object.defineProperty(exports, "__esModule", { value: true });
22
+ const child_process_1 = require("child_process");
23
+ const config_1 = require("./deploy/config");
24
+ const io_1 = require("./deploy/io");
25
+ function parseFlags(argv) {
26
+ const patch = { github: {}, lambda: {} };
27
+ let print = false;
28
+ let run = false;
29
+ let nonInteractive = false;
30
+ for (let i = 0; i < argv.length; i++) {
31
+ const a = argv[i];
32
+ const next = () => argv[++i] ?? "";
33
+ switch (a) {
34
+ case "--app":
35
+ patch.app = next();
36
+ break;
37
+ case "--region":
38
+ patch.region = next();
39
+ break;
40
+ case "--owner":
41
+ patch.github.owner = next();
42
+ break;
43
+ case "--repo":
44
+ patch.github.repo = next();
45
+ break;
46
+ case "--branch":
47
+ patch.github.branch = next();
48
+ break;
49
+ case "--prefix":
50
+ patch.namePrefix = next();
51
+ break;
52
+ case "--namespace":
53
+ patch.namespace = next();
54
+ break;
55
+ case "--stack-name":
56
+ patch.stackName = next();
57
+ break;
58
+ case "--templates-bucket":
59
+ patch.templatesBucket = next();
60
+ break;
61
+ case "--artifacts-bucket":
62
+ patch.artifactsBucket = next();
63
+ break;
64
+ case "--arch":
65
+ patch.lambda.arch = next();
66
+ break;
67
+ case "--print":
68
+ print = true;
69
+ nonInteractive = true;
70
+ break;
71
+ case "--run":
72
+ run = true;
73
+ break;
74
+ case "--yes":
75
+ case "-y":
76
+ case "--non-interactive":
77
+ nonInteractive = true;
78
+ break;
79
+ default:
80
+ if (a.startsWith("-")) {
81
+ console.error(`Unknown flag: ${a}`);
82
+ process.exit(1);
83
+ }
84
+ }
85
+ }
86
+ return { patch, print, run, nonInteractive };
87
+ }
88
+ function seedConfig(patch) {
89
+ const existing = (0, io_1.loadConfig)();
90
+ return existing ? (0, config_1.mergeConfig)(existing, patch) : (0, config_1.normalizeConfig)(patch);
91
+ }
92
+ async function promptMissing(cfg) {
93
+ return (0, io_1.withPrompt)(async (rl) => {
94
+ const app = await (0, io_1.ask)(rl, "App slug", cfg.app);
95
+ const owner = await (0, io_1.ask)(rl, "GitHub owner", cfg.github.owner);
96
+ const repo = await (0, io_1.ask)(rl, "GitHub repo", cfg.github.repo);
97
+ const branch = await (0, io_1.ask)(rl, "Deploy branch", cfg.github.branch);
98
+ const region = await (0, io_1.ask)(rl, "AWS region", cfg.region);
99
+ const namespace = await (0, io_1.ask)(rl, "Iceberg namespace", cfg.namespace);
100
+ const artifactsBucket = await (0, io_1.ask)(rl, "Artifacts bucket (from bootstrap stack)", cfg.artifactsBucket ?? "");
101
+ const templatesBucket = await (0, io_1.ask)(rl, "Templates bucket (from bootstrap stack, optional)", cfg.templatesBucket ?? "");
102
+ return (0, config_1.mergeConfig)(cfg, {
103
+ app,
104
+ region,
105
+ namespace,
106
+ artifactsBucket,
107
+ templatesBucket,
108
+ github: { owner, repo, branch },
109
+ });
110
+ });
111
+ }
112
+ /** Pad a list of `Key=Value` rows into an aligned table. */
113
+ function table(rows) {
114
+ const width = Math.max(0, ...rows.map((r) => r.key.length));
115
+ return rows.map((r) => ` ${r.key.padEnd(width)} ${r.value}`).join("\n");
116
+ }
117
+ function printSummary(cfg) {
118
+ const src = (0, config_1.templateUrl)(cfg);
119
+ console.log(`\n Deployment config (${cfg.stackName} @ ${cfg.region})\n`);
120
+ console.log(" ── 1. Create the infrastructure (once, by a human) ──");
121
+ if (!cfg.artifactsBucket) {
122
+ console.log(" ⚠ artifactsBucket is unset — app.yaml requires it. Run the bootstrap\n" +
123
+ " stack first, then set --artifacts-bucket (its ArtifactsBucketName output).");
124
+ }
125
+ if (!src) {
126
+ console.log(" (launching from the in-repo template; set --templates-bucket to launch\n" +
127
+ " the hosted app.yaml by URL instead)");
128
+ }
129
+ console.log("\n" + (0, config_1.deployStackCommand)(cfg) + "\n");
130
+ console.log(" ── 2. Wire the GitHub repo Variables (from the stack Outputs) ──");
131
+ console.log(table((0, config_1.repoVariables)(cfg).map((v) => ({
132
+ key: v.name,
133
+ value: v.value || `<${v.from}>`,
134
+ }))));
135
+ console.log("\n ── 3. The Lambda runtime env (set by the stack) ──");
136
+ console.log(table((0, config_1.lambdaEnv)(cfg)));
137
+ console.log(`\n ── 4. Then every push to ${cfg.github.branch} runs \`rastack ci deploy\`. ──\n`);
138
+ }
139
+ function runLaunch(cfg) {
140
+ const params = (0, config_1.stackParameters)(cfg).map((p) => `${p.key}=${p.value}`);
141
+ const src = (0, config_1.templateUrl)(cfg);
142
+ const args = [
143
+ "cloudformation",
144
+ "deploy",
145
+ ...(src
146
+ ? ["--template-url", src]
147
+ : ["--template-file", "deploy/cloudformation/app.yaml"]),
148
+ "--stack-name",
149
+ cfg.stackName,
150
+ "--capabilities",
151
+ "CAPABILITY_NAMED_IAM",
152
+ "--region",
153
+ cfg.region,
154
+ "--parameter-overrides",
155
+ ...params,
156
+ ];
157
+ console.log(`\n ▸ aws ${args.join(" ")}\n`);
158
+ try {
159
+ // Runs the developer's own AWS CLI with args built from their config — no
160
+ // untrusted input, and `execFileSync` (no shell) avoids any injection.
161
+ (0, child_process_1.execFileSync)("aws", args, { stdio: "inherit" });
162
+ }
163
+ catch (err) {
164
+ console.error(`\n ✗ aws cloudformation deploy failed. Is the AWS CLI installed and are` +
165
+ ` your credentials configured?\n ${err.message}`);
166
+ process.exit(1);
167
+ }
168
+ }
169
+ async function main(argv) {
170
+ const flags = parseFlags(argv);
171
+ let cfg = seedConfig(flags.patch);
172
+ if ((0, io_1.canPrompt)(flags.nonInteractive)) {
173
+ console.log("\n rastack deploy — configure the serverless deployment\n");
174
+ cfg = await promptMissing(cfg);
175
+ }
176
+ const errors = (0, config_1.validateConfig)(cfg);
177
+ if (errors.length) {
178
+ for (const e of errors)
179
+ console.error(` ✗ ${e}`);
180
+ console.error(`\n ${errors.length} problem(s) — fix the config and re-run.`);
181
+ process.exit(1);
182
+ }
183
+ if (!flags.print) {
184
+ const file = (0, io_1.saveConfig)(cfg);
185
+ console.log(` ✓ saved ${file}`);
186
+ }
187
+ printSummary(cfg);
188
+ if (flags.run)
189
+ runLaunch(cfg);
190
+ }
191
+ if (require.main === module) {
192
+ main(process.argv.slice(2)).catch((err) => {
193
+ console.error(` ✗ ${err.message}`);
194
+ process.exit(1);
195
+ });
196
+ }
@@ -0,0 +1,14 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * `rastack init` — template the base GitHub Actions + deploy config into a repo.
4
+ *
5
+ * rastack init [--app <slug>] [--owner <o>] [--repo <r>] [--branch <b>]
6
+ * [--region <r>] [--force] [--no-workflows] [--yes]
7
+ *
8
+ * Scaffolds a new project (or reconfigures an existing one) so it is ready to
9
+ * deploy: `.rastack/deploy.json` plus `.github/workflows/{ci,deploy-backend}.yml`,
10
+ * both of which run `rastack ci`. GitHub coordinates default from the repo's
11
+ * `origin` remote; missing fields are prompted for on a TTY. Existing files are
12
+ * left untouched unless `--force`.
13
+ */
14
+ export {};