farseer-cli 1.0.0 → 1.0.2
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/dist/commands/run.js +46 -4
- package/dist/commands/run.js.map +2 -2
- package/dist/services/appSyncService.js +38 -9
- package/dist/services/appSyncService.js.map +2 -2
- package/dist/services/farseerApi.js +48 -0
- package/dist/services/farseerApi.js.map +2 -2
- package/dist/services/syncService.js +37 -9
- package/dist/services/syncService.js.map +3 -3
- package/dist/utils/helpers.js +13 -0
- package/dist/utils/helpers.js.map +2 -2
- package/package.json +1 -1
package/dist/commands/run.js
CHANGED
|
@@ -38,6 +38,26 @@ var import_helpers = require("../utils/helpers");
|
|
|
38
38
|
var fs = __toESM(require("fs"));
|
|
39
39
|
var path = __toESM(require("path"));
|
|
40
40
|
var import_child_process = require("child_process");
|
|
41
|
+
function findScriptRecursively(baseDir, scriptName) {
|
|
42
|
+
if (!fs.existsSync(baseDir)) {
|
|
43
|
+
return null;
|
|
44
|
+
}
|
|
45
|
+
const search = (dir, relativePath = "") => {
|
|
46
|
+
const items = fs.readdirSync(dir, { withFileTypes: true });
|
|
47
|
+
for (const item of items) {
|
|
48
|
+
const itemRelPath = relativePath ? `${relativePath}/${item.name}` : item.name;
|
|
49
|
+
if (item.isFile() && item.name === scriptName) {
|
|
50
|
+
return itemRelPath;
|
|
51
|
+
}
|
|
52
|
+
if (item.isDirectory()) {
|
|
53
|
+
const found = search(path.join(dir, item.name), itemRelPath);
|
|
54
|
+
if (found) return found;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
return null;
|
|
58
|
+
};
|
|
59
|
+
return search(baseDir);
|
|
60
|
+
}
|
|
41
61
|
async function getRunCredentials(tenant, tenantId) {
|
|
42
62
|
const credential = (0, import_configService.getCredential)(tenant);
|
|
43
63
|
if (credential) {
|
|
@@ -114,7 +134,17 @@ function registerRunCommand(program) {
|
|
|
114
134
|
}
|
|
115
135
|
const tenantDir = (0, import_helpers.getTenantDir)(tenant);
|
|
116
136
|
const srcDir = (0, import_helpers.getTenantSrcDir)(tenant);
|
|
117
|
-
const
|
|
137
|
+
const filesDir = (0, import_helpers.getTenantFilesDir)(tenant);
|
|
138
|
+
let scriptPath = path.resolve(srcDir, scriptName);
|
|
139
|
+
let relativeScriptPath = `files/Scripts/${scriptName}`;
|
|
140
|
+
if (!fs.existsSync(scriptPath)) {
|
|
141
|
+
const foundPath = findScriptRecursively(filesDir, path.basename(scriptName));
|
|
142
|
+
if (foundPath) {
|
|
143
|
+
scriptPath = path.resolve(filesDir, foundPath);
|
|
144
|
+
relativeScriptPath = `files/${foundPath}`;
|
|
145
|
+
import_logger.logger.dim(`Found script at: ${foundPath}`);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
118
148
|
if (!fs.existsSync(scriptPath)) {
|
|
119
149
|
import_logger.logger.error(`Script not found locally: ${scriptPath}`);
|
|
120
150
|
import_logger.logger.dim('Run "farseer pull ' + tenant + '" to download scripts first.');
|
|
@@ -165,7 +195,8 @@ function registerRunCommand(program) {
|
|
|
165
195
|
for (const arg of app.expectedArguments) {
|
|
166
196
|
cmdArgs.push(appArgs[arg.name] ?? arg.defaultValue);
|
|
167
197
|
}
|
|
168
|
-
const
|
|
198
|
+
const quotedScriptPath = `"${relativeScriptPath}"`;
|
|
199
|
+
const child = (0, import_child_process.spawn)("npx", ["tsx", quotedScriptPath, ...cmdArgs], {
|
|
169
200
|
stdio: "inherit",
|
|
170
201
|
cwd: tenantDir,
|
|
171
202
|
shell: true,
|
|
@@ -195,7 +226,17 @@ function registerRunCommand(program) {
|
|
|
195
226
|
}
|
|
196
227
|
const tenantDir = (0, import_helpers.getTenantDir)(tenant);
|
|
197
228
|
const srcDir = (0, import_helpers.getTenantSrcDir)(tenant);
|
|
198
|
-
const
|
|
229
|
+
const filesDir = (0, import_helpers.getTenantFilesDir)(tenant);
|
|
230
|
+
let scriptPath = path.resolve(srcDir, script);
|
|
231
|
+
let relativeScriptPath = `files/Scripts/${script}`;
|
|
232
|
+
if (!fs.existsSync(scriptPath)) {
|
|
233
|
+
const foundPath = findScriptRecursively(filesDir, path.basename(script));
|
|
234
|
+
if (foundPath) {
|
|
235
|
+
scriptPath = path.resolve(filesDir, foundPath);
|
|
236
|
+
relativeScriptPath = `files/${foundPath}`;
|
|
237
|
+
import_logger.logger.dim(`Found script at: ${foundPath}`);
|
|
238
|
+
}
|
|
239
|
+
}
|
|
199
240
|
if (!fs.existsSync(scriptPath)) {
|
|
200
241
|
import_logger.logger.error(`Script not found: ${scriptPath}`);
|
|
201
242
|
process.exit(1);
|
|
@@ -220,7 +261,8 @@ function registerRunCommand(program) {
|
|
|
220
261
|
if (credentials.accessToken) {
|
|
221
262
|
envVars.FARSEER_ACCESS_TOKEN = credentials.accessToken;
|
|
222
263
|
}
|
|
223
|
-
const
|
|
264
|
+
const quotedScriptPath = `"${relativeScriptPath}"`;
|
|
265
|
+
const child = (0, import_child_process.spawn)("npx", ["tsx", quotedScriptPath], {
|
|
224
266
|
stdio: "inherit",
|
|
225
267
|
cwd: tenantDir,
|
|
226
268
|
shell: true,
|
package/dist/commands/run.js.map
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../src/commands/run.ts"],
|
|
4
|
-
"sourcesContent": ["import { Command } from 'commander';\nimport { logger } from '../utils/logger';\nimport { getCredential, getUserAuth, isUserAuthValid, setUserAuth, generateBasePath } from '../services/configService';\nimport { refreshAccessToken } from '../services/farseerService';\nimport { getFarseerClientWithFallback, showLoginPrompt, IFarseerClient } from '../services/farseerFactory';\nimport { getTenantDir, getTenantSrcDir, parseAppArgs } from '../utils/helpers';\nimport * as fs from 'fs';\nimport * as path from 'path';\nimport { spawn } from 'child_process';\n\ninterface RunCredentials {\n tenantId: string;\n apiKey?: string;\n accessToken?: string;\n basePath: string;\n}\n\nasync function getRunCredentials(tenant: string, tenantId?: string): Promise<RunCredentials | null> {\n // First try API key config\n const credential = getCredential(tenant);\n if (credential) {\n return {\n tenantId: credential.tenantId,\n apiKey: credential.apiKey,\n basePath: credential.basePath,\n };\n }\n\n // Try user auth (browser login)\n const auth = getUserAuth();\n if (!auth) {\n return null;\n }\n\n // Check if token needs refresh\n if (!isUserAuthValid()) {\n logger.dim('Token expired, refreshing...');\n const refreshed = await refreshAccessToken(auth.refreshToken, auth.realm || 'master');\n if (!refreshed) {\n logger.warning('Could not refresh token. Please login again with: farseer login');\n return null;\n }\n // Save refreshed token\n setUserAuth({\n accessToken: refreshed.accessToken,\n refreshToken: refreshed.refreshToken,\n expiresAt: new Date(Date.now() + refreshed.expiresIn * 1000).toISOString(),\n realm: auth.realm,\n });\n logger.success('Token refreshed');\n }\n\n const updatedAuth = getUserAuth();\n if (!updatedAuth) return null;\n\n // Use tenant name as organisation for basePath, tenantId for TENANT_ID env var\n return {\n tenantId: tenantId || tenant,\n accessToken: updatedAuth.accessToken,\n basePath: generateBasePath(tenant),\n };\n}\n\n// Helper to collect multiple --arg options\nfunction collectArgs(value: string, previous: string[]): string[] {\n return previous.concat([value]);\n}\n\nexport function registerRunCommand(program: Command): void {\n program\n .command('run <args...>')\n .description('Run an app locally with injected credentials')\n .option('--tenant-id <id>', 'Tenant ID for env variables (defaults to tenant name)')\n .option('--arg <value>', 'Pass argument in format \"Name=Value\" (can be used multiple times)', collectArgs, [])\n .action(async (args: string[], options) => {\n const { tenant, appName } = parseAppArgs(args);\n // First, get the app details to find the entrypoint script\n const clientResult = await getFarseerClientWithFallback(tenant, options.tenantId);\n\n if (!clientResult) {\n showLoginPrompt(tenant);\n process.exit(1);\n }\n\n const api: IFarseerClient = clientResult.client;\n\n // Find the app\n logger.info(`Loading app: ${appName}...`);\n const app = await api.getAppByName(appName);\n\n if (!app) {\n logger.error(`App not found: ${appName}`);\n logger.dim('List available apps with: farseer apps ' + tenant);\n process.exit(1);\n }\n\n if (!app.mainScriptFileId || app.scriptFiles.length === 0) {\n logger.error(`App \"${appName}\" has no scripts configured.`);\n logger.dim('Configure the app with: farseer app configure \"' + appName + '\" --entrypoint <script>');\n process.exit(1);\n }\n\n // Find the entrypoint script name\n const mainScript = app.scriptFiles.find(s => s.id === app.mainScriptFileId);\n if (!mainScript) {\n logger.error(`Entrypoint script not found for app \"${appName}\".`);\n process.exit(1);\n }\n\n // Normalize script name - handle both \"test.ts\" and \"Scripts/test.ts\" formats\n let scriptName = mainScript.name;\n if (scriptName.toLowerCase().startsWith('scripts/')) {\n scriptName = scriptName.substring('scripts/'.length);\n }\n\n // Get credentials for running\n const credentials = await getRunCredentials(tenant, options.tenantId);\n\n if (!credentials) {\n showLoginPrompt(tenant);\n process.exit(1);\n }\n\n const tenantDir = getTenantDir(tenant);\n const srcDir = getTenantSrcDir(tenant);\n\n // Script path is relative to files/Scripts/ folder\n const scriptPath = path.resolve(srcDir, scriptName);\n\n if (!fs.existsSync(scriptPath)) {\n logger.error(`Script not found locally: ${scriptPath}`);\n logger.dim('Run \"farseer pull ' + tenant + '\" to download scripts first.');\n process.exit(1);\n }\n\n // Check if node_modules exists\n const nodeModulesPath = path.join(tenantDir, 'node_modules');\n if (!fs.existsSync(nodeModulesPath)) {\n logger.error(`Dependencies not installed for ${tenant}`);\n logger.dim(`Run: farseer install ${tenant}`);\n process.exit(1);\n }\n\n // Parse arguments\n const appArgs: Record<string, string> = {};\n\n // Start with default values from app config\n for (const arg of app.expectedArguments) {\n appArgs[arg.name] = arg.defaultValue;\n }\n\n // Override with provided arguments\n for (const argStr of options.arg) {\n const eqIndex = argStr.indexOf('=');\n if (eqIndex === -1) {\n logger.error(`Invalid argument format: ${argStr}`);\n logger.dim('Use format: --arg \"Name=Value\"');\n process.exit(1);\n }\n const name = argStr.substring(0, eqIndex);\n const value = argStr.substring(eqIndex + 1);\n appArgs[name] = value;\n }\n\n const authType = credentials.apiKey ? 'API key' : 'access token';\n logger.info(`Running app \"${app.name}\" (${scriptName}) for tenant ${tenant} (using ${authType})...`);\n\n // Show arguments being used\n if (Object.keys(appArgs).length > 0) {\n logger.dim('Arguments:');\n for (const [key, value] of Object.entries(appArgs)) {\n logger.dim(` ${key}: ${value}`);\n }\n }\n console.log();\n\n // Build environment variables based on auth type\n const envVars: Record<string, string> = {\n ...process.env as Record<string, string>,\n TENANT_ID: credentials.tenantId,\n FARSEER_URL: credentials.basePath,\n };\n\n if (credentials.apiKey) {\n envVars.FARSEER_API_KEY = credentials.apiKey;\n }\n if (credentials.accessToken) {\n envVars.FARSEER_ACCESS_TOKEN = credentials.accessToken;\n }\n\n // Build command line arguments from app arguments (in order)\n const cmdArgs: string[] = [];\n for (const arg of app.expectedArguments) {\n cmdArgs.push(appArgs[arg.name] ?? arg.defaultValue);\n }\n\n // Run with credentials injected via environment variables, arguments via command line\n const child = spawn('npx', ['tsx', `files/Scripts/${scriptName}`, ...cmdArgs], {\n stdio: 'inherit',\n cwd: tenantDir,\n shell: true,\n env: envVars,\n });\n\n child.on('close', (code) => {\n if (code !== 0) {\n logger.error(`Script exited with code ${code}`);\n process.exit(code || 1);\n }\n });\n\n child.on('error', (err) => {\n logger.error(`Failed to run script: ${err.message}`);\n logger.dim('Make sure tsx is installed: npm install -g tsx');\n process.exit(1);\n });\n });\n\n // Keep the old script-based run as a separate command\n program\n .command('run-script <args...>')\n .description('Run a script directly (without app wrapper)')\n .option('--tenant-id <id>', 'Tenant ID for env variables (defaults to tenant name)')\n .action(async (args: string[], options) => {\n let { tenant, appName: script } = parseAppArgs(args);\n const credentials = await getRunCredentials(tenant, options.tenantId);\n\n if (!credentials) {\n showLoginPrompt(tenant);\n process.exit(1);\n }\n\n // Normalize script path - strip Scripts/ prefix if present\n if (script.toLowerCase().startsWith('scripts/')) {\n script = script.substring('scripts/'.length);\n }\n\n const tenantDir = getTenantDir(tenant);\n const srcDir = getTenantSrcDir(tenant);\n\n // Script path is relative to files/Scripts/ folder\n const scriptPath = path.resolve(srcDir, script);\n\n if (!fs.existsSync(scriptPath)) {\n logger.error(`Script not found: ${scriptPath}`);\n process.exit(1);\n }\n\n // Check if node_modules exists\n const nodeModulesPath = path.join(tenantDir, 'node_modules');\n if (!fs.existsSync(nodeModulesPath)) {\n logger.error(`Dependencies not installed for ${tenant}`);\n logger.dim(`Run: farseer install ${tenant}`);\n process.exit(1);\n }\n\n const authType = credentials.apiKey ? 'API key' : 'access token';\n logger.info(`Running ${script} for tenant ${tenant} (using ${authType})...`);\n console.log();\n\n // Build environment variables based on auth type\n const envVars: Record<string, string> = {\n ...process.env as Record<string, string>,\n TENANT_ID: credentials.tenantId,\n FARSEER_URL: credentials.basePath,\n };\n\n if (credentials.apiKey) {\n envVars.FARSEER_API_KEY = credentials.apiKey;\n }\n if (credentials.accessToken) {\n envVars.FARSEER_ACCESS_TOKEN = credentials.accessToken;\n }\n\n // Run with credentials injected via environment variables\n const child = spawn('npx', ['tsx', `files/Scripts/${script}`], {\n stdio: 'inherit',\n cwd: tenantDir,\n shell: true,\n env: envVars,\n });\n\n child.on('close', (code) => {\n if (code !== 0) {\n logger.error(`Script exited with code ${code}`);\n process.exit(code || 1);\n }\n });\n\n child.on('error', (err) => {\n logger.error(`Failed to run script: ${err.message}`);\n logger.dim('Make sure tsx is installed: npm install -g tsx');\n process.exit(1);\n });\n });\n}\n"],
|
|
5
|
-
"mappings": ";;;;;;;;;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AACA,oBAAuB;AACvB,2BAA2F;AAC3F,4BAAmC;AACnC,4BAA8E;AAC9E,
|
|
4
|
+
"sourcesContent": ["import { Command } from 'commander';\nimport { logger } from '../utils/logger';\nimport { getCredential, getUserAuth, isUserAuthValid, setUserAuth, generateBasePath } from '../services/configService';\nimport { refreshAccessToken } from '../services/farseerService';\nimport { getFarseerClientWithFallback, showLoginPrompt, IFarseerClient } from '../services/farseerFactory';\nimport { getTenantDir, getTenantSrcDir, getTenantFilesDir, parseAppArgs } from '../utils/helpers';\nimport * as fs from 'fs';\nimport * as path from 'path';\nimport { spawn } from 'child_process';\n\n/**\n * Find a script file recursively in a directory.\n * Returns the relative path from baseDir if found, null otherwise.\n */\nfunction findScriptRecursively(baseDir: string, scriptName: string): string | null {\n if (!fs.existsSync(baseDir)) {\n return null;\n }\n\n const search = (dir: string, relativePath: string = ''): string | null => {\n const items = fs.readdirSync(dir, { withFileTypes: true });\n\n for (const item of items) {\n const itemRelPath = relativePath ? `${relativePath}/${item.name}` : item.name;\n\n if (item.isFile() && item.name === scriptName) {\n return itemRelPath;\n }\n\n if (item.isDirectory()) {\n const found = search(path.join(dir, item.name), itemRelPath);\n if (found) return found;\n }\n }\n\n return null;\n };\n\n return search(baseDir);\n}\n\ninterface RunCredentials {\n tenantId: string;\n apiKey?: string;\n accessToken?: string;\n basePath: string;\n}\n\nasync function getRunCredentials(tenant: string, tenantId?: string): Promise<RunCredentials | null> {\n // First try API key config\n const credential = getCredential(tenant);\n if (credential) {\n return {\n tenantId: credential.tenantId,\n apiKey: credential.apiKey,\n basePath: credential.basePath,\n };\n }\n\n // Try user auth (browser login)\n const auth = getUserAuth();\n if (!auth) {\n return null;\n }\n\n // Check if token needs refresh\n if (!isUserAuthValid()) {\n logger.dim('Token expired, refreshing...');\n const refreshed = await refreshAccessToken(auth.refreshToken, auth.realm || 'master');\n if (!refreshed) {\n logger.warning('Could not refresh token. Please login again with: farseer login');\n return null;\n }\n // Save refreshed token\n setUserAuth({\n accessToken: refreshed.accessToken,\n refreshToken: refreshed.refreshToken,\n expiresAt: new Date(Date.now() + refreshed.expiresIn * 1000).toISOString(),\n realm: auth.realm,\n });\n logger.success('Token refreshed');\n }\n\n const updatedAuth = getUserAuth();\n if (!updatedAuth) return null;\n\n // Use tenant name as organisation for basePath, tenantId for TENANT_ID env var\n return {\n tenantId: tenantId || tenant,\n accessToken: updatedAuth.accessToken,\n basePath: generateBasePath(tenant),\n };\n}\n\n// Helper to collect multiple --arg options\nfunction collectArgs(value: string, previous: string[]): string[] {\n return previous.concat([value]);\n}\n\nexport function registerRunCommand(program: Command): void {\n program\n .command('run <args...>')\n .description('Run an app locally with injected credentials')\n .option('--tenant-id <id>', 'Tenant ID for env variables (defaults to tenant name)')\n .option('--arg <value>', 'Pass argument in format \"Name=Value\" (can be used multiple times)', collectArgs, [])\n .action(async (args: string[], options) => {\n const { tenant, appName } = parseAppArgs(args);\n // First, get the app details to find the entrypoint script\n const clientResult = await getFarseerClientWithFallback(tenant, options.tenantId);\n\n if (!clientResult) {\n showLoginPrompt(tenant);\n process.exit(1);\n }\n\n const api: IFarseerClient = clientResult.client;\n\n // Find the app\n logger.info(`Loading app: ${appName}...`);\n const app = await api.getAppByName(appName);\n\n if (!app) {\n logger.error(`App not found: ${appName}`);\n logger.dim('List available apps with: farseer apps ' + tenant);\n process.exit(1);\n }\n\n if (!app.mainScriptFileId || app.scriptFiles.length === 0) {\n logger.error(`App \"${appName}\" has no scripts configured.`);\n logger.dim('Configure the app with: farseer app configure \"' + appName + '\" --entrypoint <script>');\n process.exit(1);\n }\n\n // Find the entrypoint script name\n const mainScript = app.scriptFiles.find(s => s.id === app.mainScriptFileId);\n if (!mainScript) {\n logger.error(`Entrypoint script not found for app \"${appName}\".`);\n process.exit(1);\n }\n\n // Normalize script name - handle both \"test.ts\" and \"Scripts/test.ts\" formats\n let scriptName = mainScript.name;\n if (scriptName.toLowerCase().startsWith('scripts/')) {\n scriptName = scriptName.substring('scripts/'.length);\n }\n\n // Get credentials for running\n const credentials = await getRunCredentials(tenant, options.tenantId);\n\n if (!credentials) {\n showLoginPrompt(tenant);\n process.exit(1);\n }\n\n const tenantDir = getTenantDir(tenant);\n const srcDir = getTenantSrcDir(tenant);\n const filesDir = getTenantFilesDir(tenant);\n\n // Script path is relative to files/Scripts/ folder\n // First try direct path, then search recursively if not found\n let scriptPath = path.resolve(srcDir, scriptName);\n let relativeScriptPath = `files/Scripts/${scriptName}`;\n\n if (!fs.existsSync(scriptPath)) {\n // Try to find the script recursively in the files directory\n const foundPath = findScriptRecursively(filesDir, path.basename(scriptName));\n if (foundPath) {\n scriptPath = path.resolve(filesDir, foundPath);\n relativeScriptPath = `files/${foundPath}`;\n logger.dim(`Found script at: ${foundPath}`);\n }\n }\n\n if (!fs.existsSync(scriptPath)) {\n logger.error(`Script not found locally: ${scriptPath}`);\n logger.dim('Run \"farseer pull ' + tenant + '\" to download scripts first.');\n process.exit(1);\n }\n\n // Check if node_modules exists\n const nodeModulesPath = path.join(tenantDir, 'node_modules');\n if (!fs.existsSync(nodeModulesPath)) {\n logger.error(`Dependencies not installed for ${tenant}`);\n logger.dim(`Run: farseer install ${tenant}`);\n process.exit(1);\n }\n\n // Parse arguments\n const appArgs: Record<string, string> = {};\n\n // Start with default values from app config\n for (const arg of app.expectedArguments) {\n appArgs[arg.name] = arg.defaultValue;\n }\n\n // Override with provided arguments\n for (const argStr of options.arg) {\n const eqIndex = argStr.indexOf('=');\n if (eqIndex === -1) {\n logger.error(`Invalid argument format: ${argStr}`);\n logger.dim('Use format: --arg \"Name=Value\"');\n process.exit(1);\n }\n const name = argStr.substring(0, eqIndex);\n const value = argStr.substring(eqIndex + 1);\n appArgs[name] = value;\n }\n\n const authType = credentials.apiKey ? 'API key' : 'access token';\n logger.info(`Running app \"${app.name}\" (${scriptName}) for tenant ${tenant} (using ${authType})...`);\n\n // Show arguments being used\n if (Object.keys(appArgs).length > 0) {\n logger.dim('Arguments:');\n for (const [key, value] of Object.entries(appArgs)) {\n logger.dim(` ${key}: ${value}`);\n }\n }\n console.log();\n\n // Build environment variables based on auth type\n const envVars: Record<string, string> = {\n ...process.env as Record<string, string>,\n TENANT_ID: credentials.tenantId,\n FARSEER_URL: credentials.basePath,\n };\n\n if (credentials.apiKey) {\n envVars.FARSEER_API_KEY = credentials.apiKey;\n }\n if (credentials.accessToken) {\n envVars.FARSEER_ACCESS_TOKEN = credentials.accessToken;\n }\n\n // Build command line arguments from app arguments (in order)\n const cmdArgs: string[] = [];\n for (const arg of app.expectedArguments) {\n cmdArgs.push(appArgs[arg.name] ?? arg.defaultValue);\n }\n\n // Run with credentials injected via environment variables, arguments via command line\n // Quote the script path to handle spaces in folder names\n const quotedScriptPath = `\"${relativeScriptPath}\"`;\n const child = spawn('npx', ['tsx', quotedScriptPath, ...cmdArgs], {\n stdio: 'inherit',\n cwd: tenantDir,\n shell: true,\n env: envVars,\n });\n\n child.on('close', (code) => {\n if (code !== 0) {\n logger.error(`Script exited with code ${code}`);\n process.exit(code || 1);\n }\n });\n\n child.on('error', (err) => {\n logger.error(`Failed to run script: ${err.message}`);\n logger.dim('Make sure tsx is installed: npm install -g tsx');\n process.exit(1);\n });\n });\n\n // Keep the old script-based run as a separate command\n program\n .command('run-script <args...>')\n .description('Run a script directly (without app wrapper)')\n .option('--tenant-id <id>', 'Tenant ID for env variables (defaults to tenant name)')\n .action(async (args: string[], options) => {\n let { tenant, appName: script } = parseAppArgs(args);\n const credentials = await getRunCredentials(tenant, options.tenantId);\n\n if (!credentials) {\n showLoginPrompt(tenant);\n process.exit(1);\n }\n\n // Normalize script path - strip Scripts/ prefix if present\n if (script.toLowerCase().startsWith('scripts/')) {\n script = script.substring('scripts/'.length);\n }\n\n const tenantDir = getTenantDir(tenant);\n const srcDir = getTenantSrcDir(tenant);\n const filesDir = getTenantFilesDir(tenant);\n\n // Script path is relative to files/Scripts/ folder\n // First try direct path, then search recursively if not found\n let scriptPath = path.resolve(srcDir, script);\n let relativeScriptPath = `files/Scripts/${script}`;\n\n if (!fs.existsSync(scriptPath)) {\n // Try to find the script recursively in the files directory\n const foundPath = findScriptRecursively(filesDir, path.basename(script));\n if (foundPath) {\n scriptPath = path.resolve(filesDir, foundPath);\n relativeScriptPath = `files/${foundPath}`;\n logger.dim(`Found script at: ${foundPath}`);\n }\n }\n\n if (!fs.existsSync(scriptPath)) {\n logger.error(`Script not found: ${scriptPath}`);\n process.exit(1);\n }\n\n // Check if node_modules exists\n const nodeModulesPath = path.join(tenantDir, 'node_modules');\n if (!fs.existsSync(nodeModulesPath)) {\n logger.error(`Dependencies not installed for ${tenant}`);\n logger.dim(`Run: farseer install ${tenant}`);\n process.exit(1);\n }\n\n const authType = credentials.apiKey ? 'API key' : 'access token';\n logger.info(`Running ${script} for tenant ${tenant} (using ${authType})...`);\n console.log();\n\n // Build environment variables based on auth type\n const envVars: Record<string, string> = {\n ...process.env as Record<string, string>,\n TENANT_ID: credentials.tenantId,\n FARSEER_URL: credentials.basePath,\n };\n\n if (credentials.apiKey) {\n envVars.FARSEER_API_KEY = credentials.apiKey;\n }\n if (credentials.accessToken) {\n envVars.FARSEER_ACCESS_TOKEN = credentials.accessToken;\n }\n\n // Run with credentials injected via environment variables\n // Quote the script path to handle spaces in folder names\n const quotedScriptPath = `\"${relativeScriptPath}\"`;\n const child = spawn('npx', ['tsx', quotedScriptPath], {\n stdio: 'inherit',\n cwd: tenantDir,\n shell: true,\n env: envVars,\n });\n\n child.on('close', (code) => {\n if (code !== 0) {\n logger.error(`Script exited with code ${code}`);\n process.exit(code || 1);\n }\n });\n\n child.on('error', (err) => {\n logger.error(`Failed to run script: ${err.message}`);\n logger.dim('Make sure tsx is installed: npm install -g tsx');\n process.exit(1);\n });\n });\n}\n"],
|
|
5
|
+
"mappings": ";;;;;;;;;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AACA,oBAAuB;AACvB,2BAA2F;AAC3F,4BAAmC;AACnC,4BAA8E;AAC9E,qBAA+E;AAC/E,SAAoB;AACpB,WAAsB;AACtB,2BAAsB;AAMtB,SAAS,sBAAsB,SAAiB,YAAmC;AAC/E,MAAI,CAAC,GAAG,WAAW,OAAO,GAAG;AACzB,WAAO;AAAA,EACX;AAEA,QAAM,SAAS,CAAC,KAAa,eAAuB,OAAsB;AACtE,UAAM,QAAQ,GAAG,YAAY,KAAK,EAAE,eAAe,KAAK,CAAC;AAEzD,eAAW,QAAQ,OAAO;AACtB,YAAM,cAAc,eAAe,GAAG,YAAY,IAAI,KAAK,IAAI,KAAK,KAAK;AAEzE,UAAI,KAAK,OAAO,KAAK,KAAK,SAAS,YAAY;AAC3C,eAAO;AAAA,MACX;AAEA,UAAI,KAAK,YAAY,GAAG;AACpB,cAAM,QAAQ,OAAO,KAAK,KAAK,KAAK,KAAK,IAAI,GAAG,WAAW;AAC3D,YAAI,MAAO,QAAO;AAAA,MACtB;AAAA,IACJ;AAEA,WAAO;AAAA,EACX;AAEA,SAAO,OAAO,OAAO;AACzB;AASA,eAAe,kBAAkB,QAAgB,UAAmD;AAEhG,QAAM,iBAAa,oCAAc,MAAM;AACvC,MAAI,YAAY;AACZ,WAAO;AAAA,MACH,UAAU,WAAW;AAAA,MACrB,QAAQ,WAAW;AAAA,MACnB,UAAU,WAAW;AAAA,IACzB;AAAA,EACJ;AAGA,QAAM,WAAO,kCAAY;AACzB,MAAI,CAAC,MAAM;AACP,WAAO;AAAA,EACX;AAGA,MAAI,KAAC,sCAAgB,GAAG;AACpB,yBAAO,IAAI,8BAA8B;AACzC,UAAM,YAAY,UAAM,0CAAmB,KAAK,cAAc,KAAK,SAAS,QAAQ;AACpF,QAAI,CAAC,WAAW;AACZ,2BAAO,QAAQ,iEAAiE;AAChF,aAAO;AAAA,IACX;AAEA,0CAAY;AAAA,MACR,aAAa,UAAU;AAAA,MACvB,cAAc,UAAU;AAAA,MACxB,WAAW,IAAI,KAAK,KAAK,IAAI,IAAI,UAAU,YAAY,GAAI,EAAE,YAAY;AAAA,MACzE,OAAO,KAAK;AAAA,IAChB,CAAC;AACD,yBAAO,QAAQ,iBAAiB;AAAA,EACpC;AAEA,QAAM,kBAAc,kCAAY;AAChC,MAAI,CAAC,YAAa,QAAO;AAGzB,SAAO;AAAA,IACH,UAAU,YAAY;AAAA,IACtB,aAAa,YAAY;AAAA,IACzB,cAAU,uCAAiB,MAAM;AAAA,EACrC;AACJ;AAGA,SAAS,YAAY,OAAe,UAA8B;AAC9D,SAAO,SAAS,OAAO,CAAC,KAAK,CAAC;AAClC;AAEO,SAAS,mBAAmB,SAAwB;AACvD,UACK,QAAQ,eAAe,EACvB,YAAY,8CAA8C,EAC1D,OAAO,oBAAoB,uDAAuD,EAClF,OAAO,iBAAiB,qEAAqE,aAAa,CAAC,CAAC,EAC5G,OAAO,OAAO,MAAgB,YAAY;AACvC,UAAM,EAAE,QAAQ,QAAQ,QAAI,6BAAa,IAAI;AAE7C,UAAM,eAAe,UAAM,oDAA6B,QAAQ,QAAQ,QAAQ;AAEhF,QAAI,CAAC,cAAc;AACf,iDAAgB,MAAM;AACtB,cAAQ,KAAK,CAAC;AAAA,IAClB;AAEA,UAAM,MAAsB,aAAa;AAGzC,yBAAO,KAAK,gBAAgB,OAAO,KAAK;AACxC,UAAM,MAAM,MAAM,IAAI,aAAa,OAAO;AAE1C,QAAI,CAAC,KAAK;AACN,2BAAO,MAAM,kBAAkB,OAAO,EAAE;AACxC,2BAAO,IAAI,4CAA4C,MAAM;AAC7D,cAAQ,KAAK,CAAC;AAAA,IAClB;AAEA,QAAI,CAAC,IAAI,oBAAoB,IAAI,YAAY,WAAW,GAAG;AACvD,2BAAO,MAAM,QAAQ,OAAO,8BAA8B;AAC1D,2BAAO,IAAI,oDAAoD,UAAU,yBAAyB;AAClG,cAAQ,KAAK,CAAC;AAAA,IAClB;AAGA,UAAM,aAAa,IAAI,YAAY,KAAK,OAAK,EAAE,OAAO,IAAI,gBAAgB;AAC1E,QAAI,CAAC,YAAY;AACb,2BAAO,MAAM,wCAAwC,OAAO,IAAI;AAChE,cAAQ,KAAK,CAAC;AAAA,IAClB;AAGA,QAAI,aAAa,WAAW;AAC5B,QAAI,WAAW,YAAY,EAAE,WAAW,UAAU,GAAG;AACjD,mBAAa,WAAW,UAAU,WAAW,MAAM;AAAA,IACvD;AAGA,UAAM,cAAc,MAAM,kBAAkB,QAAQ,QAAQ,QAAQ;AAEpE,QAAI,CAAC,aAAa;AACd,iDAAgB,MAAM;AACtB,cAAQ,KAAK,CAAC;AAAA,IAClB;AAEA,UAAM,gBAAY,6BAAa,MAAM;AACrC,UAAM,aAAS,gCAAgB,MAAM;AACrC,UAAM,eAAW,kCAAkB,MAAM;AAIzC,QAAI,aAAa,KAAK,QAAQ,QAAQ,UAAU;AAChD,QAAI,qBAAqB,iBAAiB,UAAU;AAEpD,QAAI,CAAC,GAAG,WAAW,UAAU,GAAG;AAE5B,YAAM,YAAY,sBAAsB,UAAU,KAAK,SAAS,UAAU,CAAC;AAC3E,UAAI,WAAW;AACX,qBAAa,KAAK,QAAQ,UAAU,SAAS;AAC7C,6BAAqB,SAAS,SAAS;AACvC,6BAAO,IAAI,oBAAoB,SAAS,EAAE;AAAA,MAC9C;AAAA,IACJ;AAEA,QAAI,CAAC,GAAG,WAAW,UAAU,GAAG;AAC5B,2BAAO,MAAM,6BAA6B,UAAU,EAAE;AACtD,2BAAO,IAAI,uBAAuB,SAAS,8BAA8B;AACzE,cAAQ,KAAK,CAAC;AAAA,IAClB;AAGA,UAAM,kBAAkB,KAAK,KAAK,WAAW,cAAc;AAC3D,QAAI,CAAC,GAAG,WAAW,eAAe,GAAG;AACjC,2BAAO,MAAM,kCAAkC,MAAM,EAAE;AACvD,2BAAO,IAAI,wBAAwB,MAAM,EAAE;AAC3C,cAAQ,KAAK,CAAC;AAAA,IAClB;AAGA,UAAM,UAAkC,CAAC;AAGzC,eAAW,OAAO,IAAI,mBAAmB;AACrC,cAAQ,IAAI,IAAI,IAAI,IAAI;AAAA,IAC5B;AAGA,eAAW,UAAU,QAAQ,KAAK;AAC9B,YAAM,UAAU,OAAO,QAAQ,GAAG;AAClC,UAAI,YAAY,IAAI;AAChB,6BAAO,MAAM,4BAA4B,MAAM,EAAE;AACjD,6BAAO,IAAI,gCAAgC;AAC3C,gBAAQ,KAAK,CAAC;AAAA,MAClB;AACA,YAAM,OAAO,OAAO,UAAU,GAAG,OAAO;AACxC,YAAM,QAAQ,OAAO,UAAU,UAAU,CAAC;AAC1C,cAAQ,IAAI,IAAI;AAAA,IACpB;AAEA,UAAM,WAAW,YAAY,SAAS,YAAY;AAClD,yBAAO,KAAK,gBAAgB,IAAI,IAAI,MAAM,UAAU,gBAAgB,MAAM,WAAW,QAAQ,MAAM;AAGnG,QAAI,OAAO,KAAK,OAAO,EAAE,SAAS,GAAG;AACjC,2BAAO,IAAI,YAAY;AACvB,iBAAW,CAAC,KAAK,KAAK,KAAK,OAAO,QAAQ,OAAO,GAAG;AAChD,6BAAO,IAAI,KAAK,GAAG,KAAK,KAAK,EAAE;AAAA,MACnC;AAAA,IACJ;AACA,YAAQ,IAAI;AAGZ,UAAM,UAAkC;AAAA,MACpC,GAAG,QAAQ;AAAA,MACX,WAAW,YAAY;AAAA,MACvB,aAAa,YAAY;AAAA,IAC7B;AAEA,QAAI,YAAY,QAAQ;AACpB,cAAQ,kBAAkB,YAAY;AAAA,IAC1C;AACA,QAAI,YAAY,aAAa;AACzB,cAAQ,uBAAuB,YAAY;AAAA,IAC/C;AAGA,UAAM,UAAoB,CAAC;AAC3B,eAAW,OAAO,IAAI,mBAAmB;AACrC,cAAQ,KAAK,QAAQ,IAAI,IAAI,KAAK,IAAI,YAAY;AAAA,IACtD;AAIA,UAAM,mBAAmB,IAAI,kBAAkB;AAC/C,UAAM,YAAQ,4BAAM,OAAO,CAAC,OAAO,kBAAkB,GAAG,OAAO,GAAG;AAAA,MAC9D,OAAO;AAAA,MACP,KAAK;AAAA,MACL,OAAO;AAAA,MACP,KAAK;AAAA,IACT,CAAC;AAED,UAAM,GAAG,SAAS,CAAC,SAAS;AACxB,UAAI,SAAS,GAAG;AACZ,6BAAO,MAAM,2BAA2B,IAAI,EAAE;AAC9C,gBAAQ,KAAK,QAAQ,CAAC;AAAA,MAC1B;AAAA,IACJ,CAAC;AAED,UAAM,GAAG,SAAS,CAAC,QAAQ;AACvB,2BAAO,MAAM,yBAAyB,IAAI,OAAO,EAAE;AACnD,2BAAO,IAAI,gDAAgD;AAC3D,cAAQ,KAAK,CAAC;AAAA,IAClB,CAAC;AAAA,EACL,CAAC;AAGL,UACK,QAAQ,sBAAsB,EAC9B,YAAY,6CAA6C,EACzD,OAAO,oBAAoB,uDAAuD,EAClF,OAAO,OAAO,MAAgB,YAAY;AACvC,QAAI,EAAE,QAAQ,SAAS,OAAO,QAAI,6BAAa,IAAI;AACnD,UAAM,cAAc,MAAM,kBAAkB,QAAQ,QAAQ,QAAQ;AAEpE,QAAI,CAAC,aAAa;AACd,iDAAgB,MAAM;AACtB,cAAQ,KAAK,CAAC;AAAA,IAClB;AAGA,QAAI,OAAO,YAAY,EAAE,WAAW,UAAU,GAAG;AAC7C,eAAS,OAAO,UAAU,WAAW,MAAM;AAAA,IAC/C;AAEA,UAAM,gBAAY,6BAAa,MAAM;AACrC,UAAM,aAAS,gCAAgB,MAAM;AACrC,UAAM,eAAW,kCAAkB,MAAM;AAIzC,QAAI,aAAa,KAAK,QAAQ,QAAQ,MAAM;AAC5C,QAAI,qBAAqB,iBAAiB,MAAM;AAEhD,QAAI,CAAC,GAAG,WAAW,UAAU,GAAG;AAE5B,YAAM,YAAY,sBAAsB,UAAU,KAAK,SAAS,MAAM,CAAC;AACvE,UAAI,WAAW;AACX,qBAAa,KAAK,QAAQ,UAAU,SAAS;AAC7C,6BAAqB,SAAS,SAAS;AACvC,6BAAO,IAAI,oBAAoB,SAAS,EAAE;AAAA,MAC9C;AAAA,IACJ;AAEA,QAAI,CAAC,GAAG,WAAW,UAAU,GAAG;AAC5B,2BAAO,MAAM,qBAAqB,UAAU,EAAE;AAC9C,cAAQ,KAAK,CAAC;AAAA,IAClB;AAGA,UAAM,kBAAkB,KAAK,KAAK,WAAW,cAAc;AAC3D,QAAI,CAAC,GAAG,WAAW,eAAe,GAAG;AACjC,2BAAO,MAAM,kCAAkC,MAAM,EAAE;AACvD,2BAAO,IAAI,wBAAwB,MAAM,EAAE;AAC3C,cAAQ,KAAK,CAAC;AAAA,IAClB;AAEA,UAAM,WAAW,YAAY,SAAS,YAAY;AAClD,yBAAO,KAAK,WAAW,MAAM,eAAe,MAAM,WAAW,QAAQ,MAAM;AAC3E,YAAQ,IAAI;AAGZ,UAAM,UAAkC;AAAA,MACpC,GAAG,QAAQ;AAAA,MACX,WAAW,YAAY;AAAA,MACvB,aAAa,YAAY;AAAA,IAC7B;AAEA,QAAI,YAAY,QAAQ;AACpB,cAAQ,kBAAkB,YAAY;AAAA,IAC1C;AACA,QAAI,YAAY,aAAa;AACzB,cAAQ,uBAAuB,YAAY;AAAA,IAC/C;AAIA,UAAM,mBAAmB,IAAI,kBAAkB;AAC/C,UAAM,YAAQ,4BAAM,OAAO,CAAC,OAAO,gBAAgB,GAAG;AAAA,MAClD,OAAO;AAAA,MACP,KAAK;AAAA,MACL,OAAO;AAAA,MACP,KAAK;AAAA,IACT,CAAC;AAED,UAAM,GAAG,SAAS,CAAC,SAAS;AACxB,UAAI,SAAS,GAAG;AACZ,6BAAO,MAAM,2BAA2B,IAAI,EAAE;AAC9C,gBAAQ,KAAK,QAAQ,CAAC;AAAA,MAC1B;AAAA,IACJ,CAAC;AAED,UAAM,GAAG,SAAS,CAAC,QAAQ;AACvB,2BAAO,MAAM,yBAAyB,IAAI,OAAO,EAAE;AACnD,2BAAO,IAAI,gDAAgD;AAC3D,cAAQ,KAAK,CAAC;AAAA,IAClB,CAAC;AAAA,EACL,CAAC;AACT;",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -33,6 +33,7 @@ module.exports = __toCommonJS(appSyncService_exports);
|
|
|
33
33
|
var fs = __toESM(require("fs"));
|
|
34
34
|
var path = __toESM(require("path"));
|
|
35
35
|
var import_helpers = require("../utils/helpers");
|
|
36
|
+
var import_logger = require("../utils/logger");
|
|
36
37
|
function sanitizeDiacritics(text) {
|
|
37
38
|
const diacriticMap = {
|
|
38
39
|
// Croatian
|
|
@@ -195,10 +196,24 @@ class AppSyncService {
|
|
|
195
196
|
const localApps = this.loadLocalApps();
|
|
196
197
|
const remoteAppList = await this.api.listApps();
|
|
197
198
|
const remoteApps = /* @__PURE__ */ new Map();
|
|
198
|
-
|
|
199
|
-
|
|
199
|
+
import_logger.logger.dim(`Fetching ${remoteAppList.length} apps in parallel...`);
|
|
200
|
+
const appDetails = await (0, import_helpers.fetchWithConcurrency)(
|
|
201
|
+
remoteAppList,
|
|
202
|
+
async (item, index) => {
|
|
203
|
+
const app = await this.api.getApp(item.reference);
|
|
204
|
+
if ((index + 1) % 10 === 0 || index === remoteAppList.length - 1) {
|
|
205
|
+
process.stdout.write(`\r Progress: ${index + 1}/${remoteAppList.length} apps`);
|
|
206
|
+
}
|
|
207
|
+
return { app, reference: item.reference };
|
|
208
|
+
},
|
|
209
|
+
15
|
|
210
|
+
);
|
|
211
|
+
if (remoteAppList.length > 0) {
|
|
212
|
+
process.stdout.write("\r" + " ".repeat(50) + "\r");
|
|
213
|
+
}
|
|
214
|
+
for (const { app, reference } of appDetails) {
|
|
200
215
|
if (app) {
|
|
201
|
-
remoteApps.set(app.name.toLowerCase(), { app, reference
|
|
216
|
+
remoteApps.set(app.name.toLowerCase(), { app, reference });
|
|
202
217
|
}
|
|
203
218
|
}
|
|
204
219
|
for (const [key, { app, reference }] of remoteApps) {
|
|
@@ -235,10 +250,17 @@ class AppSyncService {
|
|
|
235
250
|
const localApps = this.loadLocalApps();
|
|
236
251
|
const remoteAppList = await this.api.listApps();
|
|
237
252
|
const remoteApps = /* @__PURE__ */ new Map();
|
|
238
|
-
|
|
239
|
-
|
|
253
|
+
const appDetails = await (0, import_helpers.fetchWithConcurrency)(
|
|
254
|
+
remoteAppList,
|
|
255
|
+
async (item) => {
|
|
256
|
+
const app = await this.api.getApp(item.reference);
|
|
257
|
+
return { app, reference: item.reference };
|
|
258
|
+
},
|
|
259
|
+
15
|
|
260
|
+
);
|
|
261
|
+
for (const { app, reference } of appDetails) {
|
|
240
262
|
if (app) {
|
|
241
|
-
remoteApps.set(app.name.toLowerCase(), { app, reference
|
|
263
|
+
remoteApps.set(app.name.toLowerCase(), { app, reference });
|
|
242
264
|
}
|
|
243
265
|
}
|
|
244
266
|
const availableScripts = await this.api.getScriptFiles();
|
|
@@ -339,10 +361,17 @@ class AppSyncService {
|
|
|
339
361
|
const localApps = this.loadLocalApps();
|
|
340
362
|
const remoteAppList = await this.api.listApps();
|
|
341
363
|
const remoteApps = /* @__PURE__ */ new Map();
|
|
342
|
-
|
|
343
|
-
|
|
364
|
+
const appDetails = await (0, import_helpers.fetchWithConcurrency)(
|
|
365
|
+
remoteAppList,
|
|
366
|
+
async (item) => {
|
|
367
|
+
const app = await this.api.getApp(item.reference);
|
|
368
|
+
return { app, reference: item.reference };
|
|
369
|
+
},
|
|
370
|
+
15
|
|
371
|
+
);
|
|
372
|
+
for (const { app, reference } of appDetails) {
|
|
344
373
|
if (app) {
|
|
345
|
-
remoteApps.set(app.name.toLowerCase(), { app, reference
|
|
374
|
+
remoteApps.set(app.name.toLowerCase(), { app, reference });
|
|
346
375
|
}
|
|
347
376
|
}
|
|
348
377
|
for (const [key, local] of localApps) {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../src/services/appSyncService.ts"],
|
|
4
|
-
"sourcesContent": ["import * as fs from 'fs';\nimport * as path from 'path';\nimport { RemoteApp, AppArgument } from './farseerApi';\nimport { IFarseerClient } from './farseerFactory';\nimport { getTenantAppsDir, ensureDirectoryExists } from '../utils/helpers';\n\n// Local app config format (stored in apps/*.json)\nexport interface LocalAppConfig {\n name: string;\n description: string;\n entrypoint: string | null; // Script filename (e.g., \"index.ts\")\n scripts: string[]; // List of script filenames\n arguments: {\n name: string;\n defaultValue: string;\n }[];\n _remote?: {\n id: number;\n reference: string;\n };\n}\n\nexport interface AppSyncResult {\n created: string[];\n updated: string[];\n deleted: string[];\n unchanged: string[];\n}\n\nexport interface AppSyncStatus {\n newLocal: string[]; // Local apps not on remote\n newRemote: string[]; // Remote apps not locally\n modified: string[]; // Apps that differ\n synced: string[]; // Apps in sync\n}\n\n/**\n * Sanitize Croatian diacritics and other special characters to ASCII equivalents\n */\nfunction sanitizeDiacritics(text: string): string {\n const diacriticMap: Record<string, string> = {\n // Croatian\n '\u010D': 'c', '\u010C': 'C',\n '\u0107': 'c', '\u0106': 'C',\n '\u0161': 's', '\u0160': 'S',\n '\u017E': 'z', '\u017D': 'Z',\n '\u0111': 'd', '\u0110': 'D',\n // German/Other common\n '\u00E4': 'a', '\u00C4': 'A',\n '\u00F6': 'o', '\u00D6': 'O',\n '\u00FC': 'u', '\u00DC': 'U',\n '\u00DF': 'ss',\n // Polish\n '\u0105': 'a', '\u0104': 'A',\n '\u0119': 'e', '\u0118': 'E',\n '\u0142': 'l', '\u0141': 'L',\n '\u0144': 'n', '\u0143': 'N',\n '\u00F3': 'o', '\u00D3': 'O',\n '\u015B': 's', '\u015A': 'S',\n '\u017A': 'z', '\u0179': 'Z',\n '\u017C': 'z', '\u017B': 'Z',\n // French/Spanish\n '\u00E0': 'a', '\u00E1': 'a', '\u00E2': 'a',\n '\u00E8': 'e', '\u00E9': 'e', '\u00EA': 'e', '\u00EB': 'e',\n '\u00EC': 'i', '\u00ED': 'i', '\u00EE': 'i', '\u00EF': 'i',\n '\u00F2': 'o', '\u00F4': 'o',\n '\u00F9': 'u', '\u00FA': 'u', '\u00FB': 'u',\n '\u00F1': 'n', '\u00D1': 'N',\n '\u00E7': 'c', '\u00C7': 'C',\n };\n\n return text.split('').map(char => diacriticMap[char] || char).join('');\n}\n\n/**\n * Service for syncing app configurations between local JSON files and remote Farseer instance\n */\nexport class AppSyncService {\n private api: IFarseerClient;\n private appsDir: string;\n\n constructor(tenant: string, api: IFarseerClient) {\n this.api = api;\n this.appsDir = getTenantAppsDir(tenant);\n }\n\n /**\n * Get filename for an app (keep original name, just sanitize for filesystem)\n */\n private getAppFilename(name: string): string {\n return sanitizeDiacritics(name)\n .replace(/[<>:\"/\\\\|?*]/g, '') // Remove invalid filename characters\n .trim() + '.json';\n }\n\n /**\n * Load all local app configs\n */\n loadLocalApps(): Map<string, LocalAppConfig> {\n const apps = new Map<string, LocalAppConfig>();\n\n if (!fs.existsSync(this.appsDir)) {\n return apps;\n }\n\n const files = fs.readdirSync(this.appsDir).filter(f => f.endsWith('.json'));\n\n for (const file of files) {\n try {\n const content = fs.readFileSync(path.join(this.appsDir, file), 'utf-8');\n const config = JSON.parse(content) as LocalAppConfig;\n apps.set(config.name.toLowerCase(), config);\n } catch {\n // Skip invalid files\n }\n }\n\n return apps;\n }\n\n /**\n * Save a local app config\n */\n saveLocalApp(config: LocalAppConfig): void {\n ensureDirectoryExists(this.appsDir);\n const filename = this.getAppFilename(config.name);\n const filePath = path.join(this.appsDir, filename);\n fs.writeFileSync(filePath, JSON.stringify(config, null, 2), 'utf-8');\n }\n\n /**\n * Delete a local app config\n */\n deleteLocalApp(name: string): boolean {\n const filename = this.getAppFilename(name);\n const filePath = path.join(this.appsDir, filename);\n\n if (fs.existsSync(filePath)) {\n fs.unlinkSync(filePath);\n return true;\n }\n return false;\n }\n\n /**\n * Convert remote app to local config format\n */\n private remoteToLocal(remote: RemoteApp, reference: string): LocalAppConfig {\n const mainScript = remote.scriptFiles.find(s => s.id === remote.mainScriptFileId);\n\n return {\n name: remote.name,\n description: remote.description || '',\n entrypoint: mainScript?.name || null,\n scripts: remote.scriptFiles.map(s => s.name),\n arguments: remote.expectedArguments.map(a => ({\n name: a.name,\n defaultValue: a.defaultValue,\n })),\n _remote: {\n id: remote.id,\n reference: reference,\n },\n };\n }\n\n /**\n * Check if local and remote app configs are equivalent\n */\n private areAppsEqual(local: LocalAppConfig, remote: RemoteApp): boolean {\n // Compare name\n if (local.name !== remote.name) return false;\n\n // Compare description\n if ((local.description || '') !== (remote.description || '')) return false;\n\n // Compare entrypoint\n const remoteEntrypoint = remote.scriptFiles.find(s => s.id === remote.mainScriptFileId)?.name || null;\n if (local.entrypoint !== remoteEntrypoint) return false;\n\n // Compare scripts\n const localScripts = [...local.scripts].sort();\n const remoteScripts = [...remote.scriptFiles.map(s => s.name)].sort();\n if (JSON.stringify(localScripts) !== JSON.stringify(remoteScripts)) return false;\n\n // Compare arguments\n const localArgs = local.arguments.map(a => `${a.name}=${a.defaultValue}`).sort();\n const remoteArgs = remote.expectedArguments.map(a => `${a.name}=${a.defaultValue}`).sort();\n if (JSON.stringify(localArgs) !== JSON.stringify(remoteArgs)) return false;\n\n return true;\n }\n\n /**\n * Pull apps from remote and save locally\n */\n async pull(): Promise<AppSyncResult> {\n const result: AppSyncResult = {\n created: [],\n updated: [],\n deleted: [],\n unchanged: [],\n };\n\n // Load local apps\n const localApps = this.loadLocalApps();\n\n // Fetch remote apps\n const remoteAppList = await this.api.listApps();\n const remoteApps = new Map<string, { app: RemoteApp; reference: string }>();\n\n for (const item of remoteAppList) {\n const app = await this.api.getApp(item.reference);\n if (app) {\n remoteApps.set(app.name.toLowerCase(), { app, reference: item.reference });\n }\n }\n\n // Process remote apps\n for (const [key, { app, reference }] of remoteApps) {\n const localConfig = this.remoteToLocal(app, reference);\n const existingLocal = localApps.get(key);\n\n if (!existingLocal) {\n // New app from remote\n this.saveLocalApp(localConfig);\n result.created.push(app.name);\n } else if (!this.areAppsEqual(existingLocal, app)) {\n // App changed on remote - update local\n this.saveLocalApp(localConfig);\n result.updated.push(app.name);\n } else {\n result.unchanged.push(app.name);\n }\n }\n\n // Check for apps deleted on remote\n for (const [key, local] of localApps) {\n if (!remoteApps.has(key) && local._remote) {\n // App was synced before but now deleted on remote\n this.deleteLocalApp(local.name);\n result.deleted.push(local.name);\n }\n }\n\n return result;\n }\n\n /**\n * Push local apps to remote\n */\n async push(): Promise<AppSyncResult> {\n const result: AppSyncResult = {\n created: [],\n updated: [],\n deleted: [],\n unchanged: [],\n };\n\n // Load local apps\n const localApps = this.loadLocalApps();\n\n // Fetch remote apps\n const remoteAppList = await this.api.listApps();\n const remoteApps = new Map<string, { app: RemoteApp; reference: string }>();\n\n for (const item of remoteAppList) {\n const app = await this.api.getApp(item.reference);\n if (app) {\n remoteApps.set(app.name.toLowerCase(), { app, reference: item.reference });\n }\n }\n\n // Get available scripts for ID lookup\n // Build maps for both path-based and filename-based lookup (backward compatibility)\n const availableScripts = await this.api.getScriptFiles();\n const scriptIdByPath = new Map<string, string>();\n const scriptIdByName = new Map<string, string>();\n\n for (const s of availableScripts) {\n // s.name now contains full path (e.g., \"Scripts/test.ts\")\n const pathKey = s.name.toLowerCase();\n const filenameKey = s.name.split('/').pop()?.toLowerCase() || pathKey;\n\n scriptIdByPath.set(pathKey, s.id);\n // For filename lookup, only set if not already set (first match wins for ambiguous names)\n if (!scriptIdByName.has(filenameKey)) {\n scriptIdByName.set(filenameKey, s.id);\n }\n }\n\n // Helper to find script ID by path or filename\n const findScriptId = (scriptRef: string): string | undefined => {\n const key = scriptRef.toLowerCase();\n // Try exact path match first\n const byPath = scriptIdByPath.get(key);\n if (byPath) return byPath;\n // Try with Scripts/ prefix\n const withPrefix = scriptIdByPath.get(`scripts/${key}`);\n if (withPrefix) return withPrefix;\n // Fall back to filename match\n return scriptIdByName.get(key.split('/').pop() || key);\n };\n\n // Process local apps\n for (const [key, local] of localApps) {\n const remote = remoteApps.get(key);\n\n if (!remote) {\n // New local app - create on remote\n try {\n const created = await this.api.createApp(local.name);\n\n // Update with config (API requires name and description)\n const update = this.buildUpdatePayload(local, findScriptId);\n update.name = local.name;\n update.description = local.description;\n await this.api.updateApp(created.id.toString(), update);\n\n // Update local with remote ID\n local._remote = {\n id: created.id,\n reference: created.id.toString(),\n };\n this.saveLocalApp(local);\n\n result.created.push(local.name);\n } catch (error) {\n console.error(`Failed to create app ${local.name}:`, error);\n }\n } else if (!this.areAppsEqual(local, remote.app)) {\n // App differs - update remote\n try {\n const update = this.buildUpdatePayload(local, findScriptId);\n update.name = local.name;\n update.description = local.description;\n\n await this.api.updateApp(remote.reference, update);\n result.updated.push(local.name);\n } catch (error) {\n console.error(`Failed to update app ${local.name}:`, error);\n }\n } else {\n result.unchanged.push(local.name);\n }\n }\n\n // Note: We don't auto-delete remote apps when deleted locally\n // User must use \"farseer app delete\" explicitly\n\n return result;\n }\n\n /**\n * Build update payload for API\n */\n private buildUpdatePayload(\n local: LocalAppConfig,\n findScriptId: (scriptRef: string) => string | undefined\n ): {\n name?: string;\n description?: string;\n expectedArguments?: AppArgument[];\n mainScriptFileId?: string | null;\n scriptFileIds?: string[];\n } {\n const update: {\n name?: string;\n description?: string;\n expectedArguments?: AppArgument[];\n mainScriptFileId?: string | null;\n scriptFileIds?: string[];\n } = {};\n\n // Arguments\n if (local.arguments.length > 0) {\n update.expectedArguments = local.arguments.map(a => ({\n name: a.name,\n type: 'variable' as const,\n defaultValue: a.defaultValue,\n }));\n }\n\n // Scripts - lookup by path or filename\n if (local.scripts.length > 0) {\n const scriptIds: string[] = [];\n for (const scriptRef of local.scripts) {\n const id = findScriptId(scriptRef);\n if (id) {\n scriptIds.push(id);\n }\n }\n if (scriptIds.length > 0) {\n update.scriptFileIds = scriptIds;\n }\n }\n\n // Entrypoint - lookup by path or filename\n if (local.entrypoint) {\n const entrypointId = findScriptId(local.entrypoint);\n if (entrypointId) {\n update.mainScriptFileId = entrypointId;\n }\n }\n\n return update;\n }\n\n /**\n * Get sync status without making changes\n */\n async getStatus(): Promise<AppSyncStatus> {\n const status: AppSyncStatus = {\n newLocal: [],\n newRemote: [],\n modified: [],\n synced: [],\n };\n\n // Load local apps\n const localApps = this.loadLocalApps();\n\n // Fetch remote apps\n const remoteAppList = await this.api.listApps();\n const remoteApps = new Map<string, { app: RemoteApp; reference: string }>();\n\n for (const item of remoteAppList) {\n const app = await this.api.getApp(item.reference);\n if (app) {\n remoteApps.set(app.name.toLowerCase(), { app, reference: item.reference });\n }\n }\n\n // Check local apps\n for (const [key, local] of localApps) {\n const remote = remoteApps.get(key);\n\n if (!remote) {\n status.newLocal.push(local.name);\n } else if (!this.areAppsEqual(local, remote.app)) {\n status.modified.push(local.name);\n } else {\n status.synced.push(local.name);\n }\n }\n\n // Check for new remote apps\n for (const [key, { app }] of remoteApps) {\n if (!localApps.has(key)) {\n status.newRemote.push(app.name);\n }\n }\n\n return status;\n }\n}\n"],
|
|
5
|
-
"mappings": ";;;;;;;;;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,SAAoB;AACpB,WAAsB;AAGtB,
|
|
4
|
+
"sourcesContent": ["import * as fs from 'fs';\nimport * as path from 'path';\nimport { RemoteApp, AppArgument } from './farseerApi';\nimport { IFarseerClient } from './farseerFactory';\nimport { getTenantAppsDir, ensureDirectoryExists, fetchWithConcurrency } from '../utils/helpers';\nimport { logger } from '../utils/logger';\n\n// Local app config format (stored in apps/*.json)\nexport interface LocalAppConfig {\n name: string;\n description: string;\n entrypoint: string | null; // Script filename (e.g., \"index.ts\")\n scripts: string[]; // List of script filenames\n arguments: {\n name: string;\n defaultValue: string;\n }[];\n _remote?: {\n id: number;\n reference: string;\n };\n}\n\nexport interface AppSyncResult {\n created: string[];\n updated: string[];\n deleted: string[];\n unchanged: string[];\n}\n\nexport interface AppSyncStatus {\n newLocal: string[]; // Local apps not on remote\n newRemote: string[]; // Remote apps not locally\n modified: string[]; // Apps that differ\n synced: string[]; // Apps in sync\n}\n\n/**\n * Sanitize Croatian diacritics and other special characters to ASCII equivalents\n */\nfunction sanitizeDiacritics(text: string): string {\n const diacriticMap: Record<string, string> = {\n // Croatian\n '\u010D': 'c', '\u010C': 'C',\n '\u0107': 'c', '\u0106': 'C',\n '\u0161': 's', '\u0160': 'S',\n '\u017E': 'z', '\u017D': 'Z',\n '\u0111': 'd', '\u0110': 'D',\n // German/Other common\n '\u00E4': 'a', '\u00C4': 'A',\n '\u00F6': 'o', '\u00D6': 'O',\n '\u00FC': 'u', '\u00DC': 'U',\n '\u00DF': 'ss',\n // Polish\n '\u0105': 'a', '\u0104': 'A',\n '\u0119': 'e', '\u0118': 'E',\n '\u0142': 'l', '\u0141': 'L',\n '\u0144': 'n', '\u0143': 'N',\n '\u00F3': 'o', '\u00D3': 'O',\n '\u015B': 's', '\u015A': 'S',\n '\u017A': 'z', '\u0179': 'Z',\n '\u017C': 'z', '\u017B': 'Z',\n // French/Spanish\n '\u00E0': 'a', '\u00E1': 'a', '\u00E2': 'a',\n '\u00E8': 'e', '\u00E9': 'e', '\u00EA': 'e', '\u00EB': 'e',\n '\u00EC': 'i', '\u00ED': 'i', '\u00EE': 'i', '\u00EF': 'i',\n '\u00F2': 'o', '\u00F4': 'o',\n '\u00F9': 'u', '\u00FA': 'u', '\u00FB': 'u',\n '\u00F1': 'n', '\u00D1': 'N',\n '\u00E7': 'c', '\u00C7': 'C',\n };\n\n return text.split('').map(char => diacriticMap[char] || char).join('');\n}\n\n/**\n * Service for syncing app configurations between local JSON files and remote Farseer instance\n */\nexport class AppSyncService {\n private api: IFarseerClient;\n private appsDir: string;\n\n constructor(tenant: string, api: IFarseerClient) {\n this.api = api;\n this.appsDir = getTenantAppsDir(tenant);\n }\n\n /**\n * Get filename for an app (keep original name, just sanitize for filesystem)\n */\n private getAppFilename(name: string): string {\n return sanitizeDiacritics(name)\n .replace(/[<>:\"/\\\\|?*]/g, '') // Remove invalid filename characters\n .trim() + '.json';\n }\n\n /**\n * Load all local app configs\n */\n loadLocalApps(): Map<string, LocalAppConfig> {\n const apps = new Map<string, LocalAppConfig>();\n\n if (!fs.existsSync(this.appsDir)) {\n return apps;\n }\n\n const files = fs.readdirSync(this.appsDir).filter(f => f.endsWith('.json'));\n\n for (const file of files) {\n try {\n const content = fs.readFileSync(path.join(this.appsDir, file), 'utf-8');\n const config = JSON.parse(content) as LocalAppConfig;\n apps.set(config.name.toLowerCase(), config);\n } catch {\n // Skip invalid files\n }\n }\n\n return apps;\n }\n\n /**\n * Save a local app config\n */\n saveLocalApp(config: LocalAppConfig): void {\n ensureDirectoryExists(this.appsDir);\n const filename = this.getAppFilename(config.name);\n const filePath = path.join(this.appsDir, filename);\n fs.writeFileSync(filePath, JSON.stringify(config, null, 2), 'utf-8');\n }\n\n /**\n * Delete a local app config\n */\n deleteLocalApp(name: string): boolean {\n const filename = this.getAppFilename(name);\n const filePath = path.join(this.appsDir, filename);\n\n if (fs.existsSync(filePath)) {\n fs.unlinkSync(filePath);\n return true;\n }\n return false;\n }\n\n /**\n * Convert remote app to local config format\n */\n private remoteToLocal(remote: RemoteApp, reference: string): LocalAppConfig {\n const mainScript = remote.scriptFiles.find(s => s.id === remote.mainScriptFileId);\n\n return {\n name: remote.name,\n description: remote.description || '',\n entrypoint: mainScript?.name || null,\n scripts: remote.scriptFiles.map(s => s.name),\n arguments: remote.expectedArguments.map(a => ({\n name: a.name,\n defaultValue: a.defaultValue,\n })),\n _remote: {\n id: remote.id,\n reference: reference,\n },\n };\n }\n\n /**\n * Check if local and remote app configs are equivalent\n */\n private areAppsEqual(local: LocalAppConfig, remote: RemoteApp): boolean {\n // Compare name\n if (local.name !== remote.name) return false;\n\n // Compare description\n if ((local.description || '') !== (remote.description || '')) return false;\n\n // Compare entrypoint\n const remoteEntrypoint = remote.scriptFiles.find(s => s.id === remote.mainScriptFileId)?.name || null;\n if (local.entrypoint !== remoteEntrypoint) return false;\n\n // Compare scripts\n const localScripts = [...local.scripts].sort();\n const remoteScripts = [...remote.scriptFiles.map(s => s.name)].sort();\n if (JSON.stringify(localScripts) !== JSON.stringify(remoteScripts)) return false;\n\n // Compare arguments\n const localArgs = local.arguments.map(a => `${a.name}=${a.defaultValue}`).sort();\n const remoteArgs = remote.expectedArguments.map(a => `${a.name}=${a.defaultValue}`).sort();\n if (JSON.stringify(localArgs) !== JSON.stringify(remoteArgs)) return false;\n\n return true;\n }\n\n /**\n * Pull apps from remote and save locally\n */\n async pull(): Promise<AppSyncResult> {\n const result: AppSyncResult = {\n created: [],\n updated: [],\n deleted: [],\n unchanged: [],\n };\n\n // Load local apps\n const localApps = this.loadLocalApps();\n\n // Fetch remote apps\n const remoteAppList = await this.api.listApps();\n const remoteApps = new Map<string, { app: RemoteApp; reference: string }>();\n\n // Fetch app details in parallel with concurrency limit\n logger.dim(`Fetching ${remoteAppList.length} apps in parallel...`);\n const appDetails = await fetchWithConcurrency(\n remoteAppList,\n async (item, index) => {\n const app = await this.api.getApp(item.reference);\n // Show progress every 10 apps\n if ((index + 1) % 10 === 0 || index === remoteAppList.length - 1) {\n process.stdout.write(`\\r Progress: ${index + 1}/${remoteAppList.length} apps`);\n }\n return { app, reference: item.reference };\n },\n 15\n );\n // Clear progress line\n if (remoteAppList.length > 0) {\n process.stdout.write('\\r' + ' '.repeat(50) + '\\r');\n }\n\n for (const { app, reference } of appDetails) {\n if (app) {\n remoteApps.set(app.name.toLowerCase(), { app, reference });\n }\n }\n\n // Process remote apps\n for (const [key, { app, reference }] of remoteApps) {\n const localConfig = this.remoteToLocal(app, reference);\n const existingLocal = localApps.get(key);\n\n if (!existingLocal) {\n // New app from remote\n this.saveLocalApp(localConfig);\n result.created.push(app.name);\n } else if (!this.areAppsEqual(existingLocal, app)) {\n // App changed on remote - update local\n this.saveLocalApp(localConfig);\n result.updated.push(app.name);\n } else {\n result.unchanged.push(app.name);\n }\n }\n\n // Check for apps deleted on remote\n for (const [key, local] of localApps) {\n if (!remoteApps.has(key) && local._remote) {\n // App was synced before but now deleted on remote\n this.deleteLocalApp(local.name);\n result.deleted.push(local.name);\n }\n }\n\n return result;\n }\n\n /**\n * Push local apps to remote\n */\n async push(): Promise<AppSyncResult> {\n const result: AppSyncResult = {\n created: [],\n updated: [],\n deleted: [],\n unchanged: [],\n };\n\n // Load local apps\n const localApps = this.loadLocalApps();\n\n // Fetch remote apps\n const remoteAppList = await this.api.listApps();\n const remoteApps = new Map<string, { app: RemoteApp; reference: string }>();\n\n // Fetch app details in parallel with concurrency limit\n const appDetails = await fetchWithConcurrency(\n remoteAppList,\n async (item) => {\n const app = await this.api.getApp(item.reference);\n return { app, reference: item.reference };\n },\n 15\n );\n\n for (const { app, reference } of appDetails) {\n if (app) {\n remoteApps.set(app.name.toLowerCase(), { app, reference });\n }\n }\n\n // Get available scripts for ID lookup\n // Build maps for both path-based and filename-based lookup (backward compatibility)\n const availableScripts = await this.api.getScriptFiles();\n const scriptIdByPath = new Map<string, string>();\n const scriptIdByName = new Map<string, string>();\n\n for (const s of availableScripts) {\n // s.name now contains full path (e.g., \"Scripts/test.ts\")\n const pathKey = s.name.toLowerCase();\n const filenameKey = s.name.split('/').pop()?.toLowerCase() || pathKey;\n\n scriptIdByPath.set(pathKey, s.id);\n // For filename lookup, only set if not already set (first match wins for ambiguous names)\n if (!scriptIdByName.has(filenameKey)) {\n scriptIdByName.set(filenameKey, s.id);\n }\n }\n\n // Helper to find script ID by path or filename\n const findScriptId = (scriptRef: string): string | undefined => {\n const key = scriptRef.toLowerCase();\n // Try exact path match first\n const byPath = scriptIdByPath.get(key);\n if (byPath) return byPath;\n // Try with Scripts/ prefix\n const withPrefix = scriptIdByPath.get(`scripts/${key}`);\n if (withPrefix) return withPrefix;\n // Fall back to filename match\n return scriptIdByName.get(key.split('/').pop() || key);\n };\n\n // Process local apps\n for (const [key, local] of localApps) {\n const remote = remoteApps.get(key);\n\n if (!remote) {\n // New local app - create on remote\n try {\n const created = await this.api.createApp(local.name);\n\n // Update with config (API requires name and description)\n const update = this.buildUpdatePayload(local, findScriptId);\n update.name = local.name;\n update.description = local.description;\n await this.api.updateApp(created.id.toString(), update);\n\n // Update local with remote ID\n local._remote = {\n id: created.id,\n reference: created.id.toString(),\n };\n this.saveLocalApp(local);\n\n result.created.push(local.name);\n } catch (error) {\n console.error(`Failed to create app ${local.name}:`, error);\n }\n } else if (!this.areAppsEqual(local, remote.app)) {\n // App differs - update remote\n try {\n const update = this.buildUpdatePayload(local, findScriptId);\n update.name = local.name;\n update.description = local.description;\n\n await this.api.updateApp(remote.reference, update);\n result.updated.push(local.name);\n } catch (error) {\n console.error(`Failed to update app ${local.name}:`, error);\n }\n } else {\n result.unchanged.push(local.name);\n }\n }\n\n // Note: We don't auto-delete remote apps when deleted locally\n // User must use \"farseer app delete\" explicitly\n\n return result;\n }\n\n /**\n * Build update payload for API\n */\n private buildUpdatePayload(\n local: LocalAppConfig,\n findScriptId: (scriptRef: string) => string | undefined\n ): {\n name?: string;\n description?: string;\n expectedArguments?: AppArgument[];\n mainScriptFileId?: string | null;\n scriptFileIds?: string[];\n } {\n const update: {\n name?: string;\n description?: string;\n expectedArguments?: AppArgument[];\n mainScriptFileId?: string | null;\n scriptFileIds?: string[];\n } = {};\n\n // Arguments\n if (local.arguments.length > 0) {\n update.expectedArguments = local.arguments.map(a => ({\n name: a.name,\n type: 'variable' as const,\n defaultValue: a.defaultValue,\n }));\n }\n\n // Scripts - lookup by path or filename\n if (local.scripts.length > 0) {\n const scriptIds: string[] = [];\n for (const scriptRef of local.scripts) {\n const id = findScriptId(scriptRef);\n if (id) {\n scriptIds.push(id);\n }\n }\n if (scriptIds.length > 0) {\n update.scriptFileIds = scriptIds;\n }\n }\n\n // Entrypoint - lookup by path or filename\n if (local.entrypoint) {\n const entrypointId = findScriptId(local.entrypoint);\n if (entrypointId) {\n update.mainScriptFileId = entrypointId;\n }\n }\n\n return update;\n }\n\n /**\n * Get sync status without making changes\n */\n async getStatus(): Promise<AppSyncStatus> {\n const status: AppSyncStatus = {\n newLocal: [],\n newRemote: [],\n modified: [],\n synced: [],\n };\n\n // Load local apps\n const localApps = this.loadLocalApps();\n\n // Fetch remote apps\n const remoteAppList = await this.api.listApps();\n const remoteApps = new Map<string, { app: RemoteApp; reference: string }>();\n\n // Fetch app details in parallel with concurrency limit\n const appDetails = await fetchWithConcurrency(\n remoteAppList,\n async (item) => {\n const app = await this.api.getApp(item.reference);\n return { app, reference: item.reference };\n },\n 15\n );\n\n for (const { app, reference } of appDetails) {\n if (app) {\n remoteApps.set(app.name.toLowerCase(), { app, reference });\n }\n }\n\n // Check local apps\n for (const [key, local] of localApps) {\n const remote = remoteApps.get(key);\n\n if (!remote) {\n status.newLocal.push(local.name);\n } else if (!this.areAppsEqual(local, remote.app)) {\n status.modified.push(local.name);\n } else {\n status.synced.push(local.name);\n }\n }\n\n // Check for new remote apps\n for (const [key, { app }] of remoteApps) {\n if (!localApps.has(key)) {\n status.newRemote.push(app.name);\n }\n }\n\n return status;\n }\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -32,8 +32,11 @@ __export(farseerApi_exports, {
|
|
|
32
32
|
module.exports = __toCommonJS(farseerApi_exports);
|
|
33
33
|
var import_axios = __toESM(require("axios"));
|
|
34
34
|
var import_configService = require("./configService");
|
|
35
|
+
var import_farseerService = require("./farseerService");
|
|
35
36
|
class FarseerApi {
|
|
36
37
|
constructor(accessToken, tenant, tenantId) {
|
|
38
|
+
this.isRefreshing = false;
|
|
39
|
+
this.refreshSubscribers = [];
|
|
37
40
|
this.tenant = tenant;
|
|
38
41
|
this.tenantId = tenantId || tenant;
|
|
39
42
|
const credential = (0, import_configService.getCredential)(tenant);
|
|
@@ -47,6 +50,51 @@ class FarseerApi {
|
|
|
47
50
|
"X-TENANT-ID": this.tenantId
|
|
48
51
|
}
|
|
49
52
|
});
|
|
53
|
+
this.client.interceptors.response.use(
|
|
54
|
+
(response) => response,
|
|
55
|
+
async (error) => {
|
|
56
|
+
const originalRequest = error.config;
|
|
57
|
+
if (error.response?.status === 401 && !originalRequest._retry) {
|
|
58
|
+
originalRequest._retry = true;
|
|
59
|
+
if (this.isRefreshing) {
|
|
60
|
+
return new Promise((resolve) => {
|
|
61
|
+
this.refreshSubscribers.push((token) => {
|
|
62
|
+
originalRequest.headers["Authorization"] = `Bearer ${token}`;
|
|
63
|
+
resolve(this.client(originalRequest));
|
|
64
|
+
});
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
this.isRefreshing = true;
|
|
68
|
+
try {
|
|
69
|
+
const auth = (0, import_configService.getUserAuth)();
|
|
70
|
+
if (!auth?.refreshToken) {
|
|
71
|
+
throw new Error("No refresh token available");
|
|
72
|
+
}
|
|
73
|
+
const refreshed = await (0, import_farseerService.refreshAccessToken)(auth.refreshToken, auth.realm || "master");
|
|
74
|
+
if (!refreshed) {
|
|
75
|
+
throw new Error("Token refresh failed");
|
|
76
|
+
}
|
|
77
|
+
(0, import_configService.setUserAuth)({
|
|
78
|
+
accessToken: refreshed.accessToken,
|
|
79
|
+
refreshToken: refreshed.refreshToken,
|
|
80
|
+
expiresAt: new Date(Date.now() + refreshed.expiresIn * 1e3).toISOString(),
|
|
81
|
+
realm: auth.realm
|
|
82
|
+
});
|
|
83
|
+
this.updateAccessToken(refreshed.accessToken);
|
|
84
|
+
this.refreshSubscribers.forEach((callback) => callback(refreshed.accessToken));
|
|
85
|
+
this.refreshSubscribers = [];
|
|
86
|
+
originalRequest.headers["Authorization"] = `Bearer ${refreshed.accessToken}`;
|
|
87
|
+
return this.client(originalRequest);
|
|
88
|
+
} catch (refreshError) {
|
|
89
|
+
this.refreshSubscribers = [];
|
|
90
|
+
throw error;
|
|
91
|
+
} finally {
|
|
92
|
+
this.isRefreshing = false;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
return Promise.reject(error);
|
|
96
|
+
}
|
|
97
|
+
);
|
|
50
98
|
}
|
|
51
99
|
/**
|
|
52
100
|
* Update the access token (used when token is refreshed)
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../src/services/farseerApi.ts"],
|
|
4
|
-
"sourcesContent": ["/**\n * FarseerApi - Direct HTTP API client using JWT Bearer token authentication\n *\n * This is an alternative to FarseerService that doesn't use farseer-client library.\n * Use this when authenticating via browser login (Keycloak JWT token) instead of API key.\n */\n\nimport axios, { AxiosInstance } from 'axios';\nimport { getCredential } from './configService';\n\nexport interface RemoteFile {\n name: string;\n path: string;\n reference: string;\n}\n\ninterface FolderItem {\n id: number;\n name: string;\n type: 'folder' | 'farseer-file';\n reference?: string;\n}\n\ninterface FolderContent {\n items: FolderItem[];\n}\n\n// App (Remote Job) types\nexport interface AppArgument {\n name: string;\n type: 'variable';\n defaultValue: string;\n}\n\nexport interface AppScriptFile {\n id: string;\n name: string;\n}\n\nexport interface RemoteApp {\n id: number;\n name: string;\n description: string;\n expectedArguments: AppArgument[];\n status: string;\n lastHeartbeat: string;\n mainScriptFileId: string | null;\n scriptFiles: AppScriptFile[];\n creationType: string;\n predefinedActions: unknown[];\n}\n\nexport interface AppListItem {\n id: number;\n name: string;\n type: string;\n reference: string;\n}\n\nexport class FarseerApi {\n private client: AxiosInstance;\n private tenant: string;\n private tenantId: string;\n\n constructor(accessToken: string, tenant: string, tenantId?: string) {\n this.tenant = tenant;\n this.tenantId = tenantId || tenant;\n\n // Try to get basePath from existing config, or generate from tenant name\n const credential = getCredential(tenant);\n const basePath = credential?.basePath || `https://${tenant}.farseer.io/api/v3`;\n\n this.client = axios.create({\n baseURL: basePath,\n headers: {\n 'Authorization': `Bearer ${accessToken}`,\n 'Content-Type': 'application/json',\n 'x-api-version': '3.3.0',\n 'X-TENANT-ID': this.tenantId,\n },\n });\n }\n\n /**\n * Update the access token (used when token is refreshed)\n */\n updateAccessToken(newToken: string): void {\n this.client.defaults.headers['Authorization'] = `Bearer ${newToken}`;\n }\n\n async testConnection(): Promise<boolean> {\n try {\n await this.getItemByPath(['Files']);\n return true;\n } catch {\n return false;\n }\n }\n\n async getFilesFolder(): Promise<{ id: number; name: string; type: string } | null> {\n try {\n const item = await this.getItemByPath(['Files']);\n return {\n id: item.id,\n name: item.name,\n type: item.type,\n };\n } catch {\n return null;\n }\n }\n\n private async listFolderItems(folderId: number | null): Promise<FolderContent> {\n // Body is array: [{ id: null }] for root, [{ id: folderId }] for specific folder\n const response = await this.client.post('/folders/items', [{ id: folderId }]);\n return response.data[0] || { items: [] };\n }\n\n private async findFolderByName(parentId: number | null, name: string): Promise<FolderItem | null> {\n const content = await this.listFolderItems(parentId);\n const folder = content.items?.find(\n (item) => item.name.toLowerCase() === name.toLowerCase() && item.type.toLowerCase() === 'folder'\n );\n return folder || null;\n }\n\n private async getItemByPath(path: string[]): Promise<FolderItem> {\n let currentId: number | null = null;\n\n for (const segment of path) {\n const folder = await this.findFolderByName(currentId, segment);\n if (!folder) {\n throw new Error(`Folder not found: ${segment}`);\n }\n currentId = folder.id;\n }\n\n if (currentId === null) {\n throw new Error('Path is empty');\n }\n\n // Return a minimal FolderItem with the found id\n return { id: currentId, name: path[path.length - 1], type: 'folder' };\n }\n\n async listFiles(folderPath: string[] = ['Files']): Promise<RemoteFile[]> {\n const files: RemoteFile[] = [];\n\n try {\n const folder = await this.getItemByPath(folderPath);\n const folderContent = await this.listFolderItems(folder.id);\n\n for (const item of folderContent.items || []) {\n const itemPath = [...folderPath.slice(1), item.name].join('/');\n const itemType = item.type.toLowerCase();\n\n if (itemType === 'folder') {\n const subFiles = await this.listFiles([...folderPath, item.name]);\n files.push(...subFiles);\n } else if ((itemType === 'farseer-file' || itemType === 'farseer_file') && item.reference) {\n files.push({\n name: item.name,\n path: itemPath,\n reference: item.reference,\n });\n }\n }\n } catch (error) {\n if (axios.isAxiosError(error)) {\n const status = error.response?.status;\n const data = error.response?.data;\n const message = typeof data === 'string' ? data : (data?.message || JSON.stringify(data));\n console.error(`API Error: ${status} \"${message}\" (path: ${folderPath.join('/')})`);\n if (data?.code === 'Forbidden') {\n const errorMessage = data?.message || 'Access denied';\n throw new Error(`Access denied: ${errorMessage}\\nYou may need to use --tenant-id to specify a different tenant ID.`);\n }\n }\n return [];\n }\n\n return files;\n }\n\n async getFileContent(reference: string): Promise<string> {\n const response = await this.client.get(`/farseerFiles/${reference}`, {\n responseType: 'text',\n });\n return response.data;\n }\n\n async getFileContentAsBuffer(reference: string): Promise<Buffer> {\n const response = await this.client.get(`/farseerFiles/${reference}`, {\n responseType: 'arraybuffer',\n });\n return Buffer.from(response.data);\n }\n\n async createFile(content: string | Buffer, fileName: string, folderPath: string[] = ['Files']): Promise<void> {\n await this.ensureFolderPath(folderPath);\n\n const folder = await this.getItemByPath(folderPath);\n\n const formData = new FormData();\n const blob = content instanceof Buffer ? new Blob([content]) : new Blob([content]);\n formData.append('file', blob, fileName);\n formData.append('category', 'GENERAL');\n formData.append('folderId', folder.id.toString());\n\n await this.client.post('/farseerFiles', formData, {\n headers: {\n 'Content-Type': 'multipart/form-data',\n },\n });\n }\n\n async updateFile(reference: string, content: string | Buffer, fileName: string): Promise<void> {\n const formData = new FormData();\n const blob = content instanceof Buffer ? new Blob([content]) : new Blob([content]);\n formData.append('file', blob, fileName);\n\n await this.client.put(`/farseerFiles/${reference}`, formData, {\n headers: {\n 'Content-Type': 'multipart/form-data',\n },\n });\n }\n\n async deleteFile(reference: string): Promise<void> {\n await this.client.delete(`/farseerFiles/${reference}`);\n }\n\n private async ensureFolderPath(folderPath: string[]): Promise<void> {\n let currentPath: string[] = [];\n\n for (const segment of folderPath) {\n currentPath.push(segment);\n try {\n await this.getItemByPath(currentPath);\n } catch {\n if (currentPath.length > 1) {\n const parentPath = currentPath.slice(0, -1);\n const parent = await this.getItemByPath(parentPath);\n await this.client.post('/folders', {\n parentId: parent.id,\n name: segment,\n allowedTypes: [],\n });\n }\n }\n }\n }\n\n async getFileByPath(filePath: string): Promise<RemoteFile | null> {\n const pathParts = ['Files', ...filePath.split('/')];\n const fileName = pathParts.pop()!;\n const folderPath = pathParts;\n\n try {\n const folder = await this.getItemByPath(folderPath);\n const folderContent = await this.listFolderItems(folder.id);\n\n for (const item of folderContent.items || []) {\n const itemType = item.type.toLowerCase();\n if (item.name === fileName && (itemType === 'farseer-file' || itemType === 'farseer_file') && item.reference) {\n return {\n name: item.name,\n path: filePath,\n reference: item.reference,\n };\n }\n }\n } catch {\n return null;\n }\n\n return null;\n }\n\n async getFileMetadata(reference: string): Promise<import('./farseerFactory').FileMetadata | null> {\n try {\n const response = await this.axios.get(`/farseerFiles/${reference}/metadata`);\n const data = response.data;\n\n return {\n uploadTime: data.uploadTime,\n uploader: data.uploader ? {\n id: data.uploader.id,\n email: data.uploader.email || '',\n firstName: data.uploader.firstName || '',\n lastName: data.uploader.lastName || '',\n } : undefined,\n size: data.size,\n };\n } catch (error) {\n // Metadata fetch failed - non-critical, continue without it\n return null;\n }\n }\n\n isTextFile(filename: string): boolean {\n const ext = filename.toLowerCase().split('.').pop() || '';\n return ['ts', 'js', 'mjs', 'cjs', 'json', 'txt', 'md', 'csv', 'xml', 'html', 'css', 'yaml', 'yml'].includes(ext);\n }\n\n isBinaryFile(filename: string): boolean {\n return !this.isTextFile(filename);\n }\n\n // ==================== Apps (Remote Jobs) API ====================\n\n /**\n * Get the Apps folder ID\n */\n async getAppsFolderId(): Promise<number | null> {\n const content = await this.listFolderItems(null);\n const appsFolder = content.items?.find(\n (item) => item.name.toLowerCase() === 'apps' && item.type.toLowerCase() === 'folder'\n );\n return appsFolder?.id || null;\n }\n\n /**\n * List all apps (Remote Jobs) on the tenant, including those in subfolders\n */\n async listApps(): Promise<AppListItem[]> {\n const appsFolderId = await this.getAppsFolderId();\n if (!appsFolderId) {\n return [];\n }\n\n return this.listAppsRecursive(appsFolderId);\n }\n\n /**\n * Recursively list all apps in a folder and its subfolders\n */\n private async listAppsRecursive(folderId: number): Promise<AppListItem[]> {\n const apps: AppListItem[] = [];\n const content = await this.listFolderItems(folderId);\n\n for (const item of content.items || []) {\n const itemType = item.type.toLowerCase();\n\n if (itemType === 'remote_job') {\n // Found an app\n apps.push({\n id: item.id,\n name: item.name,\n type: item.type,\n reference: item.reference || '',\n });\n } else if (itemType === 'folder') {\n // Recurse into subfolder\n const subApps = await this.listAppsRecursive(item.id);\n apps.push(...subApps);\n }\n }\n\n return apps;\n }\n\n /**\n * Get app details by reference ID\n */\n async getApp(referenceId: string): Promise<RemoteApp | null> {\n try {\n const response = await this.client.get(`/remoteJobs/${referenceId}`);\n return response.data;\n } catch {\n return null;\n }\n }\n\n /**\n * Get app by name\n */\n async getAppByName(name: string): Promise<RemoteApp | null> {\n const apps = await this.listApps();\n const app = apps.find((a) => a.name.toLowerCase() === name.toLowerCase());\n if (!app) {\n return null;\n }\n return this.getApp(app.reference);\n }\n\n /**\n * Create a new app\n */\n async createApp(name: string): Promise<RemoteApp> {\n const appsFolderId = await this.getAppsFolderId();\n if (!appsFolderId) {\n throw new Error('Apps folder not found');\n }\n\n const response = await this.client.post('/remoteJobs', {\n folderId: appsFolderId,\n name,\n });\n return response.data;\n }\n\n /**\n * Update an app\n */\n async updateApp(\n referenceId: string,\n update: {\n name?: string;\n description?: string;\n expectedArguments?: AppArgument[];\n mainScriptFileId?: string | null;\n scriptFileIds?: string[];\n }\n ): Promise<RemoteApp> {\n await this.client.put(`/remoteJobs/${referenceId}`, {\n ...update,\n schedule: null,\n runnerName: null,\n predefinedActions: [],\n });\n // API returns 204 No Content, so fetch the updated app\n const app = await this.getApp(referenceId);\n if (!app) {\n throw new Error('Failed to fetch updated app');\n }\n return app;\n }\n\n /**\n * Delete an app\n */\n async deleteApp(referenceId: string): Promise<void> {\n await this.client.delete(`/remoteJobs/${referenceId}`);\n }\n\n /**\n * Run an app with arguments\n */\n async runApp(\n referenceId: string,\n args: Record<string, string> = {}\n ): Promise<{ executionId: string }> {\n const response = await this.client.post(`/remoteJobs/${referenceId}/run`, {\n arguments: args,\n });\n return response.data;\n }\n\n /**\n * Get script files available for apps (searches all .ts/.js files in Files folder)\n * Returns full path relative to Files/ folder (e.g., \"Scripts/test.ts\", \"Scripts/subfolder/helper.ts\")\n */\n async getScriptFiles(): Promise<AppScriptFile[]> {\n // listFiles already searches recursively, so just filter for .ts/.js files\n const allFiles = await this.listFiles(['Files']);\n return allFiles\n .filter(f => f.name.endsWith('.ts') || f.name.endsWith('.js'))\n .map(f => ({\n id: f.reference,\n name: f.path, // Use full path to support scripts in subfolders\n }));\n }\n\n // ==================== Model Export API ====================\n\n /**\n * Export the entire model structure - all dimension tables, variables, and configurations.\n * Essential for understanding a tenant's data model.\n */\n async exportModel(): Promise<ModelExport> {\n const response = await this.client.post('/model/export');\n return response.data;\n }\n}\n\n// Model Export types\nexport interface ModelExport {\n tables: ModelExportTable[];\n variables: ModelExportVariable[];\n dependencies: {\n tables: string[];\n members: string[];\n variables: string[];\n };\n}\n\nexport interface ModelExportTable {\n name: string;\n columns: ModelExportColumn[];\n defaultMember?: string | null;\n folderPath?: string[];\n rows: any[][];\n autoIncrementTemplate?: string | null;\n}\n\nexport interface ModelExportColumn {\n name?: string;\n type: string; // PRIMARY_KEY, FORMULA, ORDER, ACTIVE, DESCRIPTION, FOREIGN_KEY, NUMBER, TEXT, DATE, FILE\n foreignKeyTableName?: string;\n}\n\nexport interface ModelExportVariable {\n name: string;\n description?: string | null;\n formula?: string | null;\n tables: string[];\n members: string[];\n rollupType: string; // SUM, AVERAGE, CLOSING STATE, BEGINNING STATE, FORMULA, MAX, MIN\n dataType: string; // DECIMAL, INTEGER\n numberFormat?: {\n type: string; // NUMBER, CURRENCY, PERCENTAGE, DATE, CUSTOM\n custom?: boolean;\n formatString?: string;\n decimals?: number;\n };\n numberValidation?: {\n min?: number | null;\n max?: number | null;\n };\n topDownMapping?: string | null;\n readonly: boolean;\n calculateOnDemand: boolean;\n zConstraint?: boolean;\n active?: boolean;\n optimizeCalculation?: boolean;\n useHierarchy?: boolean;\n folderPath?: string[];\n cells?: ModelExportCell[];\n}\n\nexport interface ModelExportCell {\n value: number;\n members: string[];\n}\n"],
|
|
5
|
-
"mappings": ";;;;;;;;;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAOA,
|
|
4
|
+
"sourcesContent": ["/**\n * FarseerApi - Direct HTTP API client using JWT Bearer token authentication\n *\n * This is an alternative to FarseerService that doesn't use farseer-client library.\n * Use this when authenticating via browser login (Keycloak JWT token) instead of API key.\n */\n\nimport axios, { AxiosInstance, InternalAxiosRequestConfig } from 'axios';\nimport { getCredential, getUserAuth, setUserAuth } from './configService';\nimport { refreshAccessToken } from './farseerService';\n\nexport interface RemoteFile {\n name: string;\n path: string;\n reference: string;\n}\n\ninterface FolderItem {\n id: number;\n name: string;\n type: 'folder' | 'farseer-file';\n reference?: string;\n}\n\ninterface FolderContent {\n items: FolderItem[];\n}\n\n// App (Remote Job) types\nexport interface AppArgument {\n name: string;\n type: 'variable';\n defaultValue: string;\n}\n\nexport interface AppScriptFile {\n id: string;\n name: string;\n}\n\nexport interface RemoteApp {\n id: number;\n name: string;\n description: string;\n expectedArguments: AppArgument[];\n status: string;\n lastHeartbeat: string;\n mainScriptFileId: string | null;\n scriptFiles: AppScriptFile[];\n creationType: string;\n predefinedActions: unknown[];\n}\n\nexport interface AppListItem {\n id: number;\n name: string;\n type: string;\n reference: string;\n}\n\nexport class FarseerApi {\n private client: AxiosInstance;\n private tenant: string;\n private tenantId: string;\n\n private isRefreshing = false;\n private refreshSubscribers: ((token: string) => void)[] = [];\n\n constructor(accessToken: string, tenant: string, tenantId?: string) {\n this.tenant = tenant;\n this.tenantId = tenantId || tenant;\n\n // Try to get basePath from existing config, or generate from tenant name\n const credential = getCredential(tenant);\n const basePath = credential?.basePath || `https://${tenant}.farseer.io/api/v3`;\n\n this.client = axios.create({\n baseURL: basePath,\n headers: {\n 'Authorization': `Bearer ${accessToken}`,\n 'Content-Type': 'application/json',\n 'x-api-version': '3.3.0',\n 'X-TENANT-ID': this.tenantId,\n },\n });\n\n // Add response interceptor for automatic token refresh on 401\n this.client.interceptors.response.use(\n (response) => response,\n async (error) => {\n const originalRequest = error.config as InternalAxiosRequestConfig & { _retry?: boolean };\n\n // Only handle 401 errors and avoid infinite loops\n if (error.response?.status === 401 && !originalRequest._retry) {\n originalRequest._retry = true;\n\n // If already refreshing, wait for the new token\n if (this.isRefreshing) {\n return new Promise((resolve) => {\n this.refreshSubscribers.push((token: string) => {\n originalRequest.headers['Authorization'] = `Bearer ${token}`;\n resolve(this.client(originalRequest));\n });\n });\n }\n\n this.isRefreshing = true;\n\n try {\n const auth = getUserAuth();\n if (!auth?.refreshToken) {\n throw new Error('No refresh token available');\n }\n\n const refreshed = await refreshAccessToken(auth.refreshToken, auth.realm || 'master');\n if (!refreshed) {\n throw new Error('Token refresh failed');\n }\n\n // Save the new token\n setUserAuth({\n accessToken: refreshed.accessToken,\n refreshToken: refreshed.refreshToken,\n expiresAt: new Date(Date.now() + refreshed.expiresIn * 1000).toISOString(),\n realm: auth.realm,\n });\n\n // Update this client's token\n this.updateAccessToken(refreshed.accessToken);\n\n // Notify all waiting requests\n this.refreshSubscribers.forEach((callback) => callback(refreshed.accessToken));\n this.refreshSubscribers = [];\n\n // Retry the original request\n originalRequest.headers['Authorization'] = `Bearer ${refreshed.accessToken}`;\n return this.client(originalRequest);\n } catch (refreshError) {\n // Token refresh failed - let the error propagate\n this.refreshSubscribers = [];\n throw error;\n } finally {\n this.isRefreshing = false;\n }\n }\n\n return Promise.reject(error);\n }\n );\n }\n\n /**\n * Update the access token (used when token is refreshed)\n */\n updateAccessToken(newToken: string): void {\n this.client.defaults.headers['Authorization'] = `Bearer ${newToken}`;\n }\n\n async testConnection(): Promise<boolean> {\n try {\n await this.getItemByPath(['Files']);\n return true;\n } catch {\n return false;\n }\n }\n\n async getFilesFolder(): Promise<{ id: number; name: string; type: string } | null> {\n try {\n const item = await this.getItemByPath(['Files']);\n return {\n id: item.id,\n name: item.name,\n type: item.type,\n };\n } catch {\n return null;\n }\n }\n\n private async listFolderItems(folderId: number | null): Promise<FolderContent> {\n // Body is array: [{ id: null }] for root, [{ id: folderId }] for specific folder\n const response = await this.client.post('/folders/items', [{ id: folderId }]);\n return response.data[0] || { items: [] };\n }\n\n private async findFolderByName(parentId: number | null, name: string): Promise<FolderItem | null> {\n const content = await this.listFolderItems(parentId);\n const folder = content.items?.find(\n (item) => item.name.toLowerCase() === name.toLowerCase() && item.type.toLowerCase() === 'folder'\n );\n return folder || null;\n }\n\n private async getItemByPath(path: string[]): Promise<FolderItem> {\n let currentId: number | null = null;\n\n for (const segment of path) {\n const folder = await this.findFolderByName(currentId, segment);\n if (!folder) {\n throw new Error(`Folder not found: ${segment}`);\n }\n currentId = folder.id;\n }\n\n if (currentId === null) {\n throw new Error('Path is empty');\n }\n\n // Return a minimal FolderItem with the found id\n return { id: currentId, name: path[path.length - 1], type: 'folder' };\n }\n\n async listFiles(folderPath: string[] = ['Files']): Promise<RemoteFile[]> {\n const files: RemoteFile[] = [];\n\n try {\n const folder = await this.getItemByPath(folderPath);\n const folderContent = await this.listFolderItems(folder.id);\n\n for (const item of folderContent.items || []) {\n const itemPath = [...folderPath.slice(1), item.name].join('/');\n const itemType = item.type.toLowerCase();\n\n if (itemType === 'folder') {\n const subFiles = await this.listFiles([...folderPath, item.name]);\n files.push(...subFiles);\n } else if ((itemType === 'farseer-file' || itemType === 'farseer_file') && item.reference) {\n files.push({\n name: item.name,\n path: itemPath,\n reference: item.reference,\n });\n }\n }\n } catch (error) {\n if (axios.isAxiosError(error)) {\n const status = error.response?.status;\n const data = error.response?.data;\n const message = typeof data === 'string' ? data : (data?.message || JSON.stringify(data));\n console.error(`API Error: ${status} \"${message}\" (path: ${folderPath.join('/')})`);\n if (data?.code === 'Forbidden') {\n const errorMessage = data?.message || 'Access denied';\n throw new Error(`Access denied: ${errorMessage}\\nYou may need to use --tenant-id to specify a different tenant ID.`);\n }\n }\n return [];\n }\n\n return files;\n }\n\n async getFileContent(reference: string): Promise<string> {\n const response = await this.client.get(`/farseerFiles/${reference}`, {\n responseType: 'text',\n });\n return response.data;\n }\n\n async getFileContentAsBuffer(reference: string): Promise<Buffer> {\n const response = await this.client.get(`/farseerFiles/${reference}`, {\n responseType: 'arraybuffer',\n });\n return Buffer.from(response.data);\n }\n\n async createFile(content: string | Buffer, fileName: string, folderPath: string[] = ['Files']): Promise<void> {\n await this.ensureFolderPath(folderPath);\n\n const folder = await this.getItemByPath(folderPath);\n\n const formData = new FormData();\n const blob = content instanceof Buffer ? new Blob([content]) : new Blob([content]);\n formData.append('file', blob, fileName);\n formData.append('category', 'GENERAL');\n formData.append('folderId', folder.id.toString());\n\n await this.client.post('/farseerFiles', formData, {\n headers: {\n 'Content-Type': 'multipart/form-data',\n },\n });\n }\n\n async updateFile(reference: string, content: string | Buffer, fileName: string): Promise<void> {\n const formData = new FormData();\n const blob = content instanceof Buffer ? new Blob([content]) : new Blob([content]);\n formData.append('file', blob, fileName);\n\n await this.client.put(`/farseerFiles/${reference}`, formData, {\n headers: {\n 'Content-Type': 'multipart/form-data',\n },\n });\n }\n\n async deleteFile(reference: string): Promise<void> {\n await this.client.delete(`/farseerFiles/${reference}`);\n }\n\n private async ensureFolderPath(folderPath: string[]): Promise<void> {\n let currentPath: string[] = [];\n\n for (const segment of folderPath) {\n currentPath.push(segment);\n try {\n await this.getItemByPath(currentPath);\n } catch {\n if (currentPath.length > 1) {\n const parentPath = currentPath.slice(0, -1);\n const parent = await this.getItemByPath(parentPath);\n await this.client.post('/folders', {\n parentId: parent.id,\n name: segment,\n allowedTypes: [],\n });\n }\n }\n }\n }\n\n async getFileByPath(filePath: string): Promise<RemoteFile | null> {\n const pathParts = ['Files', ...filePath.split('/')];\n const fileName = pathParts.pop()!;\n const folderPath = pathParts;\n\n try {\n const folder = await this.getItemByPath(folderPath);\n const folderContent = await this.listFolderItems(folder.id);\n\n for (const item of folderContent.items || []) {\n const itemType = item.type.toLowerCase();\n if (item.name === fileName && (itemType === 'farseer-file' || itemType === 'farseer_file') && item.reference) {\n return {\n name: item.name,\n path: filePath,\n reference: item.reference,\n };\n }\n }\n } catch {\n return null;\n }\n\n return null;\n }\n\n async getFileMetadata(reference: string): Promise<import('./farseerFactory').FileMetadata | null> {\n try {\n const response = await this.axios.get(`/farseerFiles/${reference}/metadata`);\n const data = response.data;\n\n return {\n uploadTime: data.uploadTime,\n uploader: data.uploader ? {\n id: data.uploader.id,\n email: data.uploader.email || '',\n firstName: data.uploader.firstName || '',\n lastName: data.uploader.lastName || '',\n } : undefined,\n size: data.size,\n };\n } catch (error) {\n // Metadata fetch failed - non-critical, continue without it\n return null;\n }\n }\n\n isTextFile(filename: string): boolean {\n const ext = filename.toLowerCase().split('.').pop() || '';\n return ['ts', 'js', 'mjs', 'cjs', 'json', 'txt', 'md', 'csv', 'xml', 'html', 'css', 'yaml', 'yml'].includes(ext);\n }\n\n isBinaryFile(filename: string): boolean {\n return !this.isTextFile(filename);\n }\n\n // ==================== Apps (Remote Jobs) API ====================\n\n /**\n * Get the Apps folder ID\n */\n async getAppsFolderId(): Promise<number | null> {\n const content = await this.listFolderItems(null);\n const appsFolder = content.items?.find(\n (item) => item.name.toLowerCase() === 'apps' && item.type.toLowerCase() === 'folder'\n );\n return appsFolder?.id || null;\n }\n\n /**\n * List all apps (Remote Jobs) on the tenant, including those in subfolders\n */\n async listApps(): Promise<AppListItem[]> {\n const appsFolderId = await this.getAppsFolderId();\n if (!appsFolderId) {\n return [];\n }\n\n return this.listAppsRecursive(appsFolderId);\n }\n\n /**\n * Recursively list all apps in a folder and its subfolders\n */\n private async listAppsRecursive(folderId: number): Promise<AppListItem[]> {\n const apps: AppListItem[] = [];\n const content = await this.listFolderItems(folderId);\n\n for (const item of content.items || []) {\n const itemType = item.type.toLowerCase();\n\n if (itemType === 'remote_job') {\n // Found an app\n apps.push({\n id: item.id,\n name: item.name,\n type: item.type,\n reference: item.reference || '',\n });\n } else if (itemType === 'folder') {\n // Recurse into subfolder\n const subApps = await this.listAppsRecursive(item.id);\n apps.push(...subApps);\n }\n }\n\n return apps;\n }\n\n /**\n * Get app details by reference ID\n */\n async getApp(referenceId: string): Promise<RemoteApp | null> {\n try {\n const response = await this.client.get(`/remoteJobs/${referenceId}`);\n return response.data;\n } catch {\n return null;\n }\n }\n\n /**\n * Get app by name\n */\n async getAppByName(name: string): Promise<RemoteApp | null> {\n const apps = await this.listApps();\n const app = apps.find((a) => a.name.toLowerCase() === name.toLowerCase());\n if (!app) {\n return null;\n }\n return this.getApp(app.reference);\n }\n\n /**\n * Create a new app\n */\n async createApp(name: string): Promise<RemoteApp> {\n const appsFolderId = await this.getAppsFolderId();\n if (!appsFolderId) {\n throw new Error('Apps folder not found');\n }\n\n const response = await this.client.post('/remoteJobs', {\n folderId: appsFolderId,\n name,\n });\n return response.data;\n }\n\n /**\n * Update an app\n */\n async updateApp(\n referenceId: string,\n update: {\n name?: string;\n description?: string;\n expectedArguments?: AppArgument[];\n mainScriptFileId?: string | null;\n scriptFileIds?: string[];\n }\n ): Promise<RemoteApp> {\n await this.client.put(`/remoteJobs/${referenceId}`, {\n ...update,\n schedule: null,\n runnerName: null,\n predefinedActions: [],\n });\n // API returns 204 No Content, so fetch the updated app\n const app = await this.getApp(referenceId);\n if (!app) {\n throw new Error('Failed to fetch updated app');\n }\n return app;\n }\n\n /**\n * Delete an app\n */\n async deleteApp(referenceId: string): Promise<void> {\n await this.client.delete(`/remoteJobs/${referenceId}`);\n }\n\n /**\n * Run an app with arguments\n */\n async runApp(\n referenceId: string,\n args: Record<string, string> = {}\n ): Promise<{ executionId: string }> {\n const response = await this.client.post(`/remoteJobs/${referenceId}/run`, {\n arguments: args,\n });\n return response.data;\n }\n\n /**\n * Get script files available for apps (searches all .ts/.js files in Files folder)\n * Returns full path relative to Files/ folder (e.g., \"Scripts/test.ts\", \"Scripts/subfolder/helper.ts\")\n */\n async getScriptFiles(): Promise<AppScriptFile[]> {\n // listFiles already searches recursively, so just filter for .ts/.js files\n const allFiles = await this.listFiles(['Files']);\n return allFiles\n .filter(f => f.name.endsWith('.ts') || f.name.endsWith('.js'))\n .map(f => ({\n id: f.reference,\n name: f.path, // Use full path to support scripts in subfolders\n }));\n }\n\n // ==================== Model Export API ====================\n\n /**\n * Export the entire model structure - all dimension tables, variables, and configurations.\n * Essential for understanding a tenant's data model.\n */\n async exportModel(): Promise<ModelExport> {\n const response = await this.client.post('/model/export');\n return response.data;\n }\n}\n\n// Model Export types\nexport interface ModelExport {\n tables: ModelExportTable[];\n variables: ModelExportVariable[];\n dependencies: {\n tables: string[];\n members: string[];\n variables: string[];\n };\n}\n\nexport interface ModelExportTable {\n name: string;\n columns: ModelExportColumn[];\n defaultMember?: string | null;\n folderPath?: string[];\n rows: any[][];\n autoIncrementTemplate?: string | null;\n}\n\nexport interface ModelExportColumn {\n name?: string;\n type: string; // PRIMARY_KEY, FORMULA, ORDER, ACTIVE, DESCRIPTION, FOREIGN_KEY, NUMBER, TEXT, DATE, FILE\n foreignKeyTableName?: string;\n}\n\nexport interface ModelExportVariable {\n name: string;\n description?: string | null;\n formula?: string | null;\n tables: string[];\n members: string[];\n rollupType: string; // SUM, AVERAGE, CLOSING STATE, BEGINNING STATE, FORMULA, MAX, MIN\n dataType: string; // DECIMAL, INTEGER\n numberFormat?: {\n type: string; // NUMBER, CURRENCY, PERCENTAGE, DATE, CUSTOM\n custom?: boolean;\n formatString?: string;\n decimals?: number;\n };\n numberValidation?: {\n min?: number | null;\n max?: number | null;\n };\n topDownMapping?: string | null;\n readonly: boolean;\n calculateOnDemand: boolean;\n zConstraint?: boolean;\n active?: boolean;\n optimizeCalculation?: boolean;\n useHierarchy?: boolean;\n folderPath?: string[];\n cells?: ModelExportCell[];\n}\n\nexport interface ModelExportCell {\n value: number;\n members: string[];\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": ["axios"]
|
|
7
7
|
}
|
|
@@ -34,6 +34,7 @@ var fs = __toESM(require("fs"));
|
|
|
34
34
|
var path = __toESM(require("path"));
|
|
35
35
|
var import_farseerFactory = require("./farseerFactory");
|
|
36
36
|
var import_helpers = require("../utils/helpers");
|
|
37
|
+
var import_logger = require("../utils/logger");
|
|
37
38
|
class SyncService {
|
|
38
39
|
constructor(tenant, farseerClient) {
|
|
39
40
|
this.tenant = tenant;
|
|
@@ -84,9 +85,16 @@ class SyncService {
|
|
|
84
85
|
);
|
|
85
86
|
}
|
|
86
87
|
const remoteFileHashes = {};
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
88
|
+
const fileContents = await (0, import_helpers.fetchWithConcurrency)(
|
|
89
|
+
remoteFiles,
|
|
90
|
+
async (file) => {
|
|
91
|
+
const content = await this.farseerClient.getFileContentAsBuffer(file.reference);
|
|
92
|
+
return { path: file.path, hash: (0, import_helpers.calculateHash)(content) };
|
|
93
|
+
},
|
|
94
|
+
15
|
|
95
|
+
);
|
|
96
|
+
for (const { path: path2, hash } of fileContents) {
|
|
97
|
+
remoteFileHashes[path2] = hash;
|
|
90
98
|
}
|
|
91
99
|
const status = {
|
|
92
100
|
modifiedLocally: [],
|
|
@@ -152,9 +160,23 @@ class SyncService {
|
|
|
152
160
|
unchanged: []
|
|
153
161
|
};
|
|
154
162
|
const remoteFilePaths = new Set(remoteFiles.map((f) => f.path));
|
|
155
|
-
|
|
163
|
+
import_logger.logger.dim(`Fetching ${remoteFiles.length} files in parallel...`);
|
|
164
|
+
const fileContents = await (0, import_helpers.fetchWithConcurrency)(
|
|
165
|
+
remoteFiles,
|
|
166
|
+
async (file, index) => {
|
|
167
|
+
const content = await this.farseerClient.getFileContentAsBuffer(file.reference);
|
|
168
|
+
if ((index + 1) % 10 === 0 || index === remoteFiles.length - 1) {
|
|
169
|
+
process.stdout.write(`\r Progress: ${index + 1}/${remoteFiles.length} files`);
|
|
170
|
+
}
|
|
171
|
+
return { file, content };
|
|
172
|
+
},
|
|
173
|
+
15
|
|
174
|
+
);
|
|
175
|
+
if (remoteFiles.length > 0) {
|
|
176
|
+
process.stdout.write("\r" + " ".repeat(50) + "\r");
|
|
177
|
+
}
|
|
178
|
+
for (const { file, content } of fileContents) {
|
|
156
179
|
const localPath = path.join(filesDir, file.path);
|
|
157
|
-
const content = await this.farseerClient.getFileContentAsBuffer(file.reference);
|
|
158
180
|
const remoteHash = (0, import_helpers.calculateHash)(content);
|
|
159
181
|
let needsUpdate = true;
|
|
160
182
|
if (fs.existsSync(localPath)) {
|
|
@@ -259,10 +281,16 @@ class SyncService {
|
|
|
259
281
|
remoteFiles = remoteFiles.filter(
|
|
260
282
|
(f) => import_farseerFactory.DEFAULT_SCRIPT_EXTENSIONS.some((ext) => f.name.toLowerCase().endsWith(ext))
|
|
261
283
|
);
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
284
|
+
const fileContents = await (0, import_helpers.fetchWithConcurrency)(
|
|
285
|
+
remoteFiles,
|
|
286
|
+
async (file) => {
|
|
287
|
+
const content = await this.farseerClient.getFileContentAsBuffer(file.reference);
|
|
288
|
+
return { path: file.path, hash: (0, import_helpers.calculateHash)(content) };
|
|
289
|
+
},
|
|
290
|
+
15
|
|
291
|
+
);
|
|
292
|
+
for (const { path: filePath, hash: remoteHash } of fileContents) {
|
|
293
|
+
const syncInfo = syncState.files[filePath];
|
|
266
294
|
if (!syncInfo || syncInfo.remoteHash !== remoteHash) {
|
|
267
295
|
return true;
|
|
268
296
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../src/services/syncService.ts"],
|
|
4
|
-
"sourcesContent": ["import * as fs from 'fs';\nimport * as path from 'path';\nimport { IFarseerClient, RemoteFile, DEFAULT_SCRIPT_EXTENSIONS } from './farseerFactory';\nimport { calculateHash, getTenantFilesDir, getSyncFilePath, ensureDirectoryExists, getAllFilesInDir } from '../utils/helpers';\n\nexport interface SyncFileInfo {\n localHash: string;\n remoteHash: string;\n metadata?: {\n uploadTime?: string;\n uploaderEmail?: string;\n uploaderName?: string;\n lastFetched?: string;\n };\n}\n\nexport interface SyncState {\n lastSync: string;\n files: Record<string, SyncFileInfo>;\n}\n\nexport interface SyncStatus {\n modifiedLocally: string[];\n modifiedRemotely: string[];\n onlyLocal: string[];\n onlyRemote: string[];\n inSync: string[];\n}\n\nexport class SyncService {\n private tenant: string;\n private farseerClient: IFarseerClient;\n\n constructor(tenant: string, farseerClient: IFarseerClient) {\n this.tenant = tenant;\n this.farseerClient = farseerClient;\n }\n\n loadSyncState(): SyncState {\n const syncFilePath = getSyncFilePath(this.tenant);\n\n if (!fs.existsSync(syncFilePath)) {\n return {\n lastSync: '',\n files: {},\n };\n }\n\n try {\n const content = fs.readFileSync(syncFilePath, 'utf-8');\n return JSON.parse(content) as SyncState;\n } catch {\n return {\n lastSync: '',\n files: {},\n };\n }\n }\n\n saveSyncState(state: SyncState): void {\n const syncFilePath = getSyncFilePath(this.tenant);\n ensureDirectoryExists(path.dirname(syncFilePath));\n fs.writeFileSync(syncFilePath, JSON.stringify(state, null, 2), 'utf-8');\n }\n\n async getStatus(options?: { all?: boolean }): Promise<SyncStatus> {\n const syncState = this.loadSyncState();\n const filesDir = getTenantFilesDir(this.tenant);\n\n // Get local files\n let localFiles = getAllFilesInDir(filesDir);\n\n // Filter to scripts only unless --all is specified\n if (!options?.all) {\n localFiles = localFiles.filter(f =>\n DEFAULT_SCRIPT_EXTENSIONS.some(ext => f.toLowerCase().endsWith(ext))\n );\n }\n\n const localFileHashes: Record<string, string> = {};\n\n for (const file of localFiles) {\n const fullPath = path.join(filesDir, file);\n const content = fs.readFileSync(fullPath);\n localFileHashes[file] = calculateHash(content);\n }\n\n // Get remote files\n let remoteFiles = await this.farseerClient.listFiles();\n\n // Filter to scripts only unless --all is specified\n if (!options?.all) {\n remoteFiles = remoteFiles.filter(f =>\n DEFAULT_SCRIPT_EXTENSIONS.some(ext => f.name.toLowerCase().endsWith(ext))\n );\n }\n\n const remoteFileHashes: Record<string, string> = {};\n\n for (const file of remoteFiles) {\n const content = await this.farseerClient.getFileContentAsBuffer(file.reference);\n remoteFileHashes[file.path] = calculateHash(content);\n }\n\n const status: SyncStatus = {\n modifiedLocally: [],\n modifiedRemotely: [],\n onlyLocal: [],\n onlyRemote: [],\n inSync: [],\n };\n\n const allFiles = new Set([...Object.keys(localFileHashes), ...Object.keys(remoteFileHashes)]);\n\n for (const file of allFiles) {\n const localHash = localFileHashes[file];\n const remoteHash = remoteFileHashes[file];\n const syncInfo = syncState.files[file];\n\n if (localHash && remoteHash) {\n // File exists both locally and remotely\n if (localHash === remoteHash) {\n status.inSync.push(file);\n } else if (syncInfo) {\n // We have sync state to compare\n const localChanged = localHash !== syncInfo.localHash;\n const remoteChanged = remoteHash !== syncInfo.remoteHash;\n\n if (localChanged && remoteChanged) {\n // Both changed - conflict (treat as modified locally for now)\n status.modifiedLocally.push(file);\n status.modifiedRemotely.push(file);\n } else if (localChanged) {\n status.modifiedLocally.push(file);\n } else if (remoteChanged) {\n status.modifiedRemotely.push(file);\n } else {\n status.inSync.push(file);\n }\n } else {\n // No sync state - files are different, treat as conflict\n status.modifiedLocally.push(file);\n }\n } else if (localHash && !remoteHash) {\n // Only exists locally\n if (syncInfo && syncInfo.remoteHash) {\n // Was synced before, deleted on remote\n status.modifiedRemotely.push(file);\n } else {\n status.onlyLocal.push(file);\n }\n } else if (!localHash && remoteHash) {\n // Only exists remotely\n if (syncInfo && syncInfo.localHash) {\n // Was synced before, deleted locally\n status.modifiedLocally.push(file);\n } else {\n status.onlyRemote.push(file);\n }\n }\n }\n\n return status;\n }\n\n async pull(options?: { all?: boolean }): Promise<{\n downloaded: string[];\n deleted: string[];\n unchanged: string[];\n }> {\n const filesDir = getTenantFilesDir(this.tenant);\n ensureDirectoryExists(filesDir);\n\n const syncState = this.loadSyncState();\n let remoteFiles = await this.farseerClient.listFiles();\n\n // Filter to scripts only unless --all is specified\n if (!options?.all) {\n remoteFiles = remoteFiles.filter(f =>\n DEFAULT_SCRIPT_EXTENSIONS.some(ext => f.name.toLowerCase().endsWith(ext))\n );\n }\n\n // Batch fetch metadata for all files\n remoteFiles = await this.fetchMetadataForFiles(remoteFiles);\n\n const result = {\n downloaded: [] as string[],\n deleted: [] as string[],\n unchanged: [] as string[],\n };\n\n const remoteFilePaths = new Set(remoteFiles.map(f => f.path));\n\n // Download/update files from remote\n for (const file of remoteFiles) {\n const localPath = path.join(filesDir, file.path);\n const content = await this.farseerClient.getFileContentAsBuffer(file.reference);\n const remoteHash = calculateHash(content);\n\n let needsUpdate = true;\n\n if (fs.existsSync(localPath)) {\n const localContent = fs.readFileSync(localPath);\n const localHash = calculateHash(localContent);\n\n if (localHash === remoteHash) {\n needsUpdate = false;\n result.unchanged.push(file.path);\n }\n }\n\n if (needsUpdate) {\n ensureDirectoryExists(path.dirname(localPath));\n fs.writeFileSync(localPath, content);\n result.downloaded.push(file.path);\n }\n\n // Update sync state with metadata\n syncState.files[file.path] = {\n localHash: remoteHash,\n remoteHash: remoteHash,\n metadata: file.metadata ? {\n uploadTime: file.metadata.uploadTime,\n uploaderEmail: file.metadata.uploader?.email,\n uploaderName: file.metadata.uploader\n ? `${file.metadata.uploader.firstName} ${file.metadata.uploader.lastName}`.trim()\n : undefined,\n lastFetched: new Date().toISOString(),\n } : undefined,\n };\n }\n\n // Check for files that were deleted on remote\n const localFiles = getAllFilesInDir(filesDir);\n for (const localFile of localFiles) {\n if (!remoteFilePaths.has(localFile)) {\n // File exists locally but not on remote\n const syncInfo = syncState.files[localFile];\n if (syncInfo) {\n // Was synced before, now deleted on remote - delete locally\n const localPath = path.join(filesDir, localFile);\n fs.unlinkSync(localPath);\n result.deleted.push(localFile);\n delete syncState.files[localFile];\n }\n // If no sync info, it's a new local file - don't delete\n }\n }\n\n syncState.lastSync = new Date().toISOString();\n this.saveSyncState(syncState);\n\n return result;\n }\n\n async push(): Promise<{\n uploaded: string[];\n updated: string[];\n deleted: string[];\n unchanged: string[];\n }> {\n const filesDir = getTenantFilesDir(this.tenant);\n const syncState = this.loadSyncState();\n\n const result = {\n uploaded: [] as string[],\n updated: [] as string[],\n deleted: [] as string[],\n unchanged: [] as string[],\n };\n\n // Get current remote files\n const remoteFiles = await this.farseerClient.listFiles();\n const remoteFileMap = new Map<string, RemoteFile>();\n for (const file of remoteFiles) {\n remoteFileMap.set(file.path, file);\n }\n\n // Get local files\n const localFiles = getAllFilesInDir(filesDir);\n\n // Upload/update local files\n for (const localFile of localFiles) {\n const localPath = path.join(filesDir, localFile);\n const content = fs.readFileSync(localPath);\n const localHash = calculateHash(content);\n\n const remoteFile = remoteFileMap.get(localFile);\n\n if (remoteFile) {\n // File exists on remote - check if update needed\n const remoteContent = await this.farseerClient.getFileContentAsBuffer(remoteFile.reference);\n const remoteHash = calculateHash(remoteContent);\n\n if (localHash !== remoteHash) {\n await this.farseerClient.updateFile(remoteFile.reference, content, path.basename(localFile));\n result.updated.push(localFile);\n } else {\n result.unchanged.push(localFile);\n }\n } else {\n // File doesn't exist on remote - create it\n const pathParts = localFile.split('/');\n const fileName = pathParts.pop()!;\n const folderPath = ['Files', ...pathParts];\n\n await this.farseerClient.createFile(content, fileName, folderPath);\n result.uploaded.push(localFile);\n }\n\n // Update sync state\n syncState.files[localFile] = {\n localHash: localHash,\n remoteHash: localHash,\n };\n }\n\n // Delete files on remote that were deleted locally\n const localFileSet = new Set(localFiles);\n for (const [remotePath, remoteFile] of remoteFileMap) {\n if (!localFileSet.has(remotePath)) {\n const syncInfo = syncState.files[remotePath];\n if (syncInfo) {\n // Was synced before, now deleted locally - delete on remote\n await this.farseerClient.deleteFile(remoteFile.reference);\n result.deleted.push(remotePath);\n delete syncState.files[remotePath];\n }\n }\n }\n\n syncState.lastSync = new Date().toISOString();\n this.saveSyncState(syncState);\n\n return result;\n }\n\n async checkRemoteChanges(): Promise<boolean> {\n const syncState = this.loadSyncState();\n let remoteFiles = await this.farseerClient.listFiles();\n\n // Filter to scripts only (same as pull/status) to avoid false positives\n // from non-script files like .json, .png, etc.\n remoteFiles = remoteFiles.filter(f =>\n DEFAULT_SCRIPT_EXTENSIONS.some(ext => f.name.toLowerCase().endsWith(ext))\n );\n\n for (const file of remoteFiles) {\n const content = await this.farseerClient.getFileContentAsBuffer(file.reference);\n const remoteHash = calculateHash(content);\n const syncInfo = syncState.files[file.path];\n\n if (!syncInfo || syncInfo.remoteHash !== remoteHash) {\n return true; // Remote has changes\n }\n }\n\n // Check for deleted files on remote (only for script files in sync state)\n for (const filePath of Object.keys(syncState.files)) {\n // Only check script files\n const isScript = DEFAULT_SCRIPT_EXTENSIONS.some(ext => filePath.toLowerCase().endsWith(ext));\n if (!isScript) continue;\n\n const exists = remoteFiles.some(f => f.path === filePath);\n if (!exists && syncState.files[filePath].remoteHash) {\n return true; // File was deleted on remote\n }\n }\n\n return false;\n }\n\n async getFileDiff(filePath: string): Promise<{\n localContent: string | null;\n remoteContent: string | null;\n }> {\n const filesDir = getTenantFilesDir(this.tenant);\n const localPath = path.join(filesDir, filePath);\n\n let localContent: string | null = null;\n let remoteContent: string | null = null;\n\n if (fs.existsSync(localPath)) {\n // For diff, read as text (diff only makes sense for text files)\n localContent = fs.readFileSync(localPath, 'utf-8');\n }\n\n const remoteFile = await this.farseerClient.getFileByPath(filePath);\n if (remoteFile) {\n remoteContent = await this.farseerClient.getFileContent(remoteFile.reference);\n }\n\n return { localContent, remoteContent };\n }\n\n /**\n * Batch fetch metadata for multiple files in parallel.\n * Fetches metadata in batches of 10 to avoid overwhelming the API.\n * Non-critical operation - failures are silently ignored.\n */\n private async fetchMetadataForFiles(files: RemoteFile[]): Promise<RemoteFile[]> {\n const batchSize = 10;\n\n for (let i = 0; i < files.length; i += batchSize) {\n const batch = files.slice(i, i + batchSize);\n\n await Promise.all(batch.map(async (file) => {\n try {\n const metadata = await this.farseerClient.getFileMetadata(file.reference);\n if (metadata) {\n file.metadata = metadata;\n }\n } catch {\n // Silently ignore metadata fetch errors - non-critical\n }\n }));\n }\n\n return files;\n }\n}\n"],
|
|
5
|
-
"mappings": ";;;;;;;;;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,SAAoB;AACpB,WAAsB;AACtB,4BAAsE;AACtE,
|
|
6
|
-
"names": []
|
|
4
|
+
"sourcesContent": ["import * as fs from 'fs';\nimport * as path from 'path';\nimport { IFarseerClient, RemoteFile, DEFAULT_SCRIPT_EXTENSIONS } from './farseerFactory';\nimport { calculateHash, getTenantFilesDir, getSyncFilePath, ensureDirectoryExists, getAllFilesInDir, fetchWithConcurrency } from '../utils/helpers';\nimport { logger } from '../utils/logger';\n\nexport interface SyncFileInfo {\n localHash: string;\n remoteHash: string;\n metadata?: {\n uploadTime?: string;\n uploaderEmail?: string;\n uploaderName?: string;\n lastFetched?: string;\n };\n}\n\nexport interface SyncState {\n lastSync: string;\n files: Record<string, SyncFileInfo>;\n}\n\nexport interface SyncStatus {\n modifiedLocally: string[];\n modifiedRemotely: string[];\n onlyLocal: string[];\n onlyRemote: string[];\n inSync: string[];\n}\n\nexport class SyncService {\n private tenant: string;\n private farseerClient: IFarseerClient;\n\n constructor(tenant: string, farseerClient: IFarseerClient) {\n this.tenant = tenant;\n this.farseerClient = farseerClient;\n }\n\n loadSyncState(): SyncState {\n const syncFilePath = getSyncFilePath(this.tenant);\n\n if (!fs.existsSync(syncFilePath)) {\n return {\n lastSync: '',\n files: {},\n };\n }\n\n try {\n const content = fs.readFileSync(syncFilePath, 'utf-8');\n return JSON.parse(content) as SyncState;\n } catch {\n return {\n lastSync: '',\n files: {},\n };\n }\n }\n\n saveSyncState(state: SyncState): void {\n const syncFilePath = getSyncFilePath(this.tenant);\n ensureDirectoryExists(path.dirname(syncFilePath));\n fs.writeFileSync(syncFilePath, JSON.stringify(state, null, 2), 'utf-8');\n }\n\n async getStatus(options?: { all?: boolean }): Promise<SyncStatus> {\n const syncState = this.loadSyncState();\n const filesDir = getTenantFilesDir(this.tenant);\n\n // Get local files\n let localFiles = getAllFilesInDir(filesDir);\n\n // Filter to scripts only unless --all is specified\n if (!options?.all) {\n localFiles = localFiles.filter(f =>\n DEFAULT_SCRIPT_EXTENSIONS.some(ext => f.toLowerCase().endsWith(ext))\n );\n }\n\n const localFileHashes: Record<string, string> = {};\n\n for (const file of localFiles) {\n const fullPath = path.join(filesDir, file);\n const content = fs.readFileSync(fullPath);\n localFileHashes[file] = calculateHash(content);\n }\n\n // Get remote files\n let remoteFiles = await this.farseerClient.listFiles();\n\n // Filter to scripts only unless --all is specified\n if (!options?.all) {\n remoteFiles = remoteFiles.filter(f =>\n DEFAULT_SCRIPT_EXTENSIONS.some(ext => f.name.toLowerCase().endsWith(ext))\n );\n }\n\n const remoteFileHashes: Record<string, string> = {};\n\n // Fetch file contents in parallel with concurrency limit\n const fileContents = await fetchWithConcurrency(\n remoteFiles,\n async (file) => {\n const content = await this.farseerClient.getFileContentAsBuffer(file.reference);\n return { path: file.path, hash: calculateHash(content) };\n },\n 15\n );\n\n for (const { path, hash } of fileContents) {\n remoteFileHashes[path] = hash;\n }\n\n const status: SyncStatus = {\n modifiedLocally: [],\n modifiedRemotely: [],\n onlyLocal: [],\n onlyRemote: [],\n inSync: [],\n };\n\n const allFiles = new Set([...Object.keys(localFileHashes), ...Object.keys(remoteFileHashes)]);\n\n for (const file of allFiles) {\n const localHash = localFileHashes[file];\n const remoteHash = remoteFileHashes[file];\n const syncInfo = syncState.files[file];\n\n if (localHash && remoteHash) {\n // File exists both locally and remotely\n if (localHash === remoteHash) {\n status.inSync.push(file);\n } else if (syncInfo) {\n // We have sync state to compare\n const localChanged = localHash !== syncInfo.localHash;\n const remoteChanged = remoteHash !== syncInfo.remoteHash;\n\n if (localChanged && remoteChanged) {\n // Both changed - conflict (treat as modified locally for now)\n status.modifiedLocally.push(file);\n status.modifiedRemotely.push(file);\n } else if (localChanged) {\n status.modifiedLocally.push(file);\n } else if (remoteChanged) {\n status.modifiedRemotely.push(file);\n } else {\n status.inSync.push(file);\n }\n } else {\n // No sync state - files are different, treat as conflict\n status.modifiedLocally.push(file);\n }\n } else if (localHash && !remoteHash) {\n // Only exists locally\n if (syncInfo && syncInfo.remoteHash) {\n // Was synced before, deleted on remote\n status.modifiedRemotely.push(file);\n } else {\n status.onlyLocal.push(file);\n }\n } else if (!localHash && remoteHash) {\n // Only exists remotely\n if (syncInfo && syncInfo.localHash) {\n // Was synced before, deleted locally\n status.modifiedLocally.push(file);\n } else {\n status.onlyRemote.push(file);\n }\n }\n }\n\n return status;\n }\n\n async pull(options?: { all?: boolean }): Promise<{\n downloaded: string[];\n deleted: string[];\n unchanged: string[];\n }> {\n const filesDir = getTenantFilesDir(this.tenant);\n ensureDirectoryExists(filesDir);\n\n const syncState = this.loadSyncState();\n let remoteFiles = await this.farseerClient.listFiles();\n\n // Filter to scripts only unless --all is specified\n if (!options?.all) {\n remoteFiles = remoteFiles.filter(f =>\n DEFAULT_SCRIPT_EXTENSIONS.some(ext => f.name.toLowerCase().endsWith(ext))\n );\n }\n\n // Batch fetch metadata for all files\n remoteFiles = await this.fetchMetadataForFiles(remoteFiles);\n\n const result = {\n downloaded: [] as string[],\n deleted: [] as string[],\n unchanged: [] as string[],\n };\n\n const remoteFilePaths = new Set(remoteFiles.map(f => f.path));\n\n // Download file contents in parallel with concurrency limit\n logger.dim(`Fetching ${remoteFiles.length} files in parallel...`);\n const fileContents = await fetchWithConcurrency(\n remoteFiles,\n async (file, index) => {\n const content = await this.farseerClient.getFileContentAsBuffer(file.reference);\n // Show progress every 10 files\n if ((index + 1) % 10 === 0 || index === remoteFiles.length - 1) {\n process.stdout.write(`\\r Progress: ${index + 1}/${remoteFiles.length} files`);\n }\n return { file, content };\n },\n 15\n );\n // Clear progress line\n if (remoteFiles.length > 0) {\n process.stdout.write('\\r' + ' '.repeat(50) + '\\r');\n }\n\n // Process downloaded files\n for (const { file, content } of fileContents) {\n const localPath = path.join(filesDir, file.path);\n const remoteHash = calculateHash(content);\n\n let needsUpdate = true;\n\n if (fs.existsSync(localPath)) {\n const localContent = fs.readFileSync(localPath);\n const localHash = calculateHash(localContent);\n\n if (localHash === remoteHash) {\n needsUpdate = false;\n result.unchanged.push(file.path);\n }\n }\n\n if (needsUpdate) {\n ensureDirectoryExists(path.dirname(localPath));\n fs.writeFileSync(localPath, content);\n result.downloaded.push(file.path);\n }\n\n // Update sync state with metadata\n syncState.files[file.path] = {\n localHash: remoteHash,\n remoteHash: remoteHash,\n metadata: file.metadata ? {\n uploadTime: file.metadata.uploadTime,\n uploaderEmail: file.metadata.uploader?.email,\n uploaderName: file.metadata.uploader\n ? `${file.metadata.uploader.firstName} ${file.metadata.uploader.lastName}`.trim()\n : undefined,\n lastFetched: new Date().toISOString(),\n } : undefined,\n };\n }\n\n // Check for files that were deleted on remote\n const localFiles = getAllFilesInDir(filesDir);\n for (const localFile of localFiles) {\n if (!remoteFilePaths.has(localFile)) {\n // File exists locally but not on remote\n const syncInfo = syncState.files[localFile];\n if (syncInfo) {\n // Was synced before, now deleted on remote - delete locally\n const localPath = path.join(filesDir, localFile);\n fs.unlinkSync(localPath);\n result.deleted.push(localFile);\n delete syncState.files[localFile];\n }\n // If no sync info, it's a new local file - don't delete\n }\n }\n\n syncState.lastSync = new Date().toISOString();\n this.saveSyncState(syncState);\n\n return result;\n }\n\n async push(): Promise<{\n uploaded: string[];\n updated: string[];\n deleted: string[];\n unchanged: string[];\n }> {\n const filesDir = getTenantFilesDir(this.tenant);\n const syncState = this.loadSyncState();\n\n const result = {\n uploaded: [] as string[],\n updated: [] as string[],\n deleted: [] as string[],\n unchanged: [] as string[],\n };\n\n // Get current remote files\n const remoteFiles = await this.farseerClient.listFiles();\n const remoteFileMap = new Map<string, RemoteFile>();\n for (const file of remoteFiles) {\n remoteFileMap.set(file.path, file);\n }\n\n // Get local files\n const localFiles = getAllFilesInDir(filesDir);\n\n // Upload/update local files\n for (const localFile of localFiles) {\n const localPath = path.join(filesDir, localFile);\n const content = fs.readFileSync(localPath);\n const localHash = calculateHash(content);\n\n const remoteFile = remoteFileMap.get(localFile);\n\n if (remoteFile) {\n // File exists on remote - check if update needed\n const remoteContent = await this.farseerClient.getFileContentAsBuffer(remoteFile.reference);\n const remoteHash = calculateHash(remoteContent);\n\n if (localHash !== remoteHash) {\n await this.farseerClient.updateFile(remoteFile.reference, content, path.basename(localFile));\n result.updated.push(localFile);\n } else {\n result.unchanged.push(localFile);\n }\n } else {\n // File doesn't exist on remote - create it\n const pathParts = localFile.split('/');\n const fileName = pathParts.pop()!;\n const folderPath = ['Files', ...pathParts];\n\n await this.farseerClient.createFile(content, fileName, folderPath);\n result.uploaded.push(localFile);\n }\n\n // Update sync state\n syncState.files[localFile] = {\n localHash: localHash,\n remoteHash: localHash,\n };\n }\n\n // Delete files on remote that were deleted locally\n const localFileSet = new Set(localFiles);\n for (const [remotePath, remoteFile] of remoteFileMap) {\n if (!localFileSet.has(remotePath)) {\n const syncInfo = syncState.files[remotePath];\n if (syncInfo) {\n // Was synced before, now deleted locally - delete on remote\n await this.farseerClient.deleteFile(remoteFile.reference);\n result.deleted.push(remotePath);\n delete syncState.files[remotePath];\n }\n }\n }\n\n syncState.lastSync = new Date().toISOString();\n this.saveSyncState(syncState);\n\n return result;\n }\n\n async checkRemoteChanges(): Promise<boolean> {\n const syncState = this.loadSyncState();\n let remoteFiles = await this.farseerClient.listFiles();\n\n // Filter to scripts only (same as pull/status) to avoid false positives\n // from non-script files like .json, .png, etc.\n remoteFiles = remoteFiles.filter(f =>\n DEFAULT_SCRIPT_EXTENSIONS.some(ext => f.name.toLowerCase().endsWith(ext))\n );\n\n // Fetch file contents in parallel to check for changes\n const fileContents = await fetchWithConcurrency(\n remoteFiles,\n async (file) => {\n const content = await this.farseerClient.getFileContentAsBuffer(file.reference);\n return { path: file.path, hash: calculateHash(content) };\n },\n 15\n );\n\n for (const { path: filePath, hash: remoteHash } of fileContents) {\n const syncInfo = syncState.files[filePath];\n\n if (!syncInfo || syncInfo.remoteHash !== remoteHash) {\n return true; // Remote has changes\n }\n }\n\n // Check for deleted files on remote (only for script files in sync state)\n for (const filePath of Object.keys(syncState.files)) {\n // Only check script files\n const isScript = DEFAULT_SCRIPT_EXTENSIONS.some(ext => filePath.toLowerCase().endsWith(ext));\n if (!isScript) continue;\n\n const exists = remoteFiles.some(f => f.path === filePath);\n if (!exists && syncState.files[filePath].remoteHash) {\n return true; // File was deleted on remote\n }\n }\n\n return false;\n }\n\n async getFileDiff(filePath: string): Promise<{\n localContent: string | null;\n remoteContent: string | null;\n }> {\n const filesDir = getTenantFilesDir(this.tenant);\n const localPath = path.join(filesDir, filePath);\n\n let localContent: string | null = null;\n let remoteContent: string | null = null;\n\n if (fs.existsSync(localPath)) {\n // For diff, read as text (diff only makes sense for text files)\n localContent = fs.readFileSync(localPath, 'utf-8');\n }\n\n const remoteFile = await this.farseerClient.getFileByPath(filePath);\n if (remoteFile) {\n remoteContent = await this.farseerClient.getFileContent(remoteFile.reference);\n }\n\n return { localContent, remoteContent };\n }\n\n /**\n * Batch fetch metadata for multiple files in parallel.\n * Fetches metadata in batches of 10 to avoid overwhelming the API.\n * Non-critical operation - failures are silently ignored.\n */\n private async fetchMetadataForFiles(files: RemoteFile[]): Promise<RemoteFile[]> {\n const batchSize = 10;\n\n for (let i = 0; i < files.length; i += batchSize) {\n const batch = files.slice(i, i + batchSize);\n\n await Promise.all(batch.map(async (file) => {\n try {\n const metadata = await this.farseerClient.getFileMetadata(file.reference);\n if (metadata) {\n file.metadata = metadata;\n }\n } catch {\n // Silently ignore metadata fetch errors - non-critical\n }\n }));\n }\n\n return files;\n }\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
|
+
"names": ["path"]
|
|
7
7
|
}
|
package/dist/utils/helpers.js
CHANGED
|
@@ -30,6 +30,7 @@ __export(helpers_exports, {
|
|
|
30
30
|
appendToAuditLog: () => appendToAuditLog,
|
|
31
31
|
calculateHash: () => calculateHash,
|
|
32
32
|
ensureDirectoryExists: () => ensureDirectoryExists,
|
|
33
|
+
fetchWithConcurrency: () => fetchWithConcurrency,
|
|
33
34
|
formatDate: () => formatDate,
|
|
34
35
|
formatFileSize: () => formatFileSize,
|
|
35
36
|
getAllFiles: () => getAllFiles,
|
|
@@ -360,6 +361,17 @@ function stripCredentialsInDir(srcDir) {
|
|
|
360
361
|
}
|
|
361
362
|
return count;
|
|
362
363
|
}
|
|
364
|
+
async function fetchWithConcurrency(items, fetcher, concurrency = 15) {
|
|
365
|
+
const results = [];
|
|
366
|
+
for (let i = 0; i < items.length; i += concurrency) {
|
|
367
|
+
const chunk = items.slice(i, i + concurrency);
|
|
368
|
+
const chunkResults = await Promise.all(
|
|
369
|
+
chunk.map((item, chunkIndex) => fetcher(item, i + chunkIndex))
|
|
370
|
+
);
|
|
371
|
+
results.push(...chunkResults);
|
|
372
|
+
}
|
|
373
|
+
return results;
|
|
374
|
+
}
|
|
363
375
|
function appendToAuditLog(tenant, entries) {
|
|
364
376
|
if (entries.length === 0) {
|
|
365
377
|
return;
|
|
@@ -380,6 +392,7 @@ function appendToAuditLog(tenant, entries) {
|
|
|
380
392
|
appendToAuditLog,
|
|
381
393
|
calculateHash,
|
|
382
394
|
ensureDirectoryExists,
|
|
395
|
+
fetchWithConcurrency,
|
|
383
396
|
formatDate,
|
|
384
397
|
formatFileSize,
|
|
385
398
|
getAllFiles,
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../src/utils/helpers.ts"],
|
|
4
|
-
"sourcesContent": ["import * as crypto from 'crypto';\nimport * as path from 'path';\nimport * as fs from 'fs';\nimport { getCurrentTenant, getCurrentCheckout, getCurrentOrganisation, getTenantOrg, saveTenantOrgMapping } from '../services/configService';\nimport { logger } from './logger';\n\nexport function calculateHash(content: string | Buffer): string {\n return crypto.createHash('md5').update(content).digest('hex');\n}\n\nexport function getAppsDir(): string {\n // Use current working directory - files are saved where user runs the command\n return path.join(process.cwd(), 'farseer');\n}\n\nexport function getTenantDir(tenant: string): string {\n return path.join(getAppsDir(), tenant);\n}\n\nexport function getTenantFilesDir(tenant: string): string {\n return path.join(getTenantDir(tenant), 'files');\n}\n\n// Legacy - for backwards compatibility with run command\nexport function getTenantSrcDir(tenant: string): string {\n return path.join(getTenantFilesDir(tenant), 'Scripts');\n}\n\nexport function getSyncFilePath(tenant: string): string {\n return path.join(getTenantDir(tenant), '.farseer-sync.json');\n}\n\nexport function getAuditLogPath(tenant: string): string {\n return path.join(getTenantDir(tenant), '.farseer-audit.log');\n}\n\nexport function getTenantAppsDir(tenant: string): string {\n return path.join(getTenantDir(tenant), 'apps');\n}\n\nexport function ensureDirectoryExists(dirPath: string): void {\n if (!fs.existsSync(dirPath)) {\n fs.mkdirSync(dirPath, { recursive: true });\n }\n}\n\n/**\n * Detect if we're running inside the remotejobs repository.\n * Used to enable/disable git operations automatically.\n *\n * @returns true if in remotejobs repo, false if standalone\n */\nexport function isInRemotejobsRepo(): boolean {\n try {\n // Get the apps directory (e.g., /path/to/remotejobs/apps)\n const appsDir = getAppsDir();\n const repoRoot = path.dirname(appsDir); // /path/to/remotejobs\n\n // Check for remotejobs signature: cli/ and docs/ folders exist\n const hasCli = fs.existsSync(path.join(repoRoot, 'cli'));\n const hasDocs = fs.existsSync(path.join(repoRoot, 'docs'));\n\n // Both must exist to confirm we're in remotejobs\n return hasCli && hasDocs;\n } catch {\n // If anything fails (path resolution, fs errors), assume standalone\n return false;\n }\n}\n\nexport function getRelativePath(fullPath: string, basePath: string): string {\n return path.relative(basePath, fullPath);\n}\n\nexport function getAllFiles(dirPath: string, basePath?: string): string[] {\n if (!fs.existsSync(dirPath)) {\n return [];\n }\n\n const base = basePath || dirPath;\n const files: string[] = [];\n\n const items = fs.readdirSync(dirPath, { withFileTypes: true });\n\n for (const item of items) {\n const fullPath = path.join(dirPath, item.name);\n\n if (item.isDirectory()) {\n files.push(...getAllFiles(fullPath, base));\n } else if (item.isFile() && isScriptFile(item.name)) {\n files.push(getRelativePath(fullPath, base));\n }\n }\n\n return files;\n}\n\n// Get all files in directory (not just scripts)\nexport function getAllFilesInDir(dirPath: string, basePath?: string): string[] {\n if (!fs.existsSync(dirPath)) {\n return [];\n }\n\n const base = basePath || dirPath;\n const files: string[] = [];\n\n const items = fs.readdirSync(dirPath, { withFileTypes: true });\n\n for (const item of items) {\n const fullPath = path.join(dirPath, item.name);\n\n if (item.isDirectory()) {\n files.push(...getAllFilesInDir(fullPath, base));\n } else if (item.isFile()) {\n files.push(getRelativePath(fullPath, base));\n }\n }\n\n return files;\n}\n\nexport function isScriptFile(filename: string): boolean {\n const ext = path.extname(filename).toLowerCase();\n return ['.ts', '.js', '.mjs', '.cjs'].includes(ext);\n}\n\nexport function isTextFile(filename: string): boolean {\n const ext = path.extname(filename).toLowerCase().slice(1); // remove leading dot\n return ['ts', 'js', 'mjs', 'cjs', 'json', 'txt', 'md', 'csv', 'xml', 'html', 'css', 'yaml', 'yml'].includes(ext);\n}\n\nexport function formatFileSize(bytes: number): string {\n if (bytes < 1024) return `${bytes} B`;\n if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;\n return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;\n}\n\nexport function formatDate(date: Date | string): string {\n const d = typeof date === 'string' ? new Date(date) : date;\n return d.toLocaleString();\n}\n\nexport function getCredentialsHint(tenant: string): string {\n return `Use: farseer login\n Or: farseer config set ${tenant} --api-key <key> --org <organisation>`;\n}\n\n/**\n * Resolve tenant from argument or checkout.\n * If tenant is provided, use it. Otherwise, use checked-out tenant.\n * Returns null if no tenant is available.\n */\nexport interface ResolvedCheckout {\n organisation: string;\n tenant: string;\n}\n\n/**\n * Resolve organisation and tenant from argument or checkout.\n * When only tenant is provided, assumes organisation = tenant.\n */\nexport function resolveOrgAndTenant(tenantArg?: string): ResolvedCheckout | null {\n if (tenantArg) {\n // When tenant is specified as arg, assume org = tenant\n // (User can use checkout for different org/tenant)\n return { organisation: tenantArg, tenant: tenantArg };\n }\n\n const checkout = getCurrentCheckout();\n if (checkout) {\n if (checkout.organisation === checkout.tenant) {\n logger.dim(`Using checked-out tenant: ${checkout.tenant}`);\n } else {\n logger.dim(`Using checked-out tenant: ${checkout.tenant} (org: ${checkout.organisation})`);\n }\n return checkout;\n }\n\n return null;\n}\n\nexport function resolveTenant(tenantArg?: string): string | null {\n const resolved = resolveOrgAndTenant(tenantArg);\n return resolved?.tenant || null;\n}\n\n/**\n * Resolve tenant or exit with error if not available.\n */\nexport function resolveTenantOrExit(tenantArg?: string): string {\n const tenant = resolveTenant(tenantArg);\n if (!tenant) {\n logger.error('No tenant specified.');\n logger.dim('Either specify a tenant: farseer <command> <tenant>');\n logger.dim('Or checkout a tenant: farseer checkout <tenant>');\n process.exit(1);\n }\n return tenant;\n}\n\n/**\n * Resolve organisation and tenant with support for org-tenant mapping.\n *\n * Usage patterns:\n * - <org> <tenant>: Explicit org and tenant, saves mapping for future use\n * - <tenant>: Look up org from saved mapping or use checkout\n * - (no args): Use current checkout\n *\n * @param arg1 - Organisation (if 2 args) or tenant (if 1 arg)\n * @param arg2 - Tenant (if 2 args)\n * @returns ResolvedCheckout or exits with error\n */\nexport function resolveTenantWithOrgMapping(arg1?: string, arg2?: string): ResolvedCheckout {\n // Case 1: Two arguments - <org> <tenant>\n if (arg1 && arg2) {\n const org = arg1;\n const tenant = arg2;\n\n // Save mapping for future use\n saveTenantOrgMapping(tenant, org);\n\n if (org === tenant) {\n logger.dim(`Using tenant: ${tenant}`);\n } else {\n logger.dim(`Using tenant: ${tenant} (org: ${org})`);\n }\n\n return { organisation: org, tenant };\n }\n\n // Case 2: One argument - <tenant>\n if (arg1) {\n const tenant = arg1;\n\n // Try to get org from saved mapping\n const savedOrg = getTenantOrg(tenant);\n if (savedOrg) {\n if (savedOrg === tenant) {\n logger.dim(`Using tenant: ${tenant} (from saved mapping)`);\n } else {\n logger.dim(`Using tenant: ${tenant} (org: ${savedOrg}, from saved mapping)`);\n }\n return { organisation: savedOrg, tenant };\n }\n\n // No saved mapping - show error with helpful message\n logger.error(`Organisation unknown for tenant \"${tenant}\".`);\n logger.dim('Please specify both organisation and tenant:');\n logger.dim(` farseer <command> <organisation> <tenant>`);\n logger.dim(` Example: farseer pull jgl tt-hotels-hr-dev`);\n logger.dim('');\n logger.dim('Or checkout the tenant first:');\n logger.dim(` farseer checkout <organisation> <tenant>`);\n process.exit(1);\n }\n\n // Case 3: No arguments - use current checkout\n const checkout = getCurrentCheckout();\n if (checkout) {\n if (checkout.organisation === checkout.tenant) {\n logger.dim(`Using checked-out tenant: ${checkout.tenant}`);\n } else {\n logger.dim(`Using checked-out tenant: ${checkout.tenant} (org: ${checkout.organisation})`);\n }\n return checkout;\n }\n\n // No checkout either\n logger.error('No tenant specified.');\n logger.dim('Either specify organisation and tenant: farseer <command> <org> <tenant>');\n logger.dim('Or checkout a tenant: farseer checkout <org> <tenant>');\n process.exit(1);\n}\n\n/**\n * Resolve organisation and tenant or exit with error.\n */\nexport function resolveOrgAndTenantOrExit(tenantArg?: string): ResolvedCheckout {\n const resolved = resolveOrgAndTenant(tenantArg);\n if (!resolved) {\n logger.error('No tenant specified.');\n logger.dim('Either specify a tenant: farseer <command> <tenant>');\n logger.dim('Or checkout a tenant: farseer checkout <tenant>');\n process.exit(1);\n }\n return resolved;\n}\n\n/**\n * Check if a string is a known tenant (has credentials configured).\n */\nexport function isKnownTenant(name: string): boolean {\n // Import here to avoid circular dependency\n const { getCredential } = require('../services/configService');\n return !!getCredential(name);\n}\n\nexport interface ParsedAppArgs {\n organisation: string;\n tenant: string;\n appName: string;\n}\n\n/**\n * Parse variadic arguments to extract tenant and app name.\n * If first arg is a known tenant, use it as tenant and rest as app name.\n * Otherwise, use checked-out tenant and all args as app name.\n *\n * @param args Array of arguments (from variadic command)\n * @returns { organisation: string, tenant: string, appName: string }\n */\nexport function parseAppArgs(args: string[]): ParsedAppArgs {\n if (args.length === 0) {\n logger.error('App name is required.');\n process.exit(1);\n }\n\n // Check if first argument is a known tenant\n if (args.length >= 2 && isKnownTenant(args[0])) {\n // First arg is tenant, rest is app name\n // Assume organisation = tenant when specified via command line\n return {\n organisation: args[0],\n tenant: args[0],\n appName: args.slice(1).join(' ')\n };\n }\n\n // Use checked-out tenant, all args are app name\n const checkout = getCurrentCheckout();\n if (checkout) {\n if (checkout.organisation === checkout.tenant) {\n logger.dim(`Using checked-out tenant: ${checkout.tenant}`);\n } else {\n logger.dim(`Using checked-out tenant: ${checkout.tenant} (org: ${checkout.organisation})`);\n }\n return {\n organisation: checkout.organisation,\n tenant: checkout.tenant,\n appName: args.join(' ')\n };\n }\n\n // No checked-out tenant and first arg isn't a known tenant\n // Assume first arg is tenant (will fail later if invalid)\n if (args.length >= 2) {\n return {\n organisation: args[0],\n tenant: args[0],\n appName: args.slice(1).join(' ')\n };\n }\n\n // Only one arg and no checkout - could be tenant or app name\n logger.error('No tenant specified.');\n logger.dim('Either specify a tenant: farseer app show <tenant> <app-name>');\n logger.dim('Or checkout a tenant: farseer checkout <tenant>');\n process.exit(1);\n}\n\n// Credential injection/stripping for local development\n// Matches: const farseerClient = new farseer.FarseerClient();\n// and: const fClient = new farseer.FarseerClient();\n\ninterface CredentialConfig {\n basePath: string;\n tenantId: string;\n apiKey: string;\n}\n\n// Pattern to match empty FarseerClient() constructor\nconst EMPTY_CLIENT_PATTERNS = [\n /const farseerClient = new farseer\\.FarseerClient\\(\\s*\\);/g,\n /const fClient = new farseer\\.FarseerClient\\(\\s*\\);/g,\n];\n\n// Pattern to match FarseerClient with injected credentials (multiline)\nconst INJECTED_CLIENT_PATTERN = /const (farseerClient|fClient) = new farseer\\.FarseerClient\\(\\{[\\s\\S]*?basePath:[\\s\\S]*?headers:[\\s\\S]*?'X-TENANT-ID':[\\s\\S]*?'X-API-KEY':[\\s\\S]*?\\}\\);/g;\n\nexport function injectCredentials(code: string, credential: CredentialConfig): string {\n const clientConfig = `{\n basePath: '${credential.basePath}',\n headers: {\n 'X-TENANT-ID': '${credential.tenantId}',\n 'X-API-KEY': '${credential.apiKey}',\n }\n }`;\n\n let modified = code;\n\n // Pattern 1: const farseerClient = new farseer.FarseerClient();\n modified = modified.replace(\n /const farseerClient = new farseer\\.FarseerClient\\(\\s*\\);/g,\n `const farseerClient = new farseer.FarseerClient(${clientConfig});`\n );\n\n // Pattern 2: const fClient = new farseer.FarseerClient();\n modified = modified.replace(\n /const fClient = new farseer\\.FarseerClient\\(\\s*\\);/g,\n `const fClient = new farseer.FarseerClient(${clientConfig});`\n );\n\n return modified;\n}\n\nexport function stripCredentials(code: string): string {\n // Replace injected credentials back to empty constructor\n let modified = code;\n\n // Match farseerClient with credentials\n modified = modified.replace(\n /const farseerClient = new farseer\\.FarseerClient\\(\\{[\\s\\S]*?basePath:[\\s\\S]*?headers:[\\s\\S]*?'X-TENANT-ID':[\\s\\S]*?'X-API-KEY':[\\s\\S]*?\\}\\);/g,\n 'const farseerClient = new farseer.FarseerClient();'\n );\n\n // Match fClient with credentials\n modified = modified.replace(\n /const fClient = new farseer\\.FarseerClient\\(\\{[\\s\\S]*?basePath:[\\s\\S]*?headers:[\\s\\S]*?'X-TENANT-ID':[\\s\\S]*?'X-API-KEY':[\\s\\S]*?\\}\\);/g,\n 'const fClient = new farseer.FarseerClient();'\n );\n\n return modified;\n}\n\nexport function hasInjectedCredentials(code: string): boolean {\n return INJECTED_CLIENT_PATTERN.test(code);\n}\n\nexport function hasEmptyFarseerClient(code: string): boolean {\n return EMPTY_CLIENT_PATTERNS.some(pattern => pattern.test(code));\n}\n\n// Process all script files in a directory\nexport function injectCredentialsInDir(srcDir: string, credential: CredentialConfig): number {\n let count = 0;\n const files = getAllFiles(srcDir);\n\n for (const file of files) {\n const filePath = path.join(srcDir, file);\n const content = fs.readFileSync(filePath, 'utf-8');\n const modified = injectCredentials(content, credential);\n\n if (modified !== content) {\n fs.writeFileSync(filePath, modified);\n count++;\n }\n }\n\n return count;\n}\n\nexport function stripCredentialsInDir(srcDir: string): number {\n let count = 0;\n const files = getAllFiles(srcDir);\n\n for (const file of files) {\n const filePath = path.join(srcDir, file);\n const content = fs.readFileSync(filePath, 'utf-8');\n const modified = stripCredentials(content);\n\n if (modified !== content) {\n fs.writeFileSync(filePath, modified);\n count++;\n }\n }\n\n return count;\n}\n\n// Audit log helpers\nexport interface AuditLogEntry {\n operation: 'pulled' | 'pushed' | 'deleted';\n filePath: string;\n uploadTime?: string;\n uploaderEmail?: string;\n uploaderName?: string;\n}\n\nexport function appendToAuditLog(tenant: string, entries: AuditLogEntry[]): void {\n if (entries.length === 0) {\n return;\n }\n\n const logPath = getAuditLogPath(tenant);\n const timestamp = new Date().toISOString();\n\n let logContent = '';\n for (const entry of entries) {\n const uploaderInfo = entry.uploaderName\n ? `${entry.uploaderName} (${entry.uploaderEmail})`\n : entry.uploaderEmail || 'Unknown';\n\n const uploadedAt = entry.uploadTime\n ? new Date(entry.uploadTime).toLocaleString()\n : 'Unknown';\n\n logContent += `${timestamp} | ${entry.operation} | ${entry.filePath} | Modified by: ${uploaderInfo} | Uploaded: ${uploadedAt}\\n`;\n }\n\n fs.appendFileSync(logPath, logContent, 'utf-8');\n}\n"],
|
|
5
|
-
"mappings": ";;;;;;;;;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,aAAwB;AACxB,WAAsB;AACtB,SAAoB;AACpB,2BAAiH;AACjH,oBAAuB;AAEhB,SAAS,cAAc,SAAkC;AAC5D,SAAO,OAAO,WAAW,KAAK,EAAE,OAAO,OAAO,EAAE,OAAO,KAAK;AAChE;AAEO,SAAS,aAAqB;AAEjC,SAAO,KAAK,KAAK,QAAQ,IAAI,GAAG,SAAS;AAC7C;AAEO,SAAS,aAAa,QAAwB;AACjD,SAAO,KAAK,KAAK,WAAW,GAAG,MAAM;AACzC;AAEO,SAAS,kBAAkB,QAAwB;AACtD,SAAO,KAAK,KAAK,aAAa,MAAM,GAAG,OAAO;AAClD;AAGO,SAAS,gBAAgB,QAAwB;AACpD,SAAO,KAAK,KAAK,kBAAkB,MAAM,GAAG,SAAS;AACzD;AAEO,SAAS,gBAAgB,QAAwB;AACpD,SAAO,KAAK,KAAK,aAAa,MAAM,GAAG,oBAAoB;AAC/D;AAEO,SAAS,gBAAgB,QAAwB;AACpD,SAAO,KAAK,KAAK,aAAa,MAAM,GAAG,oBAAoB;AAC/D;AAEO,SAAS,iBAAiB,QAAwB;AACrD,SAAO,KAAK,KAAK,aAAa,MAAM,GAAG,MAAM;AACjD;AAEO,SAAS,sBAAsB,SAAuB;AACzD,MAAI,CAAC,GAAG,WAAW,OAAO,GAAG;AACzB,OAAG,UAAU,SAAS,EAAE,WAAW,KAAK,CAAC;AAAA,EAC7C;AACJ;AAQO,SAAS,qBAA8B;AAC1C,MAAI;AAEA,UAAM,UAAU,WAAW;AAC3B,UAAM,WAAW,KAAK,QAAQ,OAAO;AAGrC,UAAM,SAAS,GAAG,WAAW,KAAK,KAAK,UAAU,KAAK,CAAC;AACvD,UAAM,UAAU,GAAG,WAAW,KAAK,KAAK,UAAU,MAAM,CAAC;AAGzD,WAAO,UAAU;AAAA,EACrB,QAAQ;AAEJ,WAAO;AAAA,EACX;AACJ;AAEO,SAAS,gBAAgB,UAAkB,UAA0B;AACxE,SAAO,KAAK,SAAS,UAAU,QAAQ;AAC3C;AAEO,SAAS,YAAY,SAAiB,UAA6B;AACtE,MAAI,CAAC,GAAG,WAAW,OAAO,GAAG;AACzB,WAAO,CAAC;AAAA,EACZ;AAEA,QAAM,OAAO,YAAY;AACzB,QAAM,QAAkB,CAAC;AAEzB,QAAM,QAAQ,GAAG,YAAY,SAAS,EAAE,eAAe,KAAK,CAAC;AAE7D,aAAW,QAAQ,OAAO;AACtB,UAAM,WAAW,KAAK,KAAK,SAAS,KAAK,IAAI;AAE7C,QAAI,KAAK,YAAY,GAAG;AACpB,YAAM,KAAK,GAAG,YAAY,UAAU,IAAI,CAAC;AAAA,IAC7C,WAAW,KAAK,OAAO,KAAK,aAAa,KAAK,IAAI,GAAG;AACjD,YAAM,KAAK,gBAAgB,UAAU,IAAI,CAAC;AAAA,IAC9C;AAAA,EACJ;AAEA,SAAO;AACX;AAGO,SAAS,iBAAiB,SAAiB,UAA6B;AAC3E,MAAI,CAAC,GAAG,WAAW,OAAO,GAAG;AACzB,WAAO,CAAC;AAAA,EACZ;AAEA,QAAM,OAAO,YAAY;AACzB,QAAM,QAAkB,CAAC;AAEzB,QAAM,QAAQ,GAAG,YAAY,SAAS,EAAE,eAAe,KAAK,CAAC;AAE7D,aAAW,QAAQ,OAAO;AACtB,UAAM,WAAW,KAAK,KAAK,SAAS,KAAK,IAAI;AAE7C,QAAI,KAAK,YAAY,GAAG;AACpB,YAAM,KAAK,GAAG,iBAAiB,UAAU,IAAI,CAAC;AAAA,IAClD,WAAW,KAAK,OAAO,GAAG;AACtB,YAAM,KAAK,gBAAgB,UAAU,IAAI,CAAC;AAAA,IAC9C;AAAA,EACJ;AAEA,SAAO;AACX;AAEO,SAAS,aAAa,UAA2B;AACpD,QAAM,MAAM,KAAK,QAAQ,QAAQ,EAAE,YAAY;AAC/C,SAAO,CAAC,OAAO,OAAO,QAAQ,MAAM,EAAE,SAAS,GAAG;AACtD;AAEO,SAAS,WAAW,UAA2B;AAClD,QAAM,MAAM,KAAK,QAAQ,QAAQ,EAAE,YAAY,EAAE,MAAM,CAAC;AACxD,SAAO,CAAC,MAAM,MAAM,OAAO,OAAO,QAAQ,OAAO,MAAM,OAAO,OAAO,QAAQ,OAAO,QAAQ,KAAK,EAAE,SAAS,GAAG;AACnH;AAEO,SAAS,eAAe,OAAuB;AAClD,MAAI,QAAQ,KAAM,QAAO,GAAG,KAAK;AACjC,MAAI,QAAQ,OAAO,KAAM,QAAO,IAAI,QAAQ,MAAM,QAAQ,CAAC,CAAC;AAC5D,SAAO,IAAI,SAAS,OAAO,OAAO,QAAQ,CAAC,CAAC;AAChD;AAEO,SAAS,WAAW,MAA6B;AACpD,QAAM,IAAI,OAAO,SAAS,WAAW,IAAI,KAAK,IAAI,IAAI;AACtD,SAAO,EAAE,eAAe;AAC5B;AAEO,SAAS,mBAAmB,QAAwB;AACvD,SAAO;AAAA,4BACiB,MAAM;AAClC;AAgBO,SAAS,oBAAoB,WAA6C;AAC7E,MAAI,WAAW;AAGX,WAAO,EAAE,cAAc,WAAW,QAAQ,UAAU;AAAA,EACxD;AAEA,QAAM,eAAW,yCAAmB;AACpC,MAAI,UAAU;AACV,QAAI,SAAS,iBAAiB,SAAS,QAAQ;AAC3C,2BAAO,IAAI,6BAA6B,SAAS,MAAM,EAAE;AAAA,IAC7D,OAAO;AACH,2BAAO,IAAI,6BAA6B,SAAS,MAAM,UAAU,SAAS,YAAY,GAAG;AAAA,IAC7F;AACA,WAAO;AAAA,EACX;AAEA,SAAO;AACX;AAEO,SAAS,cAAc,WAAmC;AAC7D,QAAM,WAAW,oBAAoB,SAAS;AAC9C,SAAO,UAAU,UAAU;AAC/B;AAKO,SAAS,oBAAoB,WAA4B;AAC5D,QAAM,SAAS,cAAc,SAAS;AACtC,MAAI,CAAC,QAAQ;AACT,yBAAO,MAAM,sBAAsB;AACnC,yBAAO,IAAI,qDAAqD;AAChE,yBAAO,IAAI,iDAAiD;AAC5D,YAAQ,KAAK,CAAC;AAAA,EAClB;AACA,SAAO;AACX;AAcO,SAAS,4BAA4B,MAAe,MAAiC;AAExF,MAAI,QAAQ,MAAM;AACd,UAAM,MAAM;AACZ,UAAM,SAAS;AAGf,mDAAqB,QAAQ,GAAG;AAEhC,QAAI,QAAQ,QAAQ;AAChB,2BAAO,IAAI,iBAAiB,MAAM,EAAE;AAAA,IACxC,OAAO;AACH,2BAAO,IAAI,iBAAiB,MAAM,UAAU,GAAG,GAAG;AAAA,IACtD;AAEA,WAAO,EAAE,cAAc,KAAK,OAAO;AAAA,EACvC;AAGA,MAAI,MAAM;AACN,UAAM,SAAS;AAGf,UAAM,eAAW,mCAAa,MAAM;AACpC,QAAI,UAAU;AACV,UAAI,aAAa,QAAQ;AACrB,6BAAO,IAAI,iBAAiB,MAAM,uBAAuB;AAAA,MAC7D,OAAO;AACH,6BAAO,IAAI,iBAAiB,MAAM,UAAU,QAAQ,uBAAuB;AAAA,MAC/E;AACA,aAAO,EAAE,cAAc,UAAU,OAAO;AAAA,IAC5C;AAGA,yBAAO,MAAM,oCAAoC,MAAM,IAAI;AAC3D,yBAAO,IAAI,8CAA8C;AACzD,yBAAO,IAAI,6CAA6C;AACxD,yBAAO,IAAI,8CAA8C;AACzD,yBAAO,IAAI,EAAE;AACb,yBAAO,IAAI,+BAA+B;AAC1C,yBAAO,IAAI,4CAA4C;AACvD,YAAQ,KAAK,CAAC;AAAA,EAClB;AAGA,QAAM,eAAW,yCAAmB;AACpC,MAAI,UAAU;AACV,QAAI,SAAS,iBAAiB,SAAS,QAAQ;AAC3C,2BAAO,IAAI,6BAA6B,SAAS,MAAM,EAAE;AAAA,IAC7D,OAAO;AACH,2BAAO,IAAI,6BAA6B,SAAS,MAAM,UAAU,SAAS,YAAY,GAAG;AAAA,IAC7F;AACA,WAAO;AAAA,EACX;AAGA,uBAAO,MAAM,sBAAsB;AACnC,uBAAO,IAAI,0EAA0E;AACrF,uBAAO,IAAI,uDAAuD;AAClE,UAAQ,KAAK,CAAC;AAClB;AAKO,SAAS,0BAA0B,WAAsC;AAC5E,QAAM,WAAW,oBAAoB,SAAS;AAC9C,MAAI,CAAC,UAAU;AACX,yBAAO,MAAM,sBAAsB;AACnC,yBAAO,IAAI,qDAAqD;AAChE,yBAAO,IAAI,iDAAiD;AAC5D,YAAQ,KAAK,CAAC;AAAA,EAClB;AACA,SAAO;AACX;AAKO,SAAS,cAAc,MAAuB;AAEjD,QAAM,EAAE,cAAc,IAAI,QAAQ,2BAA2B;AAC7D,SAAO,CAAC,CAAC,cAAc,IAAI;AAC/B;AAgBO,SAAS,aAAa,MAA+B;AACxD,MAAI,KAAK,WAAW,GAAG;AACnB,yBAAO,MAAM,uBAAuB;AACpC,YAAQ,KAAK,CAAC;AAAA,EAClB;AAGA,MAAI,KAAK,UAAU,KAAK,cAAc,KAAK,CAAC,CAAC,GAAG;AAG5C,WAAO;AAAA,MACH,cAAc,KAAK,CAAC;AAAA,MACpB,QAAQ,KAAK,CAAC;AAAA,MACd,SAAS,KAAK,MAAM,CAAC,EAAE,KAAK,GAAG;AAAA,IACnC;AAAA,EACJ;AAGA,QAAM,eAAW,yCAAmB;AACpC,MAAI,UAAU;AACV,QAAI,SAAS,iBAAiB,SAAS,QAAQ;AAC3C,2BAAO,IAAI,6BAA6B,SAAS,MAAM,EAAE;AAAA,IAC7D,OAAO;AACH,2BAAO,IAAI,6BAA6B,SAAS,MAAM,UAAU,SAAS,YAAY,GAAG;AAAA,IAC7F;AACA,WAAO;AAAA,MACH,cAAc,SAAS;AAAA,MACvB,QAAQ,SAAS;AAAA,MACjB,SAAS,KAAK,KAAK,GAAG;AAAA,IAC1B;AAAA,EACJ;AAIA,MAAI,KAAK,UAAU,GAAG;AAClB,WAAO;AAAA,MACH,cAAc,KAAK,CAAC;AAAA,MACpB,QAAQ,KAAK,CAAC;AAAA,MACd,SAAS,KAAK,MAAM,CAAC,EAAE,KAAK,GAAG;AAAA,IACnC;AAAA,EACJ;AAGA,uBAAO,MAAM,sBAAsB;AACnC,uBAAO,IAAI,+DAA+D;AAC1E,uBAAO,IAAI,iDAAiD;AAC5D,UAAQ,KAAK,CAAC;AAClB;AAaA,MAAM,wBAAwB;AAAA,EAC1B;AAAA,EACA;AACJ;AAGA,MAAM,0BAA0B;AAEzB,SAAS,kBAAkB,MAAc,YAAsC;AAClF,QAAM,eAAe;AAAA,qBACJ,WAAW,QAAQ;AAAA;AAAA,8BAEV,WAAW,QAAQ;AAAA,4BACrB,WAAW,MAAM;AAAA;AAAA;AAIzC,MAAI,WAAW;AAGf,aAAW,SAAS;AAAA,IAChB;AAAA,IACA,mDAAmD,YAAY;AAAA,EACnE;AAGA,aAAW,SAAS;AAAA,IAChB;AAAA,IACA,6CAA6C,YAAY;AAAA,EAC7D;AAEA,SAAO;AACX;AAEO,SAAS,iBAAiB,MAAsB;AAEnD,MAAI,WAAW;AAGf,aAAW,SAAS;AAAA,IAChB;AAAA,IACA;AAAA,EACJ;AAGA,aAAW,SAAS;AAAA,IAChB;AAAA,IACA;AAAA,EACJ;AAEA,SAAO;AACX;AAEO,SAAS,uBAAuB,MAAuB;AAC1D,SAAO,wBAAwB,KAAK,IAAI;AAC5C;AAEO,SAAS,sBAAsB,MAAuB;AACzD,SAAO,sBAAsB,KAAK,aAAW,QAAQ,KAAK,IAAI,CAAC;AACnE;AAGO,SAAS,uBAAuB,QAAgB,YAAsC;AACzF,MAAI,QAAQ;AACZ,QAAM,QAAQ,YAAY,MAAM;AAEhC,aAAW,QAAQ,OAAO;AACtB,UAAM,WAAW,KAAK,KAAK,QAAQ,IAAI;AACvC,UAAM,UAAU,GAAG,aAAa,UAAU,OAAO;AACjD,UAAM,WAAW,kBAAkB,SAAS,UAAU;AAEtD,QAAI,aAAa,SAAS;AACtB,SAAG,cAAc,UAAU,QAAQ;AACnC;AAAA,IACJ;AAAA,EACJ;AAEA,SAAO;AACX;AAEO,SAAS,sBAAsB,QAAwB;AAC1D,MAAI,QAAQ;AACZ,QAAM,QAAQ,YAAY,MAAM;AAEhC,aAAW,QAAQ,OAAO;AACtB,UAAM,WAAW,KAAK,KAAK,QAAQ,IAAI;AACvC,UAAM,UAAU,GAAG,aAAa,UAAU,OAAO;AACjD,UAAM,WAAW,iBAAiB,OAAO;AAEzC,QAAI,aAAa,SAAS;AACtB,SAAG,cAAc,UAAU,QAAQ;AACnC;AAAA,IACJ;AAAA,EACJ;AAEA,SAAO;AACX;
|
|
4
|
+
"sourcesContent": ["import * as crypto from 'crypto';\nimport * as path from 'path';\nimport * as fs from 'fs';\nimport { getCurrentTenant, getCurrentCheckout, getCurrentOrganisation, getTenantOrg, saveTenantOrgMapping } from '../services/configService';\nimport { logger } from './logger';\n\nexport function calculateHash(content: string | Buffer): string {\n return crypto.createHash('md5').update(content).digest('hex');\n}\n\nexport function getAppsDir(): string {\n // Use current working directory - files are saved where user runs the command\n return path.join(process.cwd(), 'farseer');\n}\n\nexport function getTenantDir(tenant: string): string {\n return path.join(getAppsDir(), tenant);\n}\n\nexport function getTenantFilesDir(tenant: string): string {\n return path.join(getTenantDir(tenant), 'files');\n}\n\n// Legacy - for backwards compatibility with run command\nexport function getTenantSrcDir(tenant: string): string {\n return path.join(getTenantFilesDir(tenant), 'Scripts');\n}\n\nexport function getSyncFilePath(tenant: string): string {\n return path.join(getTenantDir(tenant), '.farseer-sync.json');\n}\n\nexport function getAuditLogPath(tenant: string): string {\n return path.join(getTenantDir(tenant), '.farseer-audit.log');\n}\n\nexport function getTenantAppsDir(tenant: string): string {\n return path.join(getTenantDir(tenant), 'apps');\n}\n\nexport function ensureDirectoryExists(dirPath: string): void {\n if (!fs.existsSync(dirPath)) {\n fs.mkdirSync(dirPath, { recursive: true });\n }\n}\n\n/**\n * Detect if we're running inside the remotejobs repository.\n * Used to enable/disable git operations automatically.\n *\n * @returns true if in remotejobs repo, false if standalone\n */\nexport function isInRemotejobsRepo(): boolean {\n try {\n // Get the apps directory (e.g., /path/to/remotejobs/apps)\n const appsDir = getAppsDir();\n const repoRoot = path.dirname(appsDir); // /path/to/remotejobs\n\n // Check for remotejobs signature: cli/ and docs/ folders exist\n const hasCli = fs.existsSync(path.join(repoRoot, 'cli'));\n const hasDocs = fs.existsSync(path.join(repoRoot, 'docs'));\n\n // Both must exist to confirm we're in remotejobs\n return hasCli && hasDocs;\n } catch {\n // If anything fails (path resolution, fs errors), assume standalone\n return false;\n }\n}\n\nexport function getRelativePath(fullPath: string, basePath: string): string {\n return path.relative(basePath, fullPath);\n}\n\nexport function getAllFiles(dirPath: string, basePath?: string): string[] {\n if (!fs.existsSync(dirPath)) {\n return [];\n }\n\n const base = basePath || dirPath;\n const files: string[] = [];\n\n const items = fs.readdirSync(dirPath, { withFileTypes: true });\n\n for (const item of items) {\n const fullPath = path.join(dirPath, item.name);\n\n if (item.isDirectory()) {\n files.push(...getAllFiles(fullPath, base));\n } else if (item.isFile() && isScriptFile(item.name)) {\n files.push(getRelativePath(fullPath, base));\n }\n }\n\n return files;\n}\n\n// Get all files in directory (not just scripts)\nexport function getAllFilesInDir(dirPath: string, basePath?: string): string[] {\n if (!fs.existsSync(dirPath)) {\n return [];\n }\n\n const base = basePath || dirPath;\n const files: string[] = [];\n\n const items = fs.readdirSync(dirPath, { withFileTypes: true });\n\n for (const item of items) {\n const fullPath = path.join(dirPath, item.name);\n\n if (item.isDirectory()) {\n files.push(...getAllFilesInDir(fullPath, base));\n } else if (item.isFile()) {\n files.push(getRelativePath(fullPath, base));\n }\n }\n\n return files;\n}\n\nexport function isScriptFile(filename: string): boolean {\n const ext = path.extname(filename).toLowerCase();\n return ['.ts', '.js', '.mjs', '.cjs'].includes(ext);\n}\n\nexport function isTextFile(filename: string): boolean {\n const ext = path.extname(filename).toLowerCase().slice(1); // remove leading dot\n return ['ts', 'js', 'mjs', 'cjs', 'json', 'txt', 'md', 'csv', 'xml', 'html', 'css', 'yaml', 'yml'].includes(ext);\n}\n\nexport function formatFileSize(bytes: number): string {\n if (bytes < 1024) return `${bytes} B`;\n if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;\n return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;\n}\n\nexport function formatDate(date: Date | string): string {\n const d = typeof date === 'string' ? new Date(date) : date;\n return d.toLocaleString();\n}\n\nexport function getCredentialsHint(tenant: string): string {\n return `Use: farseer login\n Or: farseer config set ${tenant} --api-key <key> --org <organisation>`;\n}\n\n/**\n * Resolve tenant from argument or checkout.\n * If tenant is provided, use it. Otherwise, use checked-out tenant.\n * Returns null if no tenant is available.\n */\nexport interface ResolvedCheckout {\n organisation: string;\n tenant: string;\n}\n\n/**\n * Resolve organisation and tenant from argument or checkout.\n * When only tenant is provided, assumes organisation = tenant.\n */\nexport function resolveOrgAndTenant(tenantArg?: string): ResolvedCheckout | null {\n if (tenantArg) {\n // When tenant is specified as arg, assume org = tenant\n // (User can use checkout for different org/tenant)\n return { organisation: tenantArg, tenant: tenantArg };\n }\n\n const checkout = getCurrentCheckout();\n if (checkout) {\n if (checkout.organisation === checkout.tenant) {\n logger.dim(`Using checked-out tenant: ${checkout.tenant}`);\n } else {\n logger.dim(`Using checked-out tenant: ${checkout.tenant} (org: ${checkout.organisation})`);\n }\n return checkout;\n }\n\n return null;\n}\n\nexport function resolveTenant(tenantArg?: string): string | null {\n const resolved = resolveOrgAndTenant(tenantArg);\n return resolved?.tenant || null;\n}\n\n/**\n * Resolve tenant or exit with error if not available.\n */\nexport function resolveTenantOrExit(tenantArg?: string): string {\n const tenant = resolveTenant(tenantArg);\n if (!tenant) {\n logger.error('No tenant specified.');\n logger.dim('Either specify a tenant: farseer <command> <tenant>');\n logger.dim('Or checkout a tenant: farseer checkout <tenant>');\n process.exit(1);\n }\n return tenant;\n}\n\n/**\n * Resolve organisation and tenant with support for org-tenant mapping.\n *\n * Usage patterns:\n * - <org> <tenant>: Explicit org and tenant, saves mapping for future use\n * - <tenant>: Look up org from saved mapping or use checkout\n * - (no args): Use current checkout\n *\n * @param arg1 - Organisation (if 2 args) or tenant (if 1 arg)\n * @param arg2 - Tenant (if 2 args)\n * @returns ResolvedCheckout or exits with error\n */\nexport function resolveTenantWithOrgMapping(arg1?: string, arg2?: string): ResolvedCheckout {\n // Case 1: Two arguments - <org> <tenant>\n if (arg1 && arg2) {\n const org = arg1;\n const tenant = arg2;\n\n // Save mapping for future use\n saveTenantOrgMapping(tenant, org);\n\n if (org === tenant) {\n logger.dim(`Using tenant: ${tenant}`);\n } else {\n logger.dim(`Using tenant: ${tenant} (org: ${org})`);\n }\n\n return { organisation: org, tenant };\n }\n\n // Case 2: One argument - <tenant>\n if (arg1) {\n const tenant = arg1;\n\n // Try to get org from saved mapping\n const savedOrg = getTenantOrg(tenant);\n if (savedOrg) {\n if (savedOrg === tenant) {\n logger.dim(`Using tenant: ${tenant} (from saved mapping)`);\n } else {\n logger.dim(`Using tenant: ${tenant} (org: ${savedOrg}, from saved mapping)`);\n }\n return { organisation: savedOrg, tenant };\n }\n\n // No saved mapping - show error with helpful message\n logger.error(`Organisation unknown for tenant \"${tenant}\".`);\n logger.dim('Please specify both organisation and tenant:');\n logger.dim(` farseer <command> <organisation> <tenant>`);\n logger.dim(` Example: farseer pull jgl tt-hotels-hr-dev`);\n logger.dim('');\n logger.dim('Or checkout the tenant first:');\n logger.dim(` farseer checkout <organisation> <tenant>`);\n process.exit(1);\n }\n\n // Case 3: No arguments - use current checkout\n const checkout = getCurrentCheckout();\n if (checkout) {\n if (checkout.organisation === checkout.tenant) {\n logger.dim(`Using checked-out tenant: ${checkout.tenant}`);\n } else {\n logger.dim(`Using checked-out tenant: ${checkout.tenant} (org: ${checkout.organisation})`);\n }\n return checkout;\n }\n\n // No checkout either\n logger.error('No tenant specified.');\n logger.dim('Either specify organisation and tenant: farseer <command> <org> <tenant>');\n logger.dim('Or checkout a tenant: farseer checkout <org> <tenant>');\n process.exit(1);\n}\n\n/**\n * Resolve organisation and tenant or exit with error.\n */\nexport function resolveOrgAndTenantOrExit(tenantArg?: string): ResolvedCheckout {\n const resolved = resolveOrgAndTenant(tenantArg);\n if (!resolved) {\n logger.error('No tenant specified.');\n logger.dim('Either specify a tenant: farseer <command> <tenant>');\n logger.dim('Or checkout a tenant: farseer checkout <tenant>');\n process.exit(1);\n }\n return resolved;\n}\n\n/**\n * Check if a string is a known tenant (has credentials configured).\n */\nexport function isKnownTenant(name: string): boolean {\n // Import here to avoid circular dependency\n const { getCredential } = require('../services/configService');\n return !!getCredential(name);\n}\n\nexport interface ParsedAppArgs {\n organisation: string;\n tenant: string;\n appName: string;\n}\n\n/**\n * Parse variadic arguments to extract tenant and app name.\n * If first arg is a known tenant, use it as tenant and rest as app name.\n * Otherwise, use checked-out tenant and all args as app name.\n *\n * @param args Array of arguments (from variadic command)\n * @returns { organisation: string, tenant: string, appName: string }\n */\nexport function parseAppArgs(args: string[]): ParsedAppArgs {\n if (args.length === 0) {\n logger.error('App name is required.');\n process.exit(1);\n }\n\n // Check if first argument is a known tenant\n if (args.length >= 2 && isKnownTenant(args[0])) {\n // First arg is tenant, rest is app name\n // Assume organisation = tenant when specified via command line\n return {\n organisation: args[0],\n tenant: args[0],\n appName: args.slice(1).join(' ')\n };\n }\n\n // Use checked-out tenant, all args are app name\n const checkout = getCurrentCheckout();\n if (checkout) {\n if (checkout.organisation === checkout.tenant) {\n logger.dim(`Using checked-out tenant: ${checkout.tenant}`);\n } else {\n logger.dim(`Using checked-out tenant: ${checkout.tenant} (org: ${checkout.organisation})`);\n }\n return {\n organisation: checkout.organisation,\n tenant: checkout.tenant,\n appName: args.join(' ')\n };\n }\n\n // No checked-out tenant and first arg isn't a known tenant\n // Assume first arg is tenant (will fail later if invalid)\n if (args.length >= 2) {\n return {\n organisation: args[0],\n tenant: args[0],\n appName: args.slice(1).join(' ')\n };\n }\n\n // Only one arg and no checkout - could be tenant or app name\n logger.error('No tenant specified.');\n logger.dim('Either specify a tenant: farseer app show <tenant> <app-name>');\n logger.dim('Or checkout a tenant: farseer checkout <tenant>');\n process.exit(1);\n}\n\n// Credential injection/stripping for local development\n// Matches: const farseerClient = new farseer.FarseerClient();\n// and: const fClient = new farseer.FarseerClient();\n\ninterface CredentialConfig {\n basePath: string;\n tenantId: string;\n apiKey: string;\n}\n\n// Pattern to match empty FarseerClient() constructor\nconst EMPTY_CLIENT_PATTERNS = [\n /const farseerClient = new farseer\\.FarseerClient\\(\\s*\\);/g,\n /const fClient = new farseer\\.FarseerClient\\(\\s*\\);/g,\n];\n\n// Pattern to match FarseerClient with injected credentials (multiline)\nconst INJECTED_CLIENT_PATTERN = /const (farseerClient|fClient) = new farseer\\.FarseerClient\\(\\{[\\s\\S]*?basePath:[\\s\\S]*?headers:[\\s\\S]*?'X-TENANT-ID':[\\s\\S]*?'X-API-KEY':[\\s\\S]*?\\}\\);/g;\n\nexport function injectCredentials(code: string, credential: CredentialConfig): string {\n const clientConfig = `{\n basePath: '${credential.basePath}',\n headers: {\n 'X-TENANT-ID': '${credential.tenantId}',\n 'X-API-KEY': '${credential.apiKey}',\n }\n }`;\n\n let modified = code;\n\n // Pattern 1: const farseerClient = new farseer.FarseerClient();\n modified = modified.replace(\n /const farseerClient = new farseer\\.FarseerClient\\(\\s*\\);/g,\n `const farseerClient = new farseer.FarseerClient(${clientConfig});`\n );\n\n // Pattern 2: const fClient = new farseer.FarseerClient();\n modified = modified.replace(\n /const fClient = new farseer\\.FarseerClient\\(\\s*\\);/g,\n `const fClient = new farseer.FarseerClient(${clientConfig});`\n );\n\n return modified;\n}\n\nexport function stripCredentials(code: string): string {\n // Replace injected credentials back to empty constructor\n let modified = code;\n\n // Match farseerClient with credentials\n modified = modified.replace(\n /const farseerClient = new farseer\\.FarseerClient\\(\\{[\\s\\S]*?basePath:[\\s\\S]*?headers:[\\s\\S]*?'X-TENANT-ID':[\\s\\S]*?'X-API-KEY':[\\s\\S]*?\\}\\);/g,\n 'const farseerClient = new farseer.FarseerClient();'\n );\n\n // Match fClient with credentials\n modified = modified.replace(\n /const fClient = new farseer\\.FarseerClient\\(\\{[\\s\\S]*?basePath:[\\s\\S]*?headers:[\\s\\S]*?'X-TENANT-ID':[\\s\\S]*?'X-API-KEY':[\\s\\S]*?\\}\\);/g,\n 'const fClient = new farseer.FarseerClient();'\n );\n\n return modified;\n}\n\nexport function hasInjectedCredentials(code: string): boolean {\n return INJECTED_CLIENT_PATTERN.test(code);\n}\n\nexport function hasEmptyFarseerClient(code: string): boolean {\n return EMPTY_CLIENT_PATTERNS.some(pattern => pattern.test(code));\n}\n\n// Process all script files in a directory\nexport function injectCredentialsInDir(srcDir: string, credential: CredentialConfig): number {\n let count = 0;\n const files = getAllFiles(srcDir);\n\n for (const file of files) {\n const filePath = path.join(srcDir, file);\n const content = fs.readFileSync(filePath, 'utf-8');\n const modified = injectCredentials(content, credential);\n\n if (modified !== content) {\n fs.writeFileSync(filePath, modified);\n count++;\n }\n }\n\n return count;\n}\n\nexport function stripCredentialsInDir(srcDir: string): number {\n let count = 0;\n const files = getAllFiles(srcDir);\n\n for (const file of files) {\n const filePath = path.join(srcDir, file);\n const content = fs.readFileSync(filePath, 'utf-8');\n const modified = stripCredentials(content);\n\n if (modified !== content) {\n fs.writeFileSync(filePath, modified);\n count++;\n }\n }\n\n return count;\n}\n\n// Audit log helpers\nexport interface AuditLogEntry {\n operation: 'pulled' | 'pushed' | 'deleted';\n filePath: string;\n uploadTime?: string;\n uploaderEmail?: string;\n uploaderName?: string;\n}\n\n/**\n * Execute async operations with limited concurrency.\n * Processes items in chunks to avoid overwhelming the API.\n *\n * @param items Array of items to process\n * @param fetcher Async function to call for each item\n * @param concurrency Maximum number of concurrent requests (default: 15)\n * @returns Array of results in the same order as input items\n */\nexport async function fetchWithConcurrency<T, R>(\n items: T[],\n fetcher: (item: T, index: number) => Promise<R>,\n concurrency: number = 15\n): Promise<R[]> {\n const results: R[] = [];\n\n for (let i = 0; i < items.length; i += concurrency) {\n const chunk = items.slice(i, i + concurrency);\n const chunkResults = await Promise.all(\n chunk.map((item, chunkIndex) => fetcher(item, i + chunkIndex))\n );\n results.push(...chunkResults);\n }\n\n return results;\n}\n\nexport function appendToAuditLog(tenant: string, entries: AuditLogEntry[]): void {\n if (entries.length === 0) {\n return;\n }\n\n const logPath = getAuditLogPath(tenant);\n const timestamp = new Date().toISOString();\n\n let logContent = '';\n for (const entry of entries) {\n const uploaderInfo = entry.uploaderName\n ? `${entry.uploaderName} (${entry.uploaderEmail})`\n : entry.uploaderEmail || 'Unknown';\n\n const uploadedAt = entry.uploadTime\n ? new Date(entry.uploadTime).toLocaleString()\n : 'Unknown';\n\n logContent += `${timestamp} | ${entry.operation} | ${entry.filePath} | Modified by: ${uploaderInfo} | Uploaded: ${uploadedAt}\\n`;\n }\n\n fs.appendFileSync(logPath, logContent, 'utf-8');\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|