@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 +38 -0
- package/dist/index.js +2 -0
- package/dist/src/devSync.d.ts +2 -0
- package/dist/src/devSync.js +234 -0
- package/package.json +4 -3
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,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
|
|
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": "^
|
|
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
|
-
"
|
|
33
|
+
"chokidar": "^5.0.0",
|
|
34
|
+
"commander": "^14.0.2",
|
|
34
35
|
"form-data": "^4.0.2"
|
|
35
36
|
}
|
|
36
37
|
}
|