@granite-js/forge-cli 0.0.0

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