@pitcher/cli-scripts 2.0.8 → 2.1.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/README.md CHANGED
@@ -48,3 +48,41 @@ p publish-app --org pitcher --increment-version --install-to-env-name Production
48
48
  - `--publish-url <url>`: Explicit publish URL
49
49
  - `--install-to <instanceIds>`: Comma-separated list of instance IDs to install the app to after publishing
50
50
  - `--install-to-env-name <envName>`: Environment name to filter instances and install the app to after publishing (case-insensitive)
51
+
52
+ ## To run the `dev-sync` command:
53
+
54
+ Sync a marketplace app directly to S3 for live development testing without publishing. This is useful for testing app changes in a pre-production environment.
55
+
56
+ ```bash
57
+ # One-time sync from dist folder
58
+ p dev-sync sync --file-id <pitcher_file_id> --api-key myapikey
59
+
60
+ # Sync to a specific organization (default: dev)
61
+ p dev-sync sync --file-id <pitcher_file_id> --org pitcher
62
+
63
+ # Watch mode - auto-sync on file changes (reverts on Ctrl+C)
64
+ p dev-sync watch --file-id <pitcher_file_id> --api-key myapikey
65
+
66
+ # Watch with custom debounce (default: 2500ms)
67
+ p dev-sync watch --file-id <pitcher_file_id> --debounce 5000
68
+
69
+ # Manually revert dev override to restore original version
70
+ p dev-sync revert --file-id <pitcher_file_id> --api-key myapikey
71
+
72
+ # Alternative: Set API key via environment variable
73
+ export NG_API_KEY=myapikey
74
+ p dev-sync watch --file-id <pitcher_file_id> --org pitcher
75
+ ```
76
+
77
+ ### Options
78
+
79
+ - `--file-id <id>`: (Required) File ID of the installed app (`pitcher_file.id`) - copy this from the installed apps view
80
+ - `--api-key <apiKey>`: API Key for authentication (can also use `NG_API_KEY` env variable)
81
+ - `--org <organization>`: Organization subdomain (default: "dev")
82
+ - `--debounce <ms>`: Debounce time in milliseconds for watch mode (default: 2500)
83
+
84
+ ### Notes
85
+
86
+ - Run from your app's project root (with `dist/` folder) or directly from the `dist/` folder
87
+ - The `watch` command automatically reverts changes on graceful shutdown (Ctrl+C)
88
+ - Apps with dev override show a badge in the installed apps view
package/dist/index.js CHANGED
@@ -1,6 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import { Command } from "commander";
3
3
  import { createPublishAppCommand } from "./src/publishApp.js";
4
+ import { createDevSyncCommand } from "./src/devSync.js";
4
5
  import { readFileSync } from "fs";
5
6
  import { fileURLToPath } from "url";
6
7
  import { dirname, join } from "path";
@@ -14,4 +15,5 @@ program
14
15
  .version(pkg.version);
15
16
  // Add subcommands
16
17
  program.addCommand(createPublishAppCommand());
18
+ program.addCommand(createDevSyncCommand());
17
19
  program.parse(process.argv);
@@ -0,0 +1,2 @@
1
+ import { Command } from "commander";
2
+ export declare function createDevSyncCommand(): Command;
@@ -0,0 +1,234 @@
1
+ import { Command } from "commander";
2
+ import archiver from "archiver";
3
+ import axios from "axios";
4
+ import chokidar from "chokidar";
5
+ import fs from "fs";
6
+ import path from "path";
7
+ import FormData from "form-data";
8
+ function findDistPath() {
9
+ const cwd = process.cwd();
10
+ // Check if app.json exists in current directory
11
+ if (fs.existsSync(path.join(cwd, "app.json"))) {
12
+ return cwd;
13
+ }
14
+ // Check if app.json exists in dist subdirectory
15
+ const distPath = path.join(cwd, "dist");
16
+ if (fs.existsSync(path.join(distPath, "app.json"))) {
17
+ return distPath;
18
+ }
19
+ console.error("❌ Could not find app.json in current directory or ./dist");
20
+ console.error(" Run this command from your app's dist folder or from the project root.");
21
+ process.exit(1);
22
+ }
23
+ function readAppJson(distPath) {
24
+ const appJsonPath = path.join(distPath, "app.json");
25
+ if (!fs.existsSync(appJsonPath)) {
26
+ throw new Error(`app.json not found in ${distPath}`);
27
+ }
28
+ const content = fs.readFileSync(appJsonPath, "utf-8");
29
+ const appJson = JSON.parse(content);
30
+ if (!appJson.name) {
31
+ throw new Error('app.json must contain a "name" field');
32
+ }
33
+ return appJson;
34
+ }
35
+ async function createZipBuffer(distPath) {
36
+ return new Promise((resolve, reject) => {
37
+ const chunks = [];
38
+ const archive = archiver("zip", { zlib: { level: 9 } });
39
+ archive.on("data", (chunk) => chunks.push(chunk));
40
+ archive.on("end", () => resolve(Buffer.concat(chunks)));
41
+ archive.on("error", reject);
42
+ archive.directory(distPath, false);
43
+ archive.finalize();
44
+ });
45
+ }
46
+ async function syncToApi(distPath, org, appName, fileId, apiKey) {
47
+ console.log(`Creating zip from ${distPath}...`);
48
+ const zipBuffer = await createZipBuffer(distPath);
49
+ console.log(`Zip created: ${(zipBuffer.length / 1024).toFixed(2)} KB`);
50
+ const formData = new FormData();
51
+ formData.append("app_name", appName);
52
+ formData.append("file_id", fileId);
53
+ formData.append("zip", zipBuffer, {
54
+ filename: "app.zip",
55
+ contentType: "application/zip",
56
+ });
57
+ const endpoint = `https://${org}.my.pitcher.com/core/api/protected/development/s3-app-sync`;
58
+ console.log(`Uploading to ${endpoint}...`);
59
+ try {
60
+ const response = await axios.post(endpoint, formData, {
61
+ headers: {
62
+ "x-api-key": apiKey,
63
+ ...formData.getHeaders(),
64
+ },
65
+ maxBodyLength: Infinity,
66
+ maxContentLength: Infinity,
67
+ });
68
+ return response.data;
69
+ }
70
+ catch (error) {
71
+ const axiosError = error;
72
+ const errData = axiosError?.response?.data;
73
+ throw new Error(`Upload failed (${axiosError?.response?.status}): ${errData?.error || errData?.detail || axiosError.message}`);
74
+ }
75
+ }
76
+ async function revertDevOverride(org, fileId, apiKey) {
77
+ const endpoint = `https://${org}.my.pitcher.com/core/api/protected/development/s3-app-sync`;
78
+ try {
79
+ const response = await axios.delete(endpoint, {
80
+ headers: {
81
+ "x-api-key": apiKey,
82
+ "Content-Type": "application/json",
83
+ },
84
+ data: { file_id: fileId },
85
+ });
86
+ console.log("✅ Dev override reverted");
87
+ console.log(` Restored version: ${response.data.restored_version}`);
88
+ }
89
+ catch (error) {
90
+ const axiosError = error;
91
+ const errData = axiosError?.response?.data;
92
+ throw new Error(`Revert failed (${axiosError?.response?.status}): ${errData?.error || errData?.detail || axiosError.message}`);
93
+ }
94
+ }
95
+ export function createDevSyncCommand() {
96
+ const program = new Command("dev-sync");
97
+ program.description("Sync a marketplace app to S3 for live development testing");
98
+ program
99
+ .command("sync")
100
+ .description("One-time sync of the app to S3")
101
+ .requiredOption("--file-id <id>", "File ID of the installed app (pitcher_file.id)")
102
+ .option("--api-key <apiKey>", "API Key for authentication")
103
+ .option("--org <organization>", "Organization subdomain", "dev")
104
+ .action(async (options) => {
105
+ try {
106
+ const apiKey = options.apiKey || process.env.NG_API_KEY;
107
+ if (!apiKey) {
108
+ console.error("Error: API key is required. Provide it via --api-key or set the NG_API_KEY environment variable.");
109
+ process.exit(1);
110
+ }
111
+ const distPath = findDistPath();
112
+ const appJson = readAppJson(distPath);
113
+ console.log(`Syncing ${appJson.name}...`);
114
+ console.log(` From: ${distPath}`);
115
+ const result = await syncToApi(distPath, options.org, appJson.name, options.fileId, apiKey);
116
+ console.log("\n✅ Sync completed!");
117
+ console.log(` Dev version: ${result.dev_version}`);
118
+ console.log(` Original version: ${result.original_version}`);
119
+ console.log(` Files uploaded: ${result.files_uploaded}`);
120
+ console.log(` S3 prefix: ${result.s3_prefix}`);
121
+ }
122
+ catch (error) {
123
+ console.error("❌ Sync failed:", error instanceof Error ? error.message : error);
124
+ process.exit(1);
125
+ }
126
+ });
127
+ program
128
+ .command("watch")
129
+ .description("Watch the dist folder and sync on changes")
130
+ .requiredOption("--file-id <id>", "File ID of the installed app (pitcher_file.id)")
131
+ .option("--api-key <apiKey>", "API Key for authentication")
132
+ .option("--org <organization>", "Organization subdomain", "dev")
133
+ .option("--debounce <ms>", "Debounce time in milliseconds", "2500")
134
+ .action(async (options) => {
135
+ const apiKey = options.apiKey || process.env.NG_API_KEY;
136
+ if (!apiKey) {
137
+ console.error("Error: API key is required. Provide it via --api-key or set the NG_API_KEY environment variable.");
138
+ process.exit(1);
139
+ }
140
+ const distPath = findDistPath();
141
+ const debounceMs = parseInt(options.debounce, 10);
142
+ let appJson;
143
+ try {
144
+ appJson = readAppJson(distPath);
145
+ }
146
+ catch (error) {
147
+ console.error("❌ Could not read app.json. Make sure the dist folder exists and contains app.json.");
148
+ process.exit(1);
149
+ }
150
+ console.log(`👀 Watching ${distPath} for changes...`);
151
+ console.log(` App: ${appJson.name}`);
152
+ console.log(` Org: ${options.org}`);
153
+ console.log(` File ID: ${options.fileId}`);
154
+ console.log(` Debounce: ${debounceMs}ms`);
155
+ console.log("");
156
+ let syncTimeout = null;
157
+ let isSyncing = false;
158
+ const doSync = async () => {
159
+ if (isSyncing) {
160
+ console.log("⏳ Sync already in progress, queuing...");
161
+ syncTimeout = setTimeout(doSync, debounceMs);
162
+ return;
163
+ }
164
+ isSyncing = true;
165
+ const startTime = Date.now();
166
+ try {
167
+ const currentAppJson = readAppJson(distPath);
168
+ console.log(`\n🔄 Syncing ${currentAppJson.name}...`);
169
+ const result = await syncToApi(distPath, options.org, currentAppJson.name, options.fileId, apiKey);
170
+ const duration = ((Date.now() - startTime) / 1000).toFixed(1);
171
+ console.log(`✅ Synced in ${duration}s - ${result.files_uploaded} files`);
172
+ }
173
+ catch (error) {
174
+ console.error("❌ Sync failed:", error instanceof Error ? error.message : error);
175
+ }
176
+ finally {
177
+ isSyncing = false;
178
+ }
179
+ };
180
+ const watcher = chokidar.watch(distPath, {
181
+ persistent: true,
182
+ ignoreInitial: true,
183
+ awaitWriteFinish: {
184
+ stabilityThreshold: 500,
185
+ pollInterval: 100,
186
+ },
187
+ });
188
+ watcher.on("all", (_event, filePath) => {
189
+ if (syncTimeout) {
190
+ clearTimeout(syncTimeout);
191
+ }
192
+ console.log(`📝 Change detected: ${path.relative(distPath, filePath)}`);
193
+ syncTimeout = setTimeout(doSync, debounceMs);
194
+ });
195
+ // Initial sync
196
+ console.log("🚀 Initial sync...");
197
+ await doSync();
198
+ // Graceful shutdown - revert dev override
199
+ const shutdown = async (signal) => {
200
+ console.log(`\n👋 ${signal} received, reverting dev override...`);
201
+ watcher.close();
202
+ try {
203
+ await revertDevOverride(options.org, options.fileId, apiKey);
204
+ }
205
+ catch (error) {
206
+ console.error("⚠️ Failed to revert:", error instanceof Error ? error.message : error);
207
+ }
208
+ process.exit(0);
209
+ };
210
+ process.on("SIGINT", () => shutdown("SIGINT"));
211
+ process.on("SIGTERM", () => shutdown("SIGTERM"));
212
+ });
213
+ program
214
+ .command("revert")
215
+ .description("Revert the dev override and restore the original version")
216
+ .requiredOption("--file-id <id>", "File ID of the installed app (pitcher_file.id)")
217
+ .option("--api-key <apiKey>", "API Key for authentication")
218
+ .option("--org <organization>", "Organization subdomain", "dev")
219
+ .action(async (options) => {
220
+ try {
221
+ const apiKey = options.apiKey || process.env.NG_API_KEY;
222
+ if (!apiKey) {
223
+ console.error("Error: API key is required. Provide it via --api-key or set the NG_API_KEY environment variable.");
224
+ process.exit(1);
225
+ }
226
+ await revertDevOverride(options.org, options.fileId, apiKey);
227
+ }
228
+ catch (error) {
229
+ console.error("❌ Revert failed:", error instanceof Error ? error.message : error);
230
+ process.exit(1);
231
+ }
232
+ });
233
+ return program;
234
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pitcher/cli-scripts",
3
- "version": "2.0.8",
3
+ "version": "2.1.0",
4
4
  "description": "The Pitcher CLI for various tasks in its ecosystem.",
5
5
  "author": "Pitcher AG",
6
6
  "license": "MIT",
@@ -23,14 +23,15 @@
23
23
  "dist"
24
24
  ],
25
25
  "devDependencies": {
26
- "@types/archiver": "^6.0.3",
26
+ "@types/archiver": "^7.0.0",
27
27
  "@types/node": "^22.18.0",
28
28
  "typescript": "^5.9.2"
29
29
  },
30
30
  "dependencies": {
31
31
  "archiver": "^7.0.1",
32
32
  "axios": "^1.12.0",
33
- "commander": "^13.1.0",
33
+ "chokidar": "^5.0.0",
34
+ "commander": "^14.0.2",
34
35
  "form-data": "^4.0.2"
35
36
  }
36
37
  }