@pitcher/cli-scripts 2.1.2 → 2.2.1

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,60 @@
1
+ import axios from 'axios';
2
+ import { createCliError, isCliError } from './errors.js';
3
+ export function makeClient(baseUrl, apiKey) {
4
+ return axios.create({
5
+ baseURL: baseUrl,
6
+ headers: {
7
+ accept: 'application/json',
8
+ 'x-api-key': apiKey,
9
+ },
10
+ });
11
+ }
12
+ /**
13
+ * Build a human-readable message from a server error body. Handles DRF shapes:
14
+ * a `detail`/`error` string, `non_field_errors`, and field-level error arrays
15
+ * (e.g. `{ version: ["already exists"] }`). Falls back to the raw body so the
16
+ * full server response is visible in logs instead of a bare status line.
17
+ */
18
+ export function extractApiMessage(data, fallback) {
19
+ if (data == null)
20
+ return fallback;
21
+ if (typeof data === 'string')
22
+ return data.trim() || fallback;
23
+ if (typeof data !== 'object')
24
+ return String(data);
25
+ const obj = data;
26
+ if (typeof obj.detail === 'string' && obj.detail)
27
+ return obj.detail;
28
+ if (typeof obj.error === 'string' && obj.error)
29
+ return obj.error;
30
+ const parts = [];
31
+ for (const [key, val] of Object.entries(obj)) {
32
+ const text = Array.isArray(val) ? val.join('; ') : typeof val === 'string' ? val : JSON.stringify(val);
33
+ parts.push(key === 'non_field_errors' ? text : `${key}: ${text}`);
34
+ }
35
+ if (parts.length)
36
+ return parts.join(' | ');
37
+ return fallback;
38
+ }
39
+ export function toCliError(e) {
40
+ if (isCliError(e))
41
+ return e;
42
+ const err = (e ?? {});
43
+ const data = err.response?.data;
44
+ const apiMessage = extractApiMessage(data, err.message || 'Request failed');
45
+ const status = err.response?.status;
46
+ if (typeof status === 'number') {
47
+ if (status === 401 || status === 403)
48
+ return createCliError('AUTH', apiMessage);
49
+ if (status === 404)
50
+ return createCliError('NOT_FOUND', apiMessage);
51
+ if (status === 409)
52
+ return createCliError('CONFLICT', apiMessage);
53
+ if (status >= 400 && status < 500)
54
+ return createCliError('VALIDATION', apiMessage);
55
+ return createCliError('NETWORK', apiMessage);
56
+ }
57
+ if (err.isAxiosError)
58
+ return createCliError('NETWORK', apiMessage);
59
+ return createCliError('UNKNOWN', apiMessage);
60
+ }
@@ -0,0 +1,22 @@
1
+ import type { Envelope } from './envelope.js';
2
+ export interface Writer {
3
+ out: (s: string) => void;
4
+ err: (s: string) => void;
5
+ }
6
+ export declare const defaultWriter: Writer;
7
+ export declare function shouldUseJson(opts: {
8
+ output?: string;
9
+ }, isTty: boolean): boolean;
10
+ export declare function log(msg: string, { quiet, write }: {
11
+ quiet?: boolean;
12
+ write?: Writer;
13
+ }): void;
14
+ export declare function renderEnvelope(env: Envelope<unknown>, { json, quiet, write }: {
15
+ json: boolean;
16
+ quiet?: boolean;
17
+ write?: Writer;
18
+ }): number;
19
+ export declare function emitEnvelope(env: Envelope<unknown>, globals: {
20
+ output?: string;
21
+ quiet?: boolean;
22
+ }, isTty?: boolean): void;
@@ -0,0 +1,45 @@
1
+ export const defaultWriter = {
2
+ out: (s) => process.stdout.write(s + '\n'),
3
+ err: (s) => process.stderr.write(s + '\n'),
4
+ };
5
+ export function shouldUseJson(opts, isTty) {
6
+ if (opts.output === 'json')
7
+ return true;
8
+ if (opts.output === 'text')
9
+ return false;
10
+ return !isTty;
11
+ }
12
+ export function log(msg, { quiet, write = defaultWriter }) {
13
+ if (quiet)
14
+ return;
15
+ write.err(msg);
16
+ }
17
+ function formatHuman(data) {
18
+ if (typeof data === 'string')
19
+ return data;
20
+ return JSON.stringify(data, null, 2);
21
+ }
22
+ export function renderEnvelope(env, { json, quiet, write = defaultWriter }) {
23
+ if (json) {
24
+ write.out(JSON.stringify(env));
25
+ return env.error?.exitCode ?? 0;
26
+ }
27
+ if (env.ok) {
28
+ if (!quiet && env.data !== undefined)
29
+ write.out(formatHuman(env.data));
30
+ }
31
+ else if (env.error) {
32
+ write.err(`Error [${env.error.code}]: ${env.error.message}`);
33
+ }
34
+ if (!quiet && env.warnings?.length) {
35
+ for (const w of env.warnings)
36
+ write.err(`warning: ${w}`);
37
+ }
38
+ return env.error?.exitCode ?? 0;
39
+ }
40
+ export function emitEnvelope(env, globals, isTty = Boolean(process.stdout.isTTY)) {
41
+ const json = shouldUseJson(globals, isTty);
42
+ // Set exitCode and let the process drain naturally — process.exit() can
43
+ // truncate piped stdout before the envelope flushes in non-TTY mode.
44
+ process.exitCode = renderEnvelope(env, { json, quiet: globals.quiet });
45
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pitcher/cli-scripts",
3
- "version": "2.1.2",
3
+ "version": "2.2.1",
4
4
  "description": "The Pitcher CLI for various tasks in its ecosystem.",
5
5
  "author": "Pitcher AG",
6
6
  "license": "MIT",
@@ -14,6 +14,8 @@
14
14
  "types": "./dist/index.d.ts",
15
15
  "scripts": {
16
16
  "build": "tsc",
17
+ "test": "vitest run",
18
+ "test:watch": "vitest",
17
19
  "prepublishOnly": "yarn build"
18
20
  },
19
21
  "bin": {
@@ -25,13 +27,14 @@
25
27
  "devDependencies": {
26
28
  "@types/archiver": "^7.0.0",
27
29
  "@types/node": "^24.10.9",
28
- "typescript": "^5.9.3"
30
+ "typescript": "^6.0.3",
31
+ "vitest": "^4.1.9"
29
32
  },
30
33
  "dependencies": {
31
34
  "archiver": "^7.0.1",
32
- "axios": "^1.13.5",
35
+ "axios": "^1.16.0",
33
36
  "chokidar": "^5.0.0",
34
37
  "commander": "^14.0.3",
35
- "form-data": "^4.0.5"
38
+ "form-data": "^4.0.6"
36
39
  }
37
40
  }
@@ -1,2 +0,0 @@
1
- import { Command } from "commander";
2
- export declare function createCopyInstanceFilesCommand(): Command;
@@ -1,17 +0,0 @@
1
- import { Command } from "commander";
2
- export function createCopyInstanceFilesCommand() {
3
- const program = new Command("copy-instance-files");
4
- program
5
- .arguments("<fromInstance> <toInstance>")
6
- .requiredOption("--api-key <apiKey>", "API Key", process.env.PITCHER_API_KEY)
7
- .requiredOption("--org <orgName>", "Organization Name", process.env.PITCHER_ORG)
8
- .description('clone instance files into another one e.g. "p copy-instance-files <fromInstanceId> <toInstanceId> --org mondelez --api-key myapikey"')
9
- .action((sourceInstanceId, destinationInstanceId, options) => {
10
- console.log("clone instance files command called", {
11
- sourceInstanceId,
12
- destinationInstanceId,
13
- options,
14
- });
15
- });
16
- return program;
17
- }
package/dist/package.json DELETED
@@ -1,40 +0,0 @@
1
- {
2
- "name": "@pitcher/cli-scripts",
3
- "version": "1.0.6",
4
- "description": "The Pitcher CLI for various tasks in its ecosystem.",
5
- "author": "Pitcher AG",
6
- "license": "MIT",
7
- "repository": {
8
- "type": "git",
9
- "url": "https://github.com/PitcherAG/ng-scripts.git"
10
- },
11
- "keywords": [
12
- "pitcher",
13
- "cli",
14
- "ng-scripts"
15
- ],
16
- "type": "module",
17
- "main": "./dist/index.js",
18
- "types": "./dist/index.d.ts",
19
- "scripts": {
20
- "build": "tsc",
21
- "prepublishOnly": "npm run build"
22
- },
23
- "bin": {
24
- "p": "node --no-warnings ./dist/index.js"
25
- },
26
- "files": [
27
- "dist"
28
- ],
29
- "devDependencies": {
30
- "@types/archiver": "^6.0.2",
31
- "@types/node": "^20.0.0",
32
- "typescript": "^5.4.5"
33
- },
34
- "dependencies": {
35
- "archiver": "^7.0.1",
36
- "axios": "^1.7.2",
37
- "commander": "^12.1.0",
38
- "form-data": "^4.0.0"
39
- }
40
- }
@@ -1,2 +0,0 @@
1
- import { Command } from "commander";
2
- export declare function createPublishAppCommand(): Command;
@@ -1,93 +0,0 @@
1
- import { Command } from "commander";
2
- import archiver from "archiver";
3
- import axios from "axios";
4
- import fs from "fs";
5
- import path from "path";
6
- import FormData from "form-data";
7
- import { fileURLToPath } from 'url';
8
- export function createPublishAppCommand() {
9
- const program = new Command("publish-app");
10
- program
11
- .arguments("[folder]")
12
- .requiredOption("--api-key <apiKey>", "API Key for authentication")
13
- .option("--org <organization>", "Organization subdomain", "dev")
14
- .option("--increment-version", "Increment version in app.json")
15
- .option("--dry-run", "Perform a dry run without publishing")
16
- .option("--publish-url <url>", "Explicit publish URL")
17
- .description('zip the contents of a folder and publish as an app to a pitcher org URL. for current folder: "p publish-app --api-key myapikey"')
18
- .action(async (folder = ".", options) => {
19
- try {
20
- const folderPath = path.resolve(folder);
21
- if (options.incrementVersion) {
22
- const appJsonPath = path.join(folderPath, "app.json");
23
- if (fs.existsSync(appJsonPath)) {
24
- const appJson = JSON.parse(fs.readFileSync(appJsonPath, "utf-8"));
25
- const now = new Date();
26
- const year = now.getFullYear();
27
- const month = String(now.getMonth() + 1).padStart(2, "0");
28
- const day = String(now.getDate()).padStart(2, "0");
29
- const timestamp = now.getTime();
30
- const version = `${year}.${month}.${day}.${timestamp}`;
31
- appJson.version = version;
32
- fs.writeFileSync(appJsonPath, JSON.stringify(appJson, null, 2));
33
- console.log(`Updated app.json version to ${version}`);
34
- }
35
- else {
36
- console.error("app.json not found in the specified folder.");
37
- return;
38
- }
39
- }
40
- const currentDir = path.dirname(fileURLToPath(import.meta.url));
41
- const zipFilePath = path.join(currentDir, "app.zip");
42
- const output = fs.createWriteStream(zipFilePath);
43
- const archive = archiver("zip", {
44
- zlib: { level: 9 },
45
- });
46
- output.on("close", async () => {
47
- console.log(`Zipped ${archive.pointer()} total bytes`);
48
- const formData = new FormData();
49
- formData.append("file", fs.createReadStream(zipFilePath));
50
- const publishUrl = options.publishUrl || `https://${options.org}.my.pitcher.com/api/v1/apps/publish/`;
51
- if (options.dryRun) {
52
- console.log("Dry run mode enabled. The following data would be published:");
53
- console.log("Publish URL:", publishUrl);
54
- console.log("Headers:", {
55
- ...formData.getHeaders(),
56
- "x-api-key": options.apiKey,
57
- });
58
- console.log("app.json content:", fs.readFileSync(path.join(folderPath, "app.json"), "utf-8"));
59
- fs.unlinkSync(zipFilePath);
60
- }
61
- else {
62
- try {
63
- const response = await axios.post(publishUrl, formData, {
64
- headers: {
65
- ...formData.getHeaders(),
66
- "x-api-key": options.apiKey,
67
- },
68
- });
69
- console.log("Publish response:", response.data);
70
- }
71
- catch (e) {
72
- const error = e;
73
- const errData = error?.response?.data;
74
- console.error("Error publishing:", errData?.detail ?? errData);
75
- }
76
- finally {
77
- fs.unlinkSync(zipFilePath);
78
- }
79
- }
80
- });
81
- archive.on("error", (err) => {
82
- throw err;
83
- });
84
- archive.pipe(output);
85
- archive.directory(folderPath, false);
86
- await archive.finalize();
87
- }
88
- catch (error) {
89
- console.error("Error zipping or publishing:", error);
90
- }
91
- });
92
- return program;
93
- }
@@ -1,2 +0,0 @@
1
- import { Command } from "commander";
2
- export declare function createCopyInstanceFilesCommand(): Command;
@@ -1,17 +0,0 @@
1
- import { Command } from "commander";
2
- export function createCopyInstanceFilesCommand() {
3
- const program = new Command("copy-instance-files");
4
- program
5
- .arguments("<fromInstance> <toInstance>")
6
- .requiredOption("--api-key <apiKey>", "API Key", process.env.PITCHER_API_KEY)
7
- .requiredOption("--org <orgName>", "Organization Name", process.env.PITCHER_ORG)
8
- .description('clone instance files into another one e.g. "p copy-instance-files <fromInstanceId> <toInstanceId> --org pitcher --api-key myapikey"')
9
- .action((sourceInstanceId, destinationInstanceId, options) => {
10
- console.log("clone instance files command called", {
11
- sourceInstanceId,
12
- destinationInstanceId,
13
- options,
14
- });
15
- });
16
- return program;
17
- }
@@ -1,2 +0,0 @@
1
- import { Command } from "commander";
2
- export declare function createDevSyncCommand(): Command;
@@ -1,250 +0,0 @@
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
- let isShuttingDown = false;
159
- const doSync = async () => {
160
- if (isShuttingDown)
161
- return;
162
- if (isSyncing) {
163
- console.log("⏳ Sync already in progress, queuing...");
164
- syncTimeout = setTimeout(doSync, debounceMs);
165
- return;
166
- }
167
- isSyncing = true;
168
- const startTime = Date.now();
169
- try {
170
- const currentAppJson = readAppJson(distPath);
171
- console.log(`\n🔄 Syncing ${currentAppJson.name}...`);
172
- const result = await syncToApi(distPath, options.org, currentAppJson.name, options.fileId, apiKey);
173
- const duration = ((Date.now() - startTime) / 1000).toFixed(1);
174
- console.log(`✅ Synced in ${duration}s - ${result.files_uploaded} files`);
175
- }
176
- catch (error) {
177
- console.error("❌ Sync failed:", error instanceof Error ? error.message : error);
178
- }
179
- finally {
180
- isSyncing = false;
181
- }
182
- };
183
- const watcher = chokidar.watch(distPath, {
184
- persistent: true,
185
- ignoreInitial: true,
186
- awaitWriteFinish: {
187
- stabilityThreshold: 500,
188
- pollInterval: 100,
189
- },
190
- });
191
- watcher.on("all", (_event, filePath) => {
192
- if (syncTimeout) {
193
- clearTimeout(syncTimeout);
194
- }
195
- console.log(`📝 Change detected: ${path.relative(distPath, filePath)}`);
196
- syncTimeout = setTimeout(doSync, debounceMs);
197
- });
198
- // Graceful shutdown - revert dev override (register before initial sync)
199
- const shutdown = async (signal) => {
200
- if (isShuttingDown)
201
- return;
202
- isShuttingDown = true;
203
- console.log(`\n👋 ${signal} received...`);
204
- watcher.close();
205
- if (syncTimeout)
206
- clearTimeout(syncTimeout);
207
- // Wait for any in-progress sync to complete
208
- if (isSyncing) {
209
- console.log("⏳ Waiting for sync to complete...");
210
- while (isSyncing) {
211
- await new Promise((resolve) => setTimeout(resolve, 100));
212
- }
213
- }
214
- console.log("🔄 Reverting dev override...");
215
- try {
216
- await revertDevOverride(options.org, options.fileId, apiKey);
217
- }
218
- catch (error) {
219
- console.error("⚠️ Failed to revert:", error instanceof Error ? error.message : error);
220
- }
221
- process.exit(0);
222
- };
223
- process.on("SIGINT", () => shutdown("SIGINT"));
224
- process.on("SIGTERM", () => shutdown("SIGTERM"));
225
- // Initial sync
226
- console.log("🚀 Initial sync...");
227
- await doSync();
228
- });
229
- program
230
- .command("revert")
231
- .description("Revert the dev override and restore the original version")
232
- .requiredOption("--file-id <id>", "File ID of the installed app (pitcher_file.id)")
233
- .option("--api-key <apiKey>", "API Key for authentication")
234
- .option("--org <organization>", "Organization subdomain", "dev")
235
- .action(async (options) => {
236
- try {
237
- const apiKey = options.apiKey || process.env.NG_API_KEY;
238
- if (!apiKey) {
239
- console.error("Error: API key is required. Provide it via --api-key or set the NG_API_KEY environment variable.");
240
- process.exit(1);
241
- }
242
- await revertDevOverride(options.org, options.fileId, apiKey);
243
- }
244
- catch (error) {
245
- console.error("❌ Revert failed:", error instanceof Error ? error.message : error);
246
- process.exit(1);
247
- }
248
- });
249
- return program;
250
- }
@@ -1,2 +0,0 @@
1
- import { Command } from "commander";
2
- export declare function createPublishAppCommand(): Command;