appwrite-utils-cli 1.7.7 → 1.7.9

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.
Files changed (65) hide show
  1. package/SELECTION_DIALOGS.md +146 -0
  2. package/dist/cli/commands/databaseCommands.js +89 -23
  3. package/dist/config/services/ConfigLoaderService.d.ts +7 -0
  4. package/dist/config/services/ConfigLoaderService.js +47 -1
  5. package/dist/functions/deployments.js +5 -23
  6. package/dist/functions/methods.js +4 -2
  7. package/dist/functions/pathResolution.d.ts +37 -0
  8. package/dist/functions/pathResolution.js +185 -0
  9. package/dist/functions/templates/count-docs-in-collection/README.md +54 -0
  10. package/dist/functions/templates/count-docs-in-collection/package.json +25 -0
  11. package/dist/functions/templates/count-docs-in-collection/src/main.ts +159 -0
  12. package/dist/functions/templates/count-docs-in-collection/src/request.ts +9 -0
  13. package/dist/functions/templates/count-docs-in-collection/tsconfig.json +28 -0
  14. package/dist/functions/templates/hono-typescript/README.md +286 -0
  15. package/dist/functions/templates/hono-typescript/package.json +26 -0
  16. package/dist/functions/templates/hono-typescript/src/adapters/request.ts +74 -0
  17. package/dist/functions/templates/hono-typescript/src/adapters/response.ts +106 -0
  18. package/dist/functions/templates/hono-typescript/src/app.ts +180 -0
  19. package/dist/functions/templates/hono-typescript/src/context.ts +103 -0
  20. package/dist/functions/templates/hono-typescript/src/index.ts +54 -0
  21. package/dist/functions/templates/hono-typescript/src/middleware/appwrite.ts +119 -0
  22. package/dist/functions/templates/hono-typescript/tsconfig.json +20 -0
  23. package/dist/functions/templates/typescript-node/README.md +32 -0
  24. package/dist/functions/templates/typescript-node/package.json +25 -0
  25. package/dist/functions/templates/typescript-node/src/context.ts +103 -0
  26. package/dist/functions/templates/typescript-node/src/index.ts +29 -0
  27. package/dist/functions/templates/typescript-node/tsconfig.json +28 -0
  28. package/dist/functions/templates/uv/README.md +31 -0
  29. package/dist/functions/templates/uv/pyproject.toml +30 -0
  30. package/dist/functions/templates/uv/src/__init__.py +0 -0
  31. package/dist/functions/templates/uv/src/context.py +125 -0
  32. package/dist/functions/templates/uv/src/index.py +46 -0
  33. package/dist/main.js +175 -4
  34. package/dist/migrations/appwriteToX.d.ts +27 -2
  35. package/dist/migrations/appwriteToX.js +293 -69
  36. package/dist/migrations/yaml/YamlImportConfigLoader.d.ts +1 -1
  37. package/dist/migrations/yaml/generateImportSchemas.js +23 -8
  38. package/dist/shared/schemaGenerator.js +25 -12
  39. package/dist/shared/selectionDialogs.d.ts +214 -0
  40. package/dist/shared/selectionDialogs.js +540 -0
  41. package/dist/utils/configDiscovery.d.ts +4 -4
  42. package/dist/utils/configDiscovery.js +66 -30
  43. package/dist/utils/yamlConverter.d.ts +1 -0
  44. package/dist/utils/yamlConverter.js +26 -3
  45. package/dist/utilsController.d.ts +7 -1
  46. package/dist/utilsController.js +198 -17
  47. package/package.json +4 -2
  48. package/scripts/copy-templates.ts +23 -0
  49. package/src/cli/commands/databaseCommands.ts +133 -34
  50. package/src/config/services/ConfigLoaderService.ts +62 -1
  51. package/src/functions/deployments.ts +10 -35
  52. package/src/functions/methods.ts +4 -2
  53. package/src/functions/pathResolution.ts +227 -0
  54. package/src/main.ts +276 -34
  55. package/src/migrations/appwriteToX.ts +385 -90
  56. package/src/migrations/yaml/generateImportSchemas.ts +26 -8
  57. package/src/shared/schemaGenerator.ts +29 -12
  58. package/src/shared/selectionDialogs.ts +745 -0
  59. package/src/utils/configDiscovery.ts +83 -39
  60. package/src/utils/yamlConverter.ts +29 -3
  61. package/src/utilsController.ts +250 -22
  62. package/dist/utils/schemaStrings.d.ts +0 -14
  63. package/dist/utils/schemaStrings.js +0 -428
  64. package/dist/utils/sessionPreservationExample.d.ts +0 -1666
  65. package/dist/utils/sessionPreservationExample.js +0 -101
@@ -3,6 +3,9 @@ import chalk from "chalk";
3
3
  import { join } from "node:path";
4
4
  import { MessageFormatter } from "../../shared/messageFormatter.js";
5
5
  import { ConfirmationDialogs } from "../../shared/confirmationDialogs.js";
6
+ import { SelectionDialogs } from "../../shared/selectionDialogs.js";
7
+ import type { DatabaseSelection, BucketSelection } from "../../shared/selectionDialogs.js";
8
+ import { logger } from "../../shared/logging.js";
6
9
  import { fetchAllDatabases } from "../../databases/methods.js";
7
10
  import { listBuckets } from "../../storage/methods.js";
8
11
  import { getFunction, downloadLatestFunctionDeployment } from "../../functions/methods.js";
@@ -12,23 +15,40 @@ export const databaseCommands = {
12
15
  async syncDb(cli: InteractiveCLI): Promise<void> {
13
16
  MessageFormatter.progress("Pushing local configuration to Appwrite...", { prefix: "Database" });
14
17
 
15
- const databases = await (cli as any).selectDatabases(
16
- (cli as any).getLocalDatabases(),
17
- chalk.blue("Select local databases to push:"),
18
- true
19
- );
18
+ try {
19
+ // Initialize controller
20
+ await (cli as any).controller!.init();
20
21
 
21
- if (!databases.length) {
22
- MessageFormatter.warning("No databases selected. Skipping database sync.", { prefix: "Database" });
23
- return;
24
- }
22
+ // Get available and configured databases
23
+ const availableDatabases = await fetchAllDatabases((cli as any).controller!.database!);
24
+ const configuredDatabases = (cli as any).controller!.config?.databases || [];
25
25
 
26
- try {
27
- // Loop through each database and prompt for collections specific to that database
28
- for (const database of databases) {
29
- MessageFormatter.info(`\n📦 Configuring push for database: ${database.name}`, { prefix: "Database" });
26
+ // Get local collections for selection
27
+ const localCollections = (cli as any).getLocalCollections();
28
+
29
+ // Push operations always use local configuration as source of truth
30
+
31
+ // Select databases
32
+ const selectedDatabaseIds = await SelectionDialogs.selectDatabases(
33
+ availableDatabases,
34
+ configuredDatabases,
35
+ { showSelectAll: true, allowNewOnly: false }
36
+ );
37
+
38
+ if (selectedDatabaseIds.length === 0) {
39
+ MessageFormatter.warning("No databases selected. Skipping database sync.", { prefix: "Database" });
40
+ return;
41
+ }
42
+
43
+ // Select tables/collections for each database using the existing method
44
+ const tableSelectionsMap = new Map<string, string[]>();
45
+ const availableTablesMap = new Map<string, any[]>();
30
46
 
31
- const collections = await (cli as any).selectCollectionsAndTables(
47
+ for (const databaseId of selectedDatabaseIds) {
48
+ const database = availableDatabases.find(db => db.$id === databaseId)!;
49
+
50
+ // Use the existing selectCollectionsAndTables method
51
+ const selectedCollections = await (cli as any).selectCollectionsAndTables(
32
52
  database,
33
53
  (cli as any).controller!.database!,
34
54
  chalk.blue(`Select collections/tables to push to "${database.name}":`),
@@ -36,19 +56,93 @@ export const databaseCommands = {
36
56
  true // prefer local
37
57
  );
38
58
 
39
- if (collections.length === 0) {
59
+ // Map selected collections to table IDs
60
+ const selectedTableIds = selectedCollections.map((c: any) => c.$id || c.id);
61
+
62
+ // Store selections
63
+ tableSelectionsMap.set(databaseId, selectedTableIds);
64
+ availableTablesMap.set(databaseId, selectedCollections);
65
+
66
+ if (selectedCollections.length === 0) {
40
67
  MessageFormatter.warning(`No collections selected for database "${database.name}". Skipping.`, { prefix: "Database" });
41
68
  continue;
42
69
  }
70
+ }
43
71
 
44
- // Push selected collections to this specific database
45
- await (cli as any).controller!.syncDb([database], collections);
46
- MessageFormatter.success(
47
- `Pushed ${collections.length} collection(s) to database "${database.name}"`,
48
- { prefix: "Database" }
49
- );
72
+ // Ask if user wants to select buckets
73
+ const { selectBuckets } = await inquirer.prompt([
74
+ {
75
+ type: "confirm",
76
+ name: "selectBuckets",
77
+ message: "Do you want to select storage buckets to sync as well?",
78
+ default: false,
79
+ },
80
+ ]);
81
+
82
+ let bucketSelections: BucketSelection[] = [];
83
+
84
+ if (selectBuckets) {
85
+ // Get available and configured buckets
86
+ try {
87
+ const availableBucketsResponse = await listBuckets((cli as any).controller!.storage!);
88
+ const availableBuckets = availableBucketsResponse.buckets || [];
89
+ const configuredBuckets = (cli as any).controller!.config?.buckets || [];
90
+
91
+ if (availableBuckets.length === 0) {
92
+ MessageFormatter.warning("No storage buckets available in remote instance.", { prefix: "Database" });
93
+ } else {
94
+ // Select buckets using SelectionDialogs
95
+ const selectedBucketIds = await SelectionDialogs.selectBucketsForDatabases(
96
+ selectedDatabaseIds,
97
+ availableBuckets,
98
+ configuredBuckets,
99
+ { showSelectAll: true, groupByDatabase: true }
100
+ );
101
+
102
+ if (selectedBucketIds.length > 0) {
103
+ // Create BucketSelection objects
104
+ bucketSelections = SelectionDialogs.createBucketSelection(
105
+ selectedBucketIds,
106
+ availableBuckets,
107
+ configuredBuckets,
108
+ availableDatabases
109
+ );
110
+
111
+ MessageFormatter.info(`Selected ${bucketSelections.length} storage bucket(s)`, { prefix: "Database" });
112
+ }
113
+ }
114
+ } catch (error) {
115
+ MessageFormatter.warning("Failed to fetch storage buckets. Continuing with databases only.", { prefix: "Database" });
116
+ logger.warn("Storage bucket fetch failed during syncDb", { error: error instanceof Error ? error.message : String(error) });
117
+ }
50
118
  }
51
119
 
120
+ // Create DatabaseSelection objects
121
+ const databaseSelections = SelectionDialogs.createDatabaseSelection(
122
+ selectedDatabaseIds,
123
+ availableDatabases,
124
+ tableSelectionsMap,
125
+ configuredDatabases,
126
+ availableTablesMap
127
+ );
128
+
129
+ // Show confirmation summary
130
+ const selectionSummary = SelectionDialogs.createSyncSelectionSummary(
131
+ databaseSelections,
132
+ bucketSelections
133
+ );
134
+
135
+ const confirmed = await SelectionDialogs.confirmSyncSelection(selectionSummary, 'push');
136
+
137
+ if (!confirmed) {
138
+ MessageFormatter.info("Push operation cancelled by user", { prefix: "Database" });
139
+ return;
140
+ }
141
+
142
+ // Perform selective push using the controller
143
+ MessageFormatter.progress("Starting selective push...", { prefix: "Database" });
144
+ await (cli as any).controller!.selectivePush(databaseSelections, bucketSelections);
145
+
52
146
  MessageFormatter.success("\n✅ All database configurations pushed successfully!", { prefix: "Database" });
53
147
 
54
148
  // Then handle functions if requested
@@ -104,23 +198,28 @@ export const databaseCommands = {
104
198
  (cli as any).controller!.database!
105
199
  );
106
200
 
107
- // Use the controller's synchronizeConfigurations method which handles collections properly
108
- MessageFormatter.progress("Pulling collections and generating collection files...", { prefix: "Collections" });
109
- await (cli as any).controller!.synchronizeConfigurations(remoteDatabases);
110
-
111
- // Also configure buckets for any new databases
201
+ // First, prepare the combined database list for bucket configuration
112
202
  const localDatabases = (cli as any).controller!.config?.databases || [];
113
- const updatedConfig = await (cli as any).configureBuckets({
203
+ const allDatabases = [
204
+ ...localDatabases,
205
+ ...remoteDatabases.filter(
206
+ (rd: any) => !localDatabases.some((ld: any) => ld.name === rd.name)
207
+ ),
208
+ ];
209
+
210
+ // Configure buckets FIRST to get user selections before writing config
211
+ MessageFormatter.progress("Configuring storage buckets...", { prefix: "Buckets" });
212
+ const configWithBuckets = await (cli as any).configureBuckets({
114
213
  ...(cli as any).controller!.config!,
115
- databases: [
116
- ...localDatabases,
117
- ...remoteDatabases.filter(
118
- (rd: any) => !localDatabases.some((ld: any) => ld.name === rd.name)
119
- ),
120
- ],
214
+ databases: allDatabases,
121
215
  });
122
216
 
123
- (cli as any).controller!.config = updatedConfig;
217
+ // Update controller config with bucket selections
218
+ (cli as any).controller!.config = configWithBuckets;
219
+
220
+ // Now synchronize configurations with the updated config that includes bucket selections
221
+ MessageFormatter.progress("Pulling collections and generating collection files...", { prefix: "Collections" });
222
+ await (cli as any).controller!.synchronizeConfigurations(remoteDatabases, configWithBuckets);
124
223
  }
125
224
 
126
225
  // Then sync functions
@@ -1,5 +1,6 @@
1
1
  import fs from "fs";
2
2
  import path from "path";
3
+ import { resolve as resolvePath, dirname, isAbsolute } from "node:path";
3
4
  import yaml from "js-yaml";
4
5
  import { register } from "tsx/esm/api";
5
6
  import { pathToFileURL } from "node:url";
@@ -19,6 +20,7 @@ import {
19
20
  type CollectionDiscoveryResult,
20
21
  type TableDiscoveryResult,
21
22
  } from "../../utils/configDiscovery.js";
23
+ import { expandTildePath } from "../../functions/pathResolution.js";
22
24
 
23
25
  /**
24
26
  * Options for loading collections or tables
@@ -51,6 +53,35 @@ export interface CollectionLoadOptions {
51
53
  * - Validates and normalizes configuration data
52
54
  */
53
55
  export class ConfigLoaderService {
56
+ /**
57
+ * Normalizes function dirPath to absolute path
58
+ * @param func Function configuration object
59
+ * @param configDir Directory containing the config file
60
+ * @returns Function with normalized dirPath
61
+ */
62
+ private normalizeFunctionPath(func: any, configDir: string): any {
63
+ if (!func.dirPath) {
64
+ return func;
65
+ }
66
+
67
+ // Expand tilde first
68
+ const expandedPath = expandTildePath(func.dirPath);
69
+
70
+ // If already absolute, return as-is
71
+ if (isAbsolute(expandedPath)) {
72
+ return {
73
+ ...func,
74
+ dirPath: expandedPath
75
+ };
76
+ }
77
+
78
+ // Resolve relative to config directory
79
+ return {
80
+ ...func,
81
+ dirPath: resolvePath(configDir, expandedPath)
82
+ };
83
+ }
84
+
54
85
  /**
55
86
  * Loads configuration from a discovered path, auto-detecting the type
56
87
  * @param configPath Path to the configuration file
@@ -78,6 +109,13 @@ export class ConfigLoaderService {
78
109
  );
79
110
  }
80
111
 
112
+ const configDir = path.dirname(configPath);
113
+
114
+ // Normalize function paths
115
+ const normalizedFunctions = partialConfig.functions
116
+ ? partialConfig.functions.map(func => this.normalizeFunctionPath(func, configDir))
117
+ : [];
118
+
81
119
  return {
82
120
  appwriteEndpoint: partialConfig.appwriteEndpoint,
83
121
  appwriteProject: partialConfig.appwriteProject,
@@ -105,7 +143,7 @@ export class ConfigLoaderService {
105
143
  },
106
144
  databases: partialConfig.databases || [],
107
145
  buckets: partialConfig.buckets || [],
108
- functions: partialConfig.functions || [],
146
+ functions: normalizedFunctions,
109
147
  collections: partialConfig.collections || [],
110
148
  sessionCookie: partialConfig.sessionCookie,
111
149
  authMethod: partialConfig.authMethod || "auto",
@@ -155,6 +193,13 @@ export class ConfigLoaderService {
155
193
 
156
194
  // Load collections and tables from their respective directories
157
195
  const configDir = path.dirname(yamlPath);
196
+
197
+ // Normalize function paths
198
+ if (config.functions) {
199
+ config.functions = config.functions.map(func =>
200
+ this.normalizeFunctionPath(func, configDir)
201
+ );
202
+ }
158
203
  const collectionsDir = path.join(configDir, config.schemaConfig?.collectionsDirectory || "collections");
159
204
  const tablesDir = path.join(configDir, config.schemaConfig?.tablesDirectory || "tables");
160
205
 
@@ -248,6 +293,14 @@ export class ConfigLoaderService {
248
293
  throw new Error(`Failed to load TypeScript config from: ${tsPath}`);
249
294
  }
250
295
 
296
+ // Normalize function paths
297
+ const configDir = path.dirname(tsPath);
298
+ if (config.functions) {
299
+ config.functions = config.functions.map(func =>
300
+ this.normalizeFunctionPath(func, configDir)
301
+ );
302
+ }
303
+
251
304
  MessageFormatter.success(`Loaded TypeScript config from: ${tsPath}`, {
252
305
  prefix: "Config",
253
306
  });
@@ -292,6 +345,14 @@ export class ConfigLoaderService {
292
345
  appwriteConfig.collections = collections;
293
346
  }
294
347
 
348
+ // Normalize function paths
349
+ const configDir = path.dirname(jsonPath);
350
+ if (appwriteConfig.functions) {
351
+ appwriteConfig.functions = appwriteConfig.functions.map(func =>
352
+ this.normalizeFunctionPath(func, configDir)
353
+ );
354
+ }
355
+
295
356
  MessageFormatter.success(`Loaded project config from: ${jsonPath}`, {
296
357
  prefix: "Config",
297
358
  });
@@ -16,30 +16,7 @@ import {
16
16
  } from "./methods.js";
17
17
  import ignore from "ignore";
18
18
  import { MessageFormatter } from "../shared/messageFormatter.js";
19
-
20
- const findFunctionDirectory = (
21
- basePath: string,
22
- functionName: string
23
- ): string | undefined => {
24
- const normalizedName = functionName.toLowerCase().replace(/\s+/g, "-");
25
- const dirs = fs.readdirSync(basePath, { withFileTypes: true });
26
-
27
- for (const dir of dirs) {
28
- if (dir.isDirectory()) {
29
- const fullPath = join(basePath, dir.name);
30
- if (dir.name.toLowerCase() === normalizedName) {
31
- return fullPath;
32
- }
33
-
34
- const nestedResult = findFunctionDirectory(fullPath, functionName);
35
- if (nestedResult) {
36
- return nestedResult;
37
- }
38
- }
39
- }
40
-
41
- return undefined;
42
- };
19
+ import { resolveFunctionDirectory, validateFunctionDirectory } from './pathResolution.js';
43
20
 
44
21
  export const deployFunction = async (
45
22
  client: Client,
@@ -183,18 +160,16 @@ export const deployLocalFunction = async (
183
160
  functionExists = false;
184
161
  }
185
162
 
186
- const resolvedPath =
187
- functionPath ||
188
- functionConfig.dirPath ||
189
- findFunctionDirectory(process.cwd(), functionName) ||
190
- join(
191
- process.cwd(),
192
- "functions",
193
- functionName.toLowerCase().replace(/\s+/g, "-")
194
- );
163
+ const configDirPath = process.cwd(); // TODO: This should be passed from caller
164
+ const resolvedPath = resolveFunctionDirectory(
165
+ functionName,
166
+ configDirPath,
167
+ functionConfig.dirPath,
168
+ functionPath
169
+ );
195
170
 
196
- if (!fs.existsSync(resolvedPath)) {
197
- throw new Error(`Function directory not found at ${resolvedPath}`);
171
+ if (!validateFunctionDirectory(resolvedPath)) {
172
+ throw new Error(`Function directory is invalid or missing required files: ${resolvedPath}`);
198
173
  }
199
174
 
200
175
  if (functionConfig.predeployCommands?.length) {
@@ -17,6 +17,7 @@ import {
17
17
  import chalk from "chalk";
18
18
  import { extract as extractTar } from "tar";
19
19
  import { MessageFormatter } from "../shared/messageFormatter.js";
20
+ import { expandTildePath, normalizeFunctionName } from "./pathResolution.js";
20
21
 
21
22
  /**
22
23
  * Validates and filters events array for Appwrite functions
@@ -72,7 +73,7 @@ export const downloadLatestFunctionDeployment = async (
72
73
  // Create function directory using provided basePath
73
74
  const functionDir = join(
74
75
  basePath,
75
- functionInfo.name.toLowerCase().replace(/\s+/g, "-")
76
+ normalizeFunctionName(functionInfo.name)
76
77
  );
77
78
  await fs.promises.mkdir(functionDir, { recursive: true });
78
79
 
@@ -219,7 +220,8 @@ export const createFunctionTemplate = async (
219
220
  functionName: string,
220
221
  basePath: string = "./functions"
221
222
  ) => {
222
- const functionPath = join(basePath, functionName);
223
+ const expandedBasePath = expandTildePath(basePath);
224
+ const functionPath = join(expandedBasePath, functionName);
223
225
  const currentFileUrl = import.meta.url;
224
226
  const currentDir = dirname(fileURLToPath(currentFileUrl));
225
227
  const templatesPath = join(currentDir, "templates", templateType);
@@ -0,0 +1,227 @@
1
+ import { existsSync, statSync, readdirSync } from 'node:fs';
2
+ import { join, resolve, isAbsolute } from 'node:path';
3
+ import { homedir } from 'node:os';
4
+ import { MessageFormatter } from '../shared/messageFormatter.js';
5
+ import { logger } from '../shared/logging.js';
6
+
7
+ /**
8
+ * Expands tilde (~) in paths to the user's home directory
9
+ * @param pathStr - Path string that may contain ~
10
+ * @returns Expanded path with home directory
11
+ */
12
+ export function expandTildePath(pathStr: string): string {
13
+ if (!pathStr) return pathStr;
14
+
15
+ if (pathStr.startsWith('~/') || pathStr === '~') {
16
+ const expandedPath = pathStr.replace(/^~(?=$|\/|\\)/, homedir());
17
+ logger.debug('Expanded tilde path', { original: pathStr, expanded: expandedPath });
18
+ return expandedPath;
19
+ }
20
+
21
+ return pathStr;
22
+ }
23
+
24
+ /**
25
+ * Normalizes function name to standard format (lowercase, dashes instead of spaces)
26
+ * @param name - Function name to normalize
27
+ * @returns Normalized function name
28
+ */
29
+ export function normalizeFunctionName(name: string): string {
30
+ if (!name) return name;
31
+
32
+ const normalized = name.toLowerCase().replace(/\s+/g, '-');
33
+
34
+ if (normalized !== name) {
35
+ logger.debug('Normalized function name', { original: name, normalized });
36
+ }
37
+
38
+ return normalized;
39
+ }
40
+
41
+ /**
42
+ * Validates that a directory exists and contains function markers
43
+ * @param dirPath - Directory path to validate
44
+ * @returns True if directory is a valid function directory
45
+ */
46
+ export function validateFunctionDirectory(dirPath: string): boolean {
47
+ try {
48
+ // Check if directory exists
49
+ if (!existsSync(dirPath)) {
50
+ logger.debug('Directory does not exist', { dirPath });
51
+ return false;
52
+ }
53
+
54
+ // Check if it's actually a directory
55
+ const stats = statSync(dirPath);
56
+ if (!stats.isDirectory()) {
57
+ logger.debug('Path is not a directory', { dirPath });
58
+ return false;
59
+ }
60
+
61
+ // Check for function markers
62
+ const contents = readdirSync(dirPath);
63
+ const hasPackageJson = contents.includes('package.json');
64
+ const hasPyprojectToml = contents.includes('pyproject.toml');
65
+ const hasSrcDir = contents.includes('src');
66
+
67
+ const isValid = hasPackageJson || hasPyprojectToml || hasSrcDir;
68
+
69
+ logger.debug('Function directory validation', {
70
+ dirPath,
71
+ isValid,
72
+ markers: {
73
+ hasPackageJson,
74
+ hasPyprojectToml,
75
+ hasSrcDir
76
+ }
77
+ });
78
+
79
+ return isValid;
80
+ } catch (error) {
81
+ logger.debug('Error validating function directory', {
82
+ dirPath,
83
+ error: error instanceof Error ? error.message : String(error)
84
+ });
85
+ return false;
86
+ }
87
+ }
88
+
89
+ /**
90
+ * Helper function to search for function in standard locations
91
+ * @param configDirPath - Directory where config file is located
92
+ * @param normalizedName - Normalized function name
93
+ * @returns First valid function directory path or undefined
94
+ */
95
+ export function findFunctionInStandardLocations(
96
+ configDirPath: string,
97
+ normalizedName: string
98
+ ): string | undefined {
99
+ const searchPaths = [
100
+ // Same directory as config
101
+ join(configDirPath, 'functions', normalizedName),
102
+ // Parent directory of config
103
+ join(configDirPath, '..', 'functions', normalizedName),
104
+ // Current working directory
105
+ join(process.cwd(), 'functions', normalizedName),
106
+ ];
107
+
108
+ logger.debug('Searching for function in standard locations', {
109
+ normalizedName,
110
+ configDirPath,
111
+ searchPaths
112
+ });
113
+
114
+ for (const searchPath of searchPaths) {
115
+ const resolvedPath = resolve(searchPath);
116
+ logger.debug('Checking search path', { searchPath, resolvedPath });
117
+
118
+ if (validateFunctionDirectory(resolvedPath)) {
119
+ logger.debug('Found function in standard location', { resolvedPath });
120
+ return resolvedPath;
121
+ }
122
+ }
123
+
124
+ logger.debug('Function not found in any standard location', { normalizedName });
125
+ return undefined;
126
+ }
127
+
128
+ /**
129
+ * Resolves the absolute path to a function directory
130
+ * Handles multiple resolution strategies with proper priority
131
+ *
132
+ * @param functionName - Name of the function
133
+ * @param configDirPath - Directory where config file is located
134
+ * @param dirPath - Optional explicit dirPath from config
135
+ * @param explicitPath - Optional path passed as parameter (highest priority)
136
+ * @returns Absolute path to the function directory
137
+ * @throws Error if function directory cannot be found or is invalid
138
+ */
139
+ export function resolveFunctionDirectory(
140
+ functionName: string,
141
+ configDirPath: string,
142
+ dirPath?: string,
143
+ explicitPath?: string
144
+ ): string {
145
+ logger.debug('Resolving function directory', {
146
+ functionName,
147
+ configDirPath,
148
+ dirPath,
149
+ explicitPath
150
+ });
151
+
152
+ const normalizedName = normalizeFunctionName(functionName);
153
+
154
+ // Priority 1: Explicit path parameter (highest priority)
155
+ if (explicitPath) {
156
+ logger.debug('Using explicit path parameter');
157
+ const expandedPath = expandTildePath(explicitPath);
158
+ const resolvedPath = isAbsolute(expandedPath)
159
+ ? expandedPath
160
+ : resolve(process.cwd(), expandedPath);
161
+
162
+ if (!validateFunctionDirectory(resolvedPath)) {
163
+ const errorMsg = `Explicit path is not a valid function directory: ${resolvedPath}`;
164
+ logger.error(errorMsg);
165
+ MessageFormatter.error('Invalid function directory', errorMsg, { prefix: 'Path Resolution' });
166
+ throw new Error(errorMsg);
167
+ }
168
+
169
+ logger.debug('Resolved using explicit path', { resolvedPath });
170
+ MessageFormatter.debug(`Resolved function directory using explicit path: ${resolvedPath}`, undefined, { prefix: 'Path Resolution' });
171
+ return resolvedPath;
172
+ }
173
+
174
+ // Priority 2: dirPath from config (relative to config location)
175
+ if (dirPath) {
176
+ logger.debug('Using dirPath from config');
177
+ const expandedPath = expandTildePath(dirPath);
178
+ const resolvedPath = isAbsolute(expandedPath)
179
+ ? expandedPath
180
+ : resolve(configDirPath, expandedPath);
181
+
182
+ if (!validateFunctionDirectory(resolvedPath)) {
183
+ const errorMsg = `Config dirPath is not a valid function directory: ${resolvedPath}`;
184
+ logger.error(errorMsg);
185
+ MessageFormatter.error('Invalid function directory', errorMsg, { prefix: 'Path Resolution' });
186
+ throw new Error(errorMsg);
187
+ }
188
+
189
+ logger.debug('Resolved using config dirPath', { resolvedPath });
190
+ MessageFormatter.debug(`Resolved function directory using config dirPath: ${resolvedPath}`, undefined, { prefix: 'Path Resolution' });
191
+ return resolvedPath;
192
+ }
193
+
194
+ // Priority 3: Search standard locations
195
+ logger.debug('Searching standard locations for function');
196
+ const foundPath = findFunctionInStandardLocations(configDirPath, normalizedName);
197
+
198
+ if (foundPath) {
199
+ logger.debug('Resolved using standard location search', { foundPath });
200
+ MessageFormatter.debug(`Found function directory in standard location: ${foundPath}`, undefined, { prefix: 'Path Resolution' });
201
+ return foundPath;
202
+ }
203
+
204
+ // Priority 4: Not found - throw error
205
+ const searchedLocations = [
206
+ join(configDirPath, 'functions', normalizedName),
207
+ join(configDirPath, '..', 'functions', normalizedName),
208
+ join(process.cwd(), 'functions', normalizedName),
209
+ ];
210
+
211
+ const errorMsg = `Function directory not found for '${functionName}' (normalized: '${normalizedName}'). ` +
212
+ `Searched locations:\n${searchedLocations.map(p => ` - ${p}`).join('\n')}`;
213
+
214
+ logger.error('Function directory not found', {
215
+ functionName,
216
+ normalizedName,
217
+ searchedLocations
218
+ });
219
+
220
+ MessageFormatter.error(
221
+ 'Function directory not found',
222
+ errorMsg,
223
+ { prefix: 'Path Resolution' }
224
+ );
225
+
226
+ throw new Error(errorMsg);
227
+ }