@granite-js/forge-cli 0.0.0-dev-20250725013859

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/README.md ADDED
@@ -0,0 +1,3 @@
1
+ # @granite-js/forge
2
+
3
+ A CLI tool for managing Granite applications
package/bin/index.js ADDED
@@ -0,0 +1,3 @@
1
+ #!/usr/bin/env node
2
+
3
+ import '../dist/index.js';
package/dist/index.js ADDED
@@ -0,0 +1,255 @@
1
+ import { Command } from "@commander-js/extra-typings";
2
+ import path from "path";
3
+ import { fromNodeProviderChain } from "@aws-sdk/credential-providers";
4
+ import { loadSharedConfigFiles } from "@aws-sdk/shared-ini-file-loader";
5
+ import * as p$3 from "@clack/prompts";
6
+ import * as p$2 from "@clack/prompts";
7
+ import * as p$1 from "@clack/prompts";
8
+ import * as p from "@clack/prompts";
9
+ import { loadConfig } from "@granite-js/cli";
10
+ import { DeployManager, NoSuchKey, S3Client } from "@granite-js/deployment-manager";
11
+ import * as v from "valibot";
12
+ import { v7 } from "uuid";
13
+ import { createReadStream, createWriteStream } from "fs";
14
+ import { pipeline } from "stream/promises";
15
+ import { createGzip } from "zlib";
16
+
17
+ //#region src/utils/generateDeploymentId.ts
18
+ function generateDeploymentId() {
19
+ return v7();
20
+ }
21
+
22
+ //#endregion
23
+ //#region src/utils/gzip.ts
24
+ /**
25
+ * Compresses inputFile using gzip and saves it to outputFile.
26
+ *
27
+ * @param {string} inputFile - Path to the source file to compress
28
+ * @param {string} outputFile - Path to save the gzipped file (recommended to use .gz extension)
29
+ * @throws Throws an error if compression fails
30
+ */
31
+ async function gzipFile({ inputFile, outputFile }) {
32
+ await pipeline(createReadStream(inputFile), createGzip(), createWriteStream(outputFile));
33
+ }
34
+
35
+ //#endregion
36
+ //#region src/utils/handlePrompts.ts
37
+ function handlePrompts(title, task) {
38
+ return (...args) => {
39
+ p$3.intro(title);
40
+ try {
41
+ const result = task.call(null, ...args);
42
+ if (result instanceof Promise) result.catch(promptErrorHandler);
43
+ } catch (error) {
44
+ promptErrorHandler(error);
45
+ }
46
+ };
47
+ }
48
+ function promptErrorHandler(error) {
49
+ p$3.log.error(error instanceof Error ? error.message : String(error));
50
+ process.exit(1);
51
+ }
52
+
53
+ //#endregion
54
+ //#region src/operations/deploy.ts
55
+ const deploy$1 = handlePrompts("Start deployment", deployImpl);
56
+ async function deployImpl({ androidBundle, iosBundle, appName, tag }, context) {
57
+ const { s3Client } = context;
58
+ const gzippedAndroidBundle = `${androidBundle}.gz`;
59
+ const gzippedIosBundle = `${iosBundle}.gz`;
60
+ await Promise.all([gzipFile({
61
+ inputFile: androidBundle,
62
+ outputFile: gzippedAndroidBundle
63
+ }), gzipFile({
64
+ inputFile: iosBundle,
65
+ outputFile: gzippedIosBundle
66
+ })]);
67
+ const deploymentId = generateDeploymentId();
68
+ const deployedAt = new Date();
69
+ let spinner = p$2.spinner();
70
+ spinner.start("Fetching current deployment state...");
71
+ const currentDeploymentState = await DeployManager.readDeploymentState(appName, { s3Client }).catch(handleReadDeploymentStateError);
72
+ spinner.stop("Successfully fetched current deployment state");
73
+ let newDeploymentState;
74
+ if (currentDeploymentState != null) {
75
+ p$2.log.info("Planning rollout...");
76
+ newDeploymentState = await DeployManager.planRollout(currentDeploymentState, {
77
+ progress: 100,
78
+ targetDeploymentId: deploymentId
79
+ });
80
+ switch (newDeploymentState.type) {
81
+ case "STABLE":
82
+ p$2.log.info(`Stable deployment planned (target: ${newDeploymentState.deploymentId})`);
83
+ break;
84
+ case "CANARY":
85
+ case "PENDING":
86
+ console.error("unimplemented yet");
87
+ process.exit(1);
88
+ }
89
+ } else {
90
+ p$2.log.warn("No deployment state found");
91
+ newDeploymentState = {
92
+ type: "STABLE",
93
+ deploymentId
94
+ };
95
+ }
96
+ if (process.stdin.isTTY) {
97
+ const confirmed = await p$2.confirm({ message: `Are you sure you want to deploy ${appName}?` });
98
+ if (!confirmed || p$2.isCancel(confirmed)) {
99
+ p$2.outro("Deployment cancelled");
100
+ process.exit(0);
101
+ }
102
+ }
103
+ await p$2.tasks([{
104
+ bundlePath: gzippedAndroidBundle,
105
+ platform: "android"
106
+ }, {
107
+ bundlePath: gzippedIosBundle,
108
+ platform: "ios"
109
+ }].map(({ bundlePath, platform }) => ({
110
+ title: `Uploading bundle... (${platform})`,
111
+ task: async () => {
112
+ DeployManager.uploadBundle({
113
+ bundlePath,
114
+ appName,
115
+ platform,
116
+ tag,
117
+ deploymentId,
118
+ deployedAt
119
+ }, { s3Client });
120
+ return "Bundle uploaded";
121
+ }
122
+ })));
123
+ spinner = p$2.spinner();
124
+ spinner.start("Updating bundle list...");
125
+ await DeployManager.updateBundleList({
126
+ appName,
127
+ newDeploymentInfo: {
128
+ deployedAt: deployedAt.getTime(),
129
+ deploymentId
130
+ }
131
+ }, { s3Client });
132
+ spinner.stop("Bundle list updated");
133
+ spinner = p$2.spinner();
134
+ spinner.start(`Deploying ${appName}@${deploymentId}...`);
135
+ await DeployManager.rollout({
136
+ state: newDeploymentState,
137
+ appName
138
+ }, { s3Client });
139
+ spinner.stop(`Deployed successfully! (Deployment ID: ${deploymentId})`);
140
+ p$2.outro("Done");
141
+ }
142
+ function handleReadDeploymentStateError(error) {
143
+ if (error instanceof NoSuchKey) return null;
144
+ throw error;
145
+ }
146
+
147
+ //#endregion
148
+ //#region src/commands/deploy.ts
149
+ const envSchema = v.object({
150
+ AWS_REGION: v.string(),
151
+ AWS_ACCESS_KEY_ID: v.string(),
152
+ AWS_SECRET_ACCESS_KEY: v.string(),
153
+ AWS_SESSION_TOKEN: v.optional(v.string())
154
+ });
155
+ const awsCredentialsProvider = fromNodeProviderChain();
156
+ function deploy() {
157
+ return new Command("deploy").description("Deploy a Granite application").requiredOption("--bucket <BUCKET>", "AWS bucket").action(async (options) => {
158
+ const [config, awsCredentials, region] = await Promise.all([
159
+ loadConfig(),
160
+ awsCredentialsProvider(),
161
+ getRegion()
162
+ ]);
163
+ const envResult = v.safeParse(envSchema, {
164
+ AWS_REGION: region,
165
+ AWS_ACCESS_KEY_ID: awsCredentials.accessKeyId,
166
+ AWS_SECRET_ACCESS_KEY: awsCredentials.secretAccessKey,
167
+ AWS_SESSION_TOKEN: awsCredentials.sessionToken
168
+ });
169
+ if (!envResult.success) {
170
+ const missingVars = envResult.issues.map((issue) => {
171
+ const path$1 = issue.path?.[0];
172
+ return path$1 ? `process.env.${path$1.key}` : "unknown variable";
173
+ });
174
+ p$1.log.error(`Required environment variables are not set:`);
175
+ for (const missingVar of missingVars) p$1.log.error(`${missingVar}`);
176
+ process.exit(1);
177
+ }
178
+ await deploy$1({
179
+ appName: config.appName,
180
+ iosBundle: path.join(config.outdir, `bundle.ios.hbc`),
181
+ androidBundle: path.join(config.outdir, `bundle.android.hbc`)
182
+ }, { s3Client: new S3Client({
183
+ region,
184
+ bucket: options.bucket,
185
+ credentials: {
186
+ accessKeyId: envResult.output.AWS_ACCESS_KEY_ID,
187
+ secretAccessKey: envResult.output.AWS_SECRET_ACCESS_KEY,
188
+ sessionToken: envResult.output.AWS_SESSION_TOKEN
189
+ }
190
+ }) });
191
+ });
192
+ }
193
+ async function getRegion() {
194
+ const { configFile } = await loadSharedConfigFiles();
195
+ const envRegion = process.env.AWS_REGION;
196
+ if (envRegion != null) return envRegion;
197
+ const currentProfile = process.env.AWS_PROFILE;
198
+ if (currentProfile != null) {
199
+ const region = configFile?.[currentProfile]?.region;
200
+ if (region != null) return region;
201
+ }
202
+ const defaultRegion = configFile.default?.region;
203
+ return defaultRegion;
204
+ }
205
+
206
+ //#endregion
207
+ //#region src/helpers/command.ts
208
+ function withS3Client(command) {
209
+ return command.requiredOption("--region <REGION>", "AWS region").requiredOption("--bucket <BUCKET>", "AWS bucket").requiredOption("--access-key-id <ACCESS_KEY_ID>", "AWS access key ID").requiredOption("--secret-access-key <SECRET_ACCESS_KEY>", "AWS secret access key").option("--session-token <SESSION_TOKEN>", "AWS session token");
210
+ }
211
+
212
+ //#endregion
213
+ //#region src/operations/deployList.ts
214
+ const deployList$1 = handlePrompts("Deployment list", deployListImpl);
215
+ async function deployListImpl({ appName }, context) {
216
+ const { s3Client } = context;
217
+ const spinner = p.spinner();
218
+ spinner.start("Fetching deployment list...");
219
+ const deployments = await DeployManager.readBundleList(appName, { s3Client }).catch(handleReadBundleListError);
220
+ spinner.stop("Successfully fetched deployment list");
221
+ deployments.slice(0, 20).forEach((deployment, index) => {
222
+ p.log.info(`${index + 1}. ${deployment.deploymentId} (Deployed at: ${new Date(deployment.deployedAt).toLocaleString()})`);
223
+ });
224
+ p.outro(deployments.length ? `${deployments.length} deployment(s) found` : "No deployments found");
225
+ }
226
+ function handleReadBundleListError(error) {
227
+ if (error instanceof NoSuchKey) return [];
228
+ throw error;
229
+ }
230
+
231
+ //#endregion
232
+ //#region src/commands/deployList.ts
233
+ function deployList() {
234
+ return withS3Client(new Command("deploy-list")).description("Show the deployment list of a Granite application").requiredOption("-n, --app-name <APP_NAME>", "Granite application name").action(async (options) => {
235
+ await deployList$1(options, { s3Client: new S3Client({
236
+ region: options.region,
237
+ bucket: options.bucket,
238
+ credentials: {
239
+ accessKeyId: options.accessKeyId,
240
+ secretAccessKey: options.secretAccessKey,
241
+ sessionToken: options.sessionToken
242
+ }
243
+ }) });
244
+ });
245
+ }
246
+
247
+ //#endregion
248
+ //#region src/index.ts
249
+ const program = new Command("granite-forge");
250
+ program.description("A CLI tool for managing Granite applications");
251
+ program.addCommand(deploy());
252
+ program.addCommand(deployList());
253
+ program.parse();
254
+
255
+ //#endregion
package/package.json ADDED
@@ -0,0 +1,38 @@
1
+ {
2
+ "name": "@granite-js/forge-cli",
3
+ "description": "A CLI tool for managing Granite applications",
4
+ "type": "module",
5
+ "version": "0.0.0-dev-20250725013859",
6
+ "bin": {
7
+ "granite-forge": "./bin/index.js"
8
+ },
9
+ "files": [
10
+ "dist",
11
+ "bin",
12
+ "package.json"
13
+ ],
14
+ "scripts": {
15
+ "prepack": "yarn build",
16
+ "typecheck": "tsc --noEmit",
17
+ "test": "vitest --run --coverage --passWithNoTests",
18
+ "build": "tsdown"
19
+ },
20
+ "devDependencies": {
21
+ "@types/node": "^22.10.2",
22
+ "@vitest/coverage-v8": "^3.1.4",
23
+ "tsdown": "^0.11.13",
24
+ "typescript": "^5.8.3",
25
+ "vitest": "^3.1.4"
26
+ },
27
+ "dependencies": {
28
+ "@aws-sdk/credential-providers": "^3.817.0",
29
+ "@aws-sdk/shared-ini-file-loader": "^3.374.0",
30
+ "@clack/prompts": "^0.10.1",
31
+ "@commander-js/extra-typings": "^14.0.0",
32
+ "@granite-js/cli": "workspace:*",
33
+ "@granite-js/deployment-manager": "workspace:*",
34
+ "commander": "^14.0.0",
35
+ "uuid": "^11.1.0",
36
+ "valibot": "^1.1.0"
37
+ }
38
+ }