farseer-cli 1.0.1 → 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.
@@ -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 scriptPath = path.resolve(srcDir, scriptName);
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 child = (0, import_child_process.spawn)("npx", ["tsx", `files/Scripts/${scriptName}`, ...cmdArgs], {
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 scriptPath = path.resolve(srcDir, script);
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 child = (0, import_child_process.spawn)("npx", ["tsx", `files/Scripts/${script}`], {
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,
@@ -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": "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",
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": "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",
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
- for (const item of remoteAppList) {
199
- const app = await this.api.getApp(item.reference);
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: item.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
- for (const item of remoteAppList) {
239
- const app = await this.api.getApp(item.reference);
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: item.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
- for (const item of remoteAppList) {
343
- const app = await this.api.getApp(item.reference);
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: item.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": "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",
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
  }
@@ -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
- for (const file of remoteFiles) {
88
- const content = await this.farseerClient.getFileContentAsBuffer(file.reference);
89
- remoteFileHashes[file.path] = (0, import_helpers.calculateHash)(content);
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
- for (const file of remoteFiles) {
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
- for (const file of remoteFiles) {
263
- const content = await this.farseerClient.getFileContentAsBuffer(file.reference);
264
- const remoteHash = (0, import_helpers.calculateHash)(content);
265
- const syncInfo = syncState.files[file.path];
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": "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",
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": ";;;;;;;;;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,SAAoB;AACpB,WAAsB;AACtB,4BAAsE;AACtE,qBAAiI;AACjI,oBAAuB;AA0BhB,MAAM,YAAY;AAAA,EAIrB,YAAY,QAAgB,eAA+B;AACvD,SAAK,SAAS;AACd,SAAK,gBAAgB;AAAA,EACzB;AAAA,EAEA,gBAA2B;AACvB,UAAM,mBAAe,gCAAgB,KAAK,MAAM;AAEhD,QAAI,CAAC,GAAG,WAAW,YAAY,GAAG;AAC9B,aAAO;AAAA,QACH,UAAU;AAAA,QACV,OAAO,CAAC;AAAA,MACZ;AAAA,IACJ;AAEA,QAAI;AACA,YAAM,UAAU,GAAG,aAAa,cAAc,OAAO;AACrD,aAAO,KAAK,MAAM,OAAO;AAAA,IAC7B,QAAQ;AACJ,aAAO;AAAA,QACH,UAAU;AAAA,QACV,OAAO,CAAC;AAAA,MACZ;AAAA,IACJ;AAAA,EACJ;AAAA,EAEA,cAAc,OAAwB;AAClC,UAAM,mBAAe,gCAAgB,KAAK,MAAM;AAChD,8CAAsB,KAAK,QAAQ,YAAY,CAAC;AAChD,OAAG,cAAc,cAAc,KAAK,UAAU,OAAO,MAAM,CAAC,GAAG,OAAO;AAAA,EAC1E;AAAA,EAEA,MAAM,UAAU,SAAkD;AAC9D,UAAM,YAAY,KAAK,cAAc;AACrC,UAAM,eAAW,kCAAkB,KAAK,MAAM;AAG9C,QAAI,iBAAa,iCAAiB,QAAQ;AAG1C,QAAI,CAAC,SAAS,KAAK;AACf,mBAAa,WAAW;AAAA,QAAO,OAC3B,gDAA0B,KAAK,SAAO,EAAE,YAAY,EAAE,SAAS,GAAG,CAAC;AAAA,MACvE;AAAA,IACJ;AAEA,UAAM,kBAA0C,CAAC;AAEjD,eAAW,QAAQ,YAAY;AAC3B,YAAM,WAAW,KAAK,KAAK,UAAU,IAAI;AACzC,YAAM,UAAU,GAAG,aAAa,QAAQ;AACxC,sBAAgB,IAAI,QAAI,8BAAc,OAAO;AAAA,IACjD;AAGA,QAAI,cAAc,MAAM,KAAK,cAAc,UAAU;AAGrD,QAAI,CAAC,SAAS,KAAK;AACf,oBAAc,YAAY;AAAA,QAAO,OAC7B,gDAA0B,KAAK,SAAO,EAAE,KAAK,YAAY,EAAE,SAAS,GAAG,CAAC;AAAA,MAC5E;AAAA,IACJ;AAEA,UAAM,mBAA2C,CAAC;AAGlD,UAAM,eAAe,UAAM;AAAA,MACvB;AAAA,MACA,OAAO,SAAS;AACZ,cAAM,UAAU,MAAM,KAAK,cAAc,uBAAuB,KAAK,SAAS;AAC9E,eAAO,EAAE,MAAM,KAAK,MAAM,UAAM,8BAAc,OAAO,EAAE;AAAA,MAC3D;AAAA,MACA;AAAA,IACJ;AAEA,eAAW,EAAE,MAAAA,OAAM,KAAK,KAAK,cAAc;AACvC,uBAAiBA,KAAI,IAAI;AAAA,IAC7B;AAEA,UAAM,SAAqB;AAAA,MACvB,iBAAiB,CAAC;AAAA,MAClB,kBAAkB,CAAC;AAAA,MACnB,WAAW,CAAC;AAAA,MACZ,YAAY,CAAC;AAAA,MACb,QAAQ,CAAC;AAAA,IACb;AAEA,UAAM,WAAW,oBAAI,IAAI,CAAC,GAAG,OAAO,KAAK,eAAe,GAAG,GAAG,OAAO,KAAK,gBAAgB,CAAC,CAAC;AAE5F,eAAW,QAAQ,UAAU;AACzB,YAAM,YAAY,gBAAgB,IAAI;AACtC,YAAM,aAAa,iBAAiB,IAAI;AACxC,YAAM,WAAW,UAAU,MAAM,IAAI;AAErC,UAAI,aAAa,YAAY;AAEzB,YAAI,cAAc,YAAY;AAC1B,iBAAO,OAAO,KAAK,IAAI;AAAA,QAC3B,WAAW,UAAU;AAEjB,gBAAM,eAAe,cAAc,SAAS;AAC5C,gBAAM,gBAAgB,eAAe,SAAS;AAE9C,cAAI,gBAAgB,eAAe;AAE/B,mBAAO,gBAAgB,KAAK,IAAI;AAChC,mBAAO,iBAAiB,KAAK,IAAI;AAAA,UACrC,WAAW,cAAc;AACrB,mBAAO,gBAAgB,KAAK,IAAI;AAAA,UACpC,WAAW,eAAe;AACtB,mBAAO,iBAAiB,KAAK,IAAI;AAAA,UACrC,OAAO;AACH,mBAAO,OAAO,KAAK,IAAI;AAAA,UAC3B;AAAA,QACJ,OAAO;AAEH,iBAAO,gBAAgB,KAAK,IAAI;AAAA,QACpC;AAAA,MACJ,WAAW,aAAa,CAAC,YAAY;AAEjC,YAAI,YAAY,SAAS,YAAY;AAEjC,iBAAO,iBAAiB,KAAK,IAAI;AAAA,QACrC,OAAO;AACH,iBAAO,UAAU,KAAK,IAAI;AAAA,QAC9B;AAAA,MACJ,WAAW,CAAC,aAAa,YAAY;AAEjC,YAAI,YAAY,SAAS,WAAW;AAEhC,iBAAO,gBAAgB,KAAK,IAAI;AAAA,QACpC,OAAO;AACH,iBAAO,WAAW,KAAK,IAAI;AAAA,QAC/B;AAAA,MACJ;AAAA,IACJ;AAEA,WAAO;AAAA,EACX;AAAA,EAEA,MAAM,KAAK,SAIR;AACC,UAAM,eAAW,kCAAkB,KAAK,MAAM;AAC9C,8CAAsB,QAAQ;AAE9B,UAAM,YAAY,KAAK,cAAc;AACrC,QAAI,cAAc,MAAM,KAAK,cAAc,UAAU;AAGrD,QAAI,CAAC,SAAS,KAAK;AACf,oBAAc,YAAY;AAAA,QAAO,OAC7B,gDAA0B,KAAK,SAAO,EAAE,KAAK,YAAY,EAAE,SAAS,GAAG,CAAC;AAAA,MAC5E;AAAA,IACJ;AAGA,kBAAc,MAAM,KAAK,sBAAsB,WAAW;AAE1D,UAAM,SAAS;AAAA,MACX,YAAY,CAAC;AAAA,MACb,SAAS,CAAC;AAAA,MACV,WAAW,CAAC;AAAA,IAChB;AAEA,UAAM,kBAAkB,IAAI,IAAI,YAAY,IAAI,OAAK,EAAE,IAAI,CAAC;AAG5D,yBAAO,IAAI,YAAY,YAAY,MAAM,uBAAuB;AAChE,UAAM,eAAe,UAAM;AAAA,MACvB;AAAA,MACA,OAAO,MAAM,UAAU;AACnB,cAAM,UAAU,MAAM,KAAK,cAAc,uBAAuB,KAAK,SAAS;AAE9E,aAAK,QAAQ,KAAK,OAAO,KAAK,UAAU,YAAY,SAAS,GAAG;AAC5D,kBAAQ,OAAO,MAAM,iBAAiB,QAAQ,CAAC,IAAI,YAAY,MAAM,QAAQ;AAAA,QACjF;AACA,eAAO,EAAE,MAAM,QAAQ;AAAA,MAC3B;AAAA,MACA;AAAA,IACJ;AAEA,QAAI,YAAY,SAAS,GAAG;AACxB,cAAQ,OAAO,MAAM,OAAO,IAAI,OAAO,EAAE,IAAI,IAAI;AAAA,IACrD;AAGA,eAAW,EAAE,MAAM,QAAQ,KAAK,cAAc;AAC1C,YAAM,YAAY,KAAK,KAAK,UAAU,KAAK,IAAI;AAC/C,YAAM,iBAAa,8BAAc,OAAO;AAExC,UAAI,cAAc;AAElB,UAAI,GAAG,WAAW,SAAS,GAAG;AAC1B,cAAM,eAAe,GAAG,aAAa,SAAS;AAC9C,cAAM,gBAAY,8BAAc,YAAY;AAE5C,YAAI,cAAc,YAAY;AAC1B,wBAAc;AACd,iBAAO,UAAU,KAAK,KAAK,IAAI;AAAA,QACnC;AAAA,MACJ;AAEA,UAAI,aAAa;AACb,kDAAsB,KAAK,QAAQ,SAAS,CAAC;AAC7C,WAAG,cAAc,WAAW,OAAO;AACnC,eAAO,WAAW,KAAK,KAAK,IAAI;AAAA,MACpC;AAGA,gBAAU,MAAM,KAAK,IAAI,IAAI;AAAA,QACzB,WAAW;AAAA,QACX;AAAA,QACA,UAAU,KAAK,WAAW;AAAA,UACtB,YAAY,KAAK,SAAS;AAAA,UAC1B,eAAe,KAAK,SAAS,UAAU;AAAA,UACvC,cAAc,KAAK,SAAS,WACtB,GAAG,KAAK,SAAS,SAAS,SAAS,IAAI,KAAK,SAAS,SAAS,QAAQ,GAAG,KAAK,IAC9E;AAAA,UACN,cAAa,oBAAI,KAAK,GAAE,YAAY;AAAA,QACxC,IAAI;AAAA,MACR;AAAA,IACJ;AAGA,UAAM,iBAAa,iCAAiB,QAAQ;AAC5C,eAAW,aAAa,YAAY;AAChC,UAAI,CAAC,gBAAgB,IAAI,SAAS,GAAG;AAEjC,cAAM,WAAW,UAAU,MAAM,SAAS;AAC1C,YAAI,UAAU;AAEV,gBAAM,YAAY,KAAK,KAAK,UAAU,SAAS;AAC/C,aAAG,WAAW,SAAS;AACvB,iBAAO,QAAQ,KAAK,SAAS;AAC7B,iBAAO,UAAU,MAAM,SAAS;AAAA,QACpC;AAAA,MAEJ;AAAA,IACJ;AAEA,cAAU,YAAW,oBAAI,KAAK,GAAE,YAAY;AAC5C,SAAK,cAAc,SAAS;AAE5B,WAAO;AAAA,EACX;AAAA,EAEA,MAAM,OAKH;AACC,UAAM,eAAW,kCAAkB,KAAK,MAAM;AAC9C,UAAM,YAAY,KAAK,cAAc;AAErC,UAAM,SAAS;AAAA,MACX,UAAU,CAAC;AAAA,MACX,SAAS,CAAC;AAAA,MACV,SAAS,CAAC;AAAA,MACV,WAAW,CAAC;AAAA,IAChB;AAGA,UAAM,cAAc,MAAM,KAAK,cAAc,UAAU;AACvD,UAAM,gBAAgB,oBAAI,IAAwB;AAClD,eAAW,QAAQ,aAAa;AAC5B,oBAAc,IAAI,KAAK,MAAM,IAAI;AAAA,IACrC;AAGA,UAAM,iBAAa,iCAAiB,QAAQ;AAG5C,eAAW,aAAa,YAAY;AAChC,YAAM,YAAY,KAAK,KAAK,UAAU,SAAS;AAC/C,YAAM,UAAU,GAAG,aAAa,SAAS;AACzC,YAAM,gBAAY,8BAAc,OAAO;AAEvC,YAAM,aAAa,cAAc,IAAI,SAAS;AAE9C,UAAI,YAAY;AAEZ,cAAM,gBAAgB,MAAM,KAAK,cAAc,uBAAuB,WAAW,SAAS;AAC1F,cAAM,iBAAa,8BAAc,aAAa;AAE9C,YAAI,cAAc,YAAY;AAC1B,gBAAM,KAAK,cAAc,WAAW,WAAW,WAAW,SAAS,KAAK,SAAS,SAAS,CAAC;AAC3F,iBAAO,QAAQ,KAAK,SAAS;AAAA,QACjC,OAAO;AACH,iBAAO,UAAU,KAAK,SAAS;AAAA,QACnC;AAAA,MACJ,OAAO;AAEH,cAAM,YAAY,UAAU,MAAM,GAAG;AACrC,cAAM,WAAW,UAAU,IAAI;AAC/B,cAAM,aAAa,CAAC,SAAS,GAAG,SAAS;AAEzC,cAAM,KAAK,cAAc,WAAW,SAAS,UAAU,UAAU;AACjE,eAAO,SAAS,KAAK,SAAS;AAAA,MAClC;AAGA,gBAAU,MAAM,SAAS,IAAI;AAAA,QACzB;AAAA,QACA,YAAY;AAAA,MAChB;AAAA,IACJ;AAGA,UAAM,eAAe,IAAI,IAAI,UAAU;AACvC,eAAW,CAAC,YAAY,UAAU,KAAK,eAAe;AAClD,UAAI,CAAC,aAAa,IAAI,UAAU,GAAG;AAC/B,cAAM,WAAW,UAAU,MAAM,UAAU;AAC3C,YAAI,UAAU;AAEV,gBAAM,KAAK,cAAc,WAAW,WAAW,SAAS;AACxD,iBAAO,QAAQ,KAAK,UAAU;AAC9B,iBAAO,UAAU,MAAM,UAAU;AAAA,QACrC;AAAA,MACJ;AAAA,IACJ;AAEA,cAAU,YAAW,oBAAI,KAAK,GAAE,YAAY;AAC5C,SAAK,cAAc,SAAS;AAE5B,WAAO;AAAA,EACX;AAAA,EAEA,MAAM,qBAAuC;AACzC,UAAM,YAAY,KAAK,cAAc;AACrC,QAAI,cAAc,MAAM,KAAK,cAAc,UAAU;AAIrD,kBAAc,YAAY;AAAA,MAAO,OAC7B,gDAA0B,KAAK,SAAO,EAAE,KAAK,YAAY,EAAE,SAAS,GAAG,CAAC;AAAA,IAC5E;AAGA,UAAM,eAAe,UAAM;AAAA,MACvB;AAAA,MACA,OAAO,SAAS;AACZ,cAAM,UAAU,MAAM,KAAK,cAAc,uBAAuB,KAAK,SAAS;AAC9E,eAAO,EAAE,MAAM,KAAK,MAAM,UAAM,8BAAc,OAAO,EAAE;AAAA,MAC3D;AAAA,MACA;AAAA,IACJ;AAEA,eAAW,EAAE,MAAM,UAAU,MAAM,WAAW,KAAK,cAAc;AAC7D,YAAM,WAAW,UAAU,MAAM,QAAQ;AAEzC,UAAI,CAAC,YAAY,SAAS,eAAe,YAAY;AACjD,eAAO;AAAA,MACX;AAAA,IACJ;AAGA,eAAW,YAAY,OAAO,KAAK,UAAU,KAAK,GAAG;AAEjD,YAAM,WAAW,gDAA0B,KAAK,SAAO,SAAS,YAAY,EAAE,SAAS,GAAG,CAAC;AAC3F,UAAI,CAAC,SAAU;AAEf,YAAM,SAAS,YAAY,KAAK,OAAK,EAAE,SAAS,QAAQ;AACxD,UAAI,CAAC,UAAU,UAAU,MAAM,QAAQ,EAAE,YAAY;AACjD,eAAO;AAAA,MACX;AAAA,IACJ;AAEA,WAAO;AAAA,EACX;AAAA,EAEA,MAAM,YAAY,UAGf;AACC,UAAM,eAAW,kCAAkB,KAAK,MAAM;AAC9C,UAAM,YAAY,KAAK,KAAK,UAAU,QAAQ;AAE9C,QAAI,eAA8B;AAClC,QAAI,gBAA+B;AAEnC,QAAI,GAAG,WAAW,SAAS,GAAG;AAE1B,qBAAe,GAAG,aAAa,WAAW,OAAO;AAAA,IACrD;AAEA,UAAM,aAAa,MAAM,KAAK,cAAc,cAAc,QAAQ;AAClE,QAAI,YAAY;AACZ,sBAAgB,MAAM,KAAK,cAAc,eAAe,WAAW,SAAS;AAAA,IAChF;AAEA,WAAO,EAAE,cAAc,cAAc;AAAA,EACzC;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOA,MAAc,sBAAsB,OAA4C;AAC5E,UAAM,YAAY;AAElB,aAAS,IAAI,GAAG,IAAI,MAAM,QAAQ,KAAK,WAAW;AAC9C,YAAM,QAAQ,MAAM,MAAM,GAAG,IAAI,SAAS;AAE1C,YAAM,QAAQ,IAAI,MAAM,IAAI,OAAO,SAAS;AACxC,YAAI;AACA,gBAAM,WAAW,MAAM,KAAK,cAAc,gBAAgB,KAAK,SAAS;AACxE,cAAI,UAAU;AACV,iBAAK,WAAW;AAAA,UACpB;AAAA,QACJ,QAAQ;AAAA,QAER;AAAA,MACJ,CAAC,CAAC;AAAA,IACN;AAEA,WAAO;AAAA,EACX;AACJ;",
6
+ "names": ["path"]
7
7
  }
@@ -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": "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",
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
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "farseer-cli",
3
- "version": "1.0.1",
3
+ "version": "1.0.2",
4
4
  "description": "CLI tool for syncing Farseer App scripts between local repository and Farseer instances",
5
5
  "main": "dist/index.js",
6
6
  "bin": {