appwrite-utils-cli 1.8.2 → 1.8.5

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 (51) hide show
  1. package/CHANGELOG.md +6 -1
  2. package/README.md +42 -13
  3. package/dist/adapters/TablesDBAdapter.js +1 -1
  4. package/dist/cli/commands/functionCommands.js +30 -3
  5. package/dist/cli/commands/schemaCommands.js +39 -4
  6. package/dist/cli/commands/storageCommands.d.ts +5 -0
  7. package/dist/cli/commands/storageCommands.js +143 -0
  8. package/dist/collections/attributes.js +7 -7
  9. package/dist/collections/methods.js +1 -1
  10. package/dist/collections/tableOperations.js +2 -2
  11. package/dist/interactiveCLI.d.ts +1 -0
  12. package/dist/interactiveCLI.js +30 -0
  13. package/dist/main.js +17 -0
  14. package/dist/migrations/appwriteToX.js +1 -1
  15. package/dist/migrations/yaml/generateImportSchemas.js +2 -2
  16. package/dist/setupCommands.js +6 -0
  17. package/dist/shared/attributeMapper.js +2 -2
  18. package/dist/shared/jsonSchemaGenerator.js +3 -1
  19. package/dist/shared/pydanticModelGenerator.d.ts +17 -0
  20. package/dist/shared/pydanticModelGenerator.js +615 -0
  21. package/dist/shared/schemaGenerator.d.ts +3 -2
  22. package/dist/shared/schemaGenerator.js +22 -9
  23. package/dist/storage/methods.js +50 -7
  24. package/dist/utils/configDiscovery.js +2 -3
  25. package/dist/utils/constantsGenerator.d.ts +20 -8
  26. package/dist/utils/constantsGenerator.js +37 -25
  27. package/dist/utils/projectConfig.js +1 -1
  28. package/dist/utils/yamlConverter.d.ts +2 -2
  29. package/dist/utils/yamlConverter.js +2 -2
  30. package/package.json +1 -1
  31. package/src/adapters/TablesDBAdapter.ts +1 -1
  32. package/src/cli/commands/functionCommands.ts +28 -3
  33. package/src/cli/commands/schemaCommands.ts +59 -22
  34. package/src/cli/commands/storageCommands.ts +152 -0
  35. package/src/collections/attributes.ts +7 -7
  36. package/src/collections/methods.ts +7 -7
  37. package/src/collections/tableOperations.ts +2 -2
  38. package/src/interactiveCLI.ts +42 -12
  39. package/src/main.ts +32 -9
  40. package/src/migrations/appwriteToX.ts +1 -1
  41. package/src/migrations/yaml/generateImportSchemas.ts +7 -7
  42. package/src/setupCommands.ts +6 -0
  43. package/src/shared/attributeMapper.ts +2 -2
  44. package/src/shared/jsonSchemaGenerator.ts +4 -2
  45. package/src/shared/pydanticModelGenerator.ts +618 -0
  46. package/src/shared/schemaGenerator.ts +38 -25
  47. package/src/storage/methods.ts +67 -23
  48. package/src/utils/configDiscovery.ts +40 -41
  49. package/src/utils/constantsGenerator.ts +43 -26
  50. package/src/utils/projectConfig.ts +11 -11
  51. package/src/utils/yamlConverter.ts +40 -40
@@ -44,8 +44,54 @@ export const ensureDatabaseConfigBucketsExist = async (storage, config, database
44
44
  const database = config.databases?.find((d) => d.$id === db.$id);
45
45
  if (database?.bucket) {
46
46
  try {
47
- await storage.getBucket(database.bucket.$id);
48
- console.log(`Bucket ${database.bucket.$id} already exists.`);
47
+ const existing = await storage.getBucket(database.bucket.$id);
48
+ // Compare and update if needed
49
+ const desired = database.bucket;
50
+ // Build desired permissions via Permission helper
51
+ const permissions = [];
52
+ if (desired.permissions && desired.permissions.length > 0) {
53
+ for (const p of desired.permissions) {
54
+ switch (p.permission) {
55
+ case 'read':
56
+ permissions.push(Permission.read(p.target));
57
+ break;
58
+ case 'create':
59
+ permissions.push(Permission.create(p.target));
60
+ break;
61
+ case 'update':
62
+ permissions.push(Permission.update(p.target));
63
+ break;
64
+ case 'delete':
65
+ permissions.push(Permission.delete(p.target));
66
+ break;
67
+ case 'write':
68
+ permissions.push(Permission.write(p.target));
69
+ break;
70
+ default: break;
71
+ }
72
+ }
73
+ }
74
+ const diff = (existing.name !== desired.name ||
75
+ JSON.stringify(existing.$permissions || []) !== JSON.stringify(permissions) ||
76
+ !!existing.fileSecurity !== !!desired.fileSecurity ||
77
+ !!existing.enabled !== !!desired.enabled ||
78
+ (existing.maximumFileSize ?? undefined) !== (desired.maximumFileSize ?? undefined) ||
79
+ JSON.stringify(existing.allowedFileExtensions || []) !== JSON.stringify(desired.allowedFileExtensions || []) ||
80
+ String(existing.compression || 'none') !== String(desired.compression || 'none') ||
81
+ !!existing.encryption !== !!desired.encryption ||
82
+ !!existing.antivirus !== !!desired.antivirus);
83
+ if (diff) {
84
+ try {
85
+ await storage.updateBucket(desired.$id, desired.name, permissions, desired.fileSecurity, desired.enabled, desired.maximumFileSize, desired.allowedFileExtensions, desired.compression, desired.encryption, desired.antivirus);
86
+ MessageFormatter.info(`Updated bucket ${desired.$id} to match config`, { prefix: 'Buckets' });
87
+ }
88
+ catch (updateErr) {
89
+ MessageFormatter.warning(`Failed to update bucket ${desired.$id}: ${updateErr instanceof Error ? updateErr.message : String(updateErr)}`, { prefix: 'Buckets' });
90
+ }
91
+ }
92
+ else {
93
+ MessageFormatter.debug(`Bucket ${desired.$id} up-to-date`, undefined, { prefix: 'Buckets' });
94
+ }
49
95
  }
50
96
  catch (e) {
51
97
  const permissions = [];
@@ -76,13 +122,10 @@ export const ensureDatabaseConfigBucketsExist = async (storage, config, database
76
122
  }
77
123
  try {
78
124
  await storage.createBucket(database.bucket.$id, database.bucket.name, permissions, database.bucket.fileSecurity, database.bucket.enabled, database.bucket.maximumFileSize, database.bucket.allowedFileExtensions, database.bucket.compression, database.bucket.encryption, database.bucket.antivirus);
79
- console.log(`Bucket ${database.bucket.$id} created successfully.`);
125
+ MessageFormatter.success(`Bucket ${database.bucket.$id} created`, { prefix: 'Buckets' });
80
126
  }
81
127
  catch (createError) {
82
- // console.error(
83
- // `Failed to create bucket ${database.bucket.$id}:`,
84
- // createError
85
- // );
128
+ MessageFormatter.error(`Failed to create bucket ${database.bucket.$id}`, createError instanceof Error ? createError : new Error(String(createError)), { prefix: 'Buckets' });
86
129
  }
87
130
  }
88
131
  }
@@ -152,7 +152,7 @@ const YamlTableSchema = z.object({
152
152
  size: z.number().optional(),
153
153
  required: z.boolean().default(false),
154
154
  array: z.boolean().optional(),
155
- encrypted: z.boolean().optional(), // Tables support encrypted property
155
+ encrypt: z.boolean().optional(), // Tables support encrypt property
156
156
  default: z.any().optional(),
157
157
  min: z.number().optional(),
158
158
  max: z.number().optional(),
@@ -163,7 +163,6 @@ const YamlTableSchema = z.object({
163
163
  twoWayKey: z.string().optional(),
164
164
  onDelete: z.string().optional(),
165
165
  side: z.string().optional(),
166
- encrypt: z.boolean().optional(),
167
166
  format: z.string().optional()
168
167
  })).optional().default([]),
169
168
  indexes: z.array(z.object({
@@ -266,7 +265,7 @@ export const loadYamlTable = (filePath) => {
266
265
  twoWayKey: col.twoWayKey,
267
266
  onDelete: col.onDelete,
268
267
  side: col.side,
269
- encrypted: col.encrypted || col.encrypt, // Support both encrypted and encrypt
268
+ encrypt: col.encrypt,
270
269
  format: col.format
271
270
  })),
272
271
  indexes: parsedTable.indexes.map(idx => ({
@@ -1,5 +1,11 @@
1
1
  import { type AppwriteConfig } from "appwrite-utils";
2
2
  export type SupportedLanguage = "typescript" | "javascript" | "python" | "php" | "dart" | "json" | "env";
3
+ interface Constants {
4
+ databases: Record<string, string>;
5
+ collections: Record<string, string>;
6
+ buckets: Record<string, string>;
7
+ functions: Record<string, string>;
8
+ }
3
9
  export declare class ConstantsGenerator {
4
10
  private config;
5
11
  private constants;
@@ -8,12 +14,18 @@ export declare class ConstantsGenerator {
8
14
  private toConstantName;
9
15
  private toCamelCase;
10
16
  private toSnakeCase;
11
- generateTypeScript(): string;
12
- generateJavaScript(): string;
13
- generatePython(): string;
14
- generatePHP(): string;
15
- generateDart(): string;
16
- generateJSON(): string;
17
- generateEnv(): string;
18
- generateFiles(languages: SupportedLanguage[], outputDir: string): Promise<void>;
17
+ generateTypeScript(constantsOverride?: Constants): string;
18
+ generateJavaScript(constantsOverride?: Constants): string;
19
+ generatePython(constantsOverride?: Constants): string;
20
+ generatePHP(constantsOverride?: Constants): string;
21
+ generateDart(constantsOverride?: Constants): string;
22
+ generateJSON(constantsOverride?: Constants): string;
23
+ generateEnv(constantsOverride?: Constants): string;
24
+ generateFiles(languages: SupportedLanguage[], outputDir: string, include?: {
25
+ databases?: boolean;
26
+ collections?: boolean;
27
+ buckets?: boolean;
28
+ functions?: boolean;
29
+ }): Promise<void>;
19
30
  }
31
+ export {};
@@ -61,8 +61,8 @@ export class ConstantsGenerator {
61
61
  toSnakeCase(name) {
62
62
  return name.toLowerCase();
63
63
  }
64
- generateTypeScript() {
65
- const { databases, collections, buckets, functions } = this.constants;
64
+ generateTypeScript(constantsOverride) {
65
+ const { databases, collections, buckets, functions } = constantsOverride || this.constants;
66
66
  return `// Auto-generated Appwrite constants
67
67
  // Generated on ${new Date().toISOString()}
68
68
 
@@ -95,8 +95,8 @@ export const ALL_BUCKET_IDS = Object.values(BUCKET_IDS);
95
95
  export const ALL_FUNCTION_IDS = Object.values(FUNCTION_IDS);
96
96
  `;
97
97
  }
98
- generateJavaScript() {
99
- const { databases, collections, buckets, functions } = this.constants;
98
+ generateJavaScript(constantsOverride) {
99
+ const { databases, collections, buckets, functions } = constantsOverride || this.constants;
100
100
  return `// Auto-generated Appwrite constants
101
101
  // Generated on ${new Date().toISOString()}
102
102
 
@@ -123,8 +123,8 @@ export const ALL_BUCKET_IDS = Object.values(BUCKET_IDS);
123
123
  export const ALL_FUNCTION_IDS = Object.values(FUNCTION_IDS);
124
124
  `;
125
125
  }
126
- generatePython() {
127
- const { databases, collections, buckets, functions } = this.constants;
126
+ generatePython(constantsOverride) {
127
+ const { databases, collections, buckets, functions } = constantsOverride || this.constants;
128
128
  return `# Auto-generated Appwrite constants
129
129
  # Generated on ${new Date().toISOString()}
130
130
 
@@ -162,8 +162,8 @@ ${Object.entries(functions).map(([key, value]) => ` "${this.toSnakeCase(key)}
162
162
  }
163
163
  `;
164
164
  }
165
- generatePHP() {
166
- const { databases, collections, buckets, functions } = this.constants;
165
+ generatePHP(constantsOverride) {
166
+ const { databases, collections, buckets, functions } = constantsOverride || this.constants;
167
167
  return `<?php
168
168
  // Auto-generated Appwrite constants
169
169
  // Generated on ${new Date().toISOString()}
@@ -216,8 +216,8 @@ ${Object.entries(functions).map(([key, value]) => ` '${key}' => '${value}
216
216
  }
217
217
  `;
218
218
  }
219
- generateDart() {
220
- const { databases, collections, buckets, functions } = this.constants;
219
+ generateDart(constantsOverride) {
220
+ const { databases, collections, buckets, functions } = constantsOverride || this.constants;
221
221
  return `// Auto-generated Appwrite constants
222
222
  // Generated on ${new Date().toISOString()}
223
223
 
@@ -250,20 +250,21 @@ ${Object.entries(functions).map(([key, value]) => ` static String get ${this.to
250
250
  }
251
251
  `;
252
252
  }
253
- generateJSON() {
253
+ generateJSON(constantsOverride) {
254
+ const c = constantsOverride || this.constants;
254
255
  return JSON.stringify({
255
256
  meta: {
256
257
  generated: new Date().toISOString(),
257
258
  generator: "appwrite-utils-cli"
258
259
  },
259
- databases: this.constants.databases,
260
- collections: this.constants.collections,
261
- buckets: this.constants.buckets,
262
- functions: this.constants.functions
260
+ databases: c.databases,
261
+ collections: c.collections,
262
+ buckets: c.buckets,
263
+ functions: c.functions
263
264
  }, null, 2);
264
265
  }
265
- generateEnv() {
266
- const { databases, collections, buckets, functions } = this.constants;
266
+ generateEnv(constantsOverride) {
267
+ const { databases, collections, buckets, functions } = constantsOverride || this.constants;
267
268
  const lines = [
268
269
  "# Auto-generated Appwrite constants",
269
270
  `# Generated on ${new Date().toISOString()}`,
@@ -282,16 +283,27 @@ ${Object.entries(functions).map(([key, value]) => ` static String get ${this.to
282
283
  ];
283
284
  return lines.join('\n');
284
285
  }
285
- async generateFiles(languages, outputDir) {
286
+ async generateFiles(languages, outputDir, include) {
286
287
  await fs.mkdir(outputDir, { recursive: true });
288
+ const filterConstants = () => {
289
+ if (!include)
290
+ return this.constants;
291
+ return {
292
+ databases: include.databases === false ? {} : this.constants.databases,
293
+ collections: include.collections === false ? {} : this.constants.collections,
294
+ buckets: include.buckets === false ? {} : this.constants.buckets,
295
+ functions: include.functions === false ? {} : this.constants.functions,
296
+ };
297
+ };
298
+ const subset = filterConstants();
287
299
  const generators = {
288
- typescript: () => ({ content: this.generateTypeScript(), filename: "appwrite-constants.ts" }),
289
- javascript: () => ({ content: this.generateJavaScript(), filename: "appwrite-constants.js" }),
290
- python: () => ({ content: this.generatePython(), filename: "appwrite_constants.py" }),
291
- php: () => ({ content: this.generatePHP(), filename: "AppwriteConstants.php" }),
292
- dart: () => ({ content: this.generateDart(), filename: "appwrite_constants.dart" }),
293
- json: () => ({ content: this.generateJSON(), filename: "appwrite-constants.json" }),
294
- env: () => ({ content: this.generateEnv(), filename: ".env.appwrite" })
300
+ typescript: () => ({ content: this.generateTypeScript(subset), filename: "appwrite-constants.ts" }),
301
+ javascript: () => ({ content: this.generateJavaScript(subset), filename: "appwrite-constants.js" }),
302
+ python: () => ({ content: this.generatePython(subset), filename: "appwrite_constants.py" }),
303
+ php: () => ({ content: this.generatePHP(subset), filename: "AppwriteConstants.php" }),
304
+ dart: () => ({ content: this.generateDart(subset), filename: "appwrite_constants.dart" }),
305
+ json: () => ({ content: this.generateJSON(subset), filename: "appwrite-constants.json" }),
306
+ env: () => ({ content: this.generateEnv(subset), filename: ".env.appwrite" })
295
307
  };
296
308
  for (const language of languages) {
297
309
  const generator = generators[language];
@@ -134,7 +134,7 @@ export function getCollectionsFromProject(projectConfig) {
134
134
  array: col.array,
135
135
  size: col.size,
136
136
  default: col.default,
137
- encrypted: col.encrypt,
137
+ encrypt: col.encrypt,
138
138
  unique: col.unique,
139
139
  })),
140
140
  indexes: table.indexes || [],
@@ -15,7 +15,7 @@ export interface YamlCollectionData {
15
15
  size?: number;
16
16
  required?: boolean;
17
17
  array?: boolean;
18
- encrypted?: boolean;
18
+ encrypt?: boolean;
19
19
  default?: any;
20
20
  min?: number;
21
21
  max?: number;
@@ -33,7 +33,7 @@ export interface YamlCollectionData {
33
33
  size?: number;
34
34
  required?: boolean;
35
35
  array?: boolean;
36
- encrypted?: boolean;
36
+ encrypt?: boolean;
37
37
  default?: any;
38
38
  min?: number;
39
39
  max?: number;
@@ -125,9 +125,9 @@ export function collectionToYaml(collection, config = {
125
125
  yamlAttr.required = attr.required;
126
126
  if (attr.array !== undefined)
127
127
  yamlAttr.array = attr.array;
128
- // Always include encrypted field for string attributes (default to false)
128
+ // Always include encrypt field for string attributes (default to false)
129
129
  if (attr.type === 'string') {
130
- yamlAttr.encrypted = ('encrypted' in attr && attr.encrypted === true) ? true : false;
130
+ yamlAttr.encrypt = ('encrypt' in attr && attr.encrypt === true) ? true : false;
131
131
  }
132
132
  if ('xdefault' in attr && attr.xdefault !== undefined)
133
133
  yamlAttr.default = attr.xdefault;
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "appwrite-utils-cli",
3
3
  "description": "Appwrite Utility Functions to help with database management, data conversion, data import, migrations, and much more. Meant to be used as a CLI tool, I do not recommend installing this in frontend environments.",
4
- "version": "1.8.2",
4
+ "version": "1.8.5",
5
5
  "main": "src/main.ts",
6
6
  "type": "module",
7
7
  "repository": {
@@ -315,7 +315,7 @@ export class TablesDBAdapter extends BaseAdapter {
315
315
  const type = (params.type || "").toLowerCase();
316
316
  const required = params.required ?? false;
317
317
  const array = params.array ?? false;
318
- const encrypt = params.encrypt ?? (params as any).encrypted ?? false;
318
+ const encrypt = params.encrypt ?? false;
319
319
  const normalizedDefault =
320
320
  params.default === null || params.default === undefined
321
321
  ? undefined
@@ -138,11 +138,36 @@ export const functionCommands = {
138
138
  return;
139
139
  }
140
140
 
141
- // Discover local .fnconfig.yaml functions and merge into controller config
141
+ // Offer choice of function config sources: central YAML, .fnconfig.yaml, or both
142
+ let sourceChoice: 'central' | 'fnconfig' | 'both' = 'both';
143
+ try {
144
+ const answer = await inquirer.prompt([
145
+ {
146
+ type: 'list',
147
+ name: 'source',
148
+ message: 'Select function config source:',
149
+ choices: [
150
+ { name: 'config.yaml functions (central)', value: 'central' },
151
+ { name: '.fnconfig.yaml (discovered per-function)', value: 'fnconfig' },
152
+ { name: 'Both (merge; .fnconfig overrides)', value: 'both' },
153
+ ],
154
+ default: 'both'
155
+ }
156
+ ]);
157
+ sourceChoice = answer.source;
158
+ } catch {}
159
+
142
160
  try {
143
161
  const discovered = discoverFnConfigs((cli as any).currentDir);
144
- const merged = mergeDiscoveredFunctions((cli as any).controller!.config!.functions || [], discovered);
145
- (cli as any).controller!.config!.functions = merged as any;
162
+ const central = (cli as any).controller!.config!.functions || [];
163
+ if (sourceChoice === 'central') {
164
+ (cli as any).controller!.config!.functions = central as any;
165
+ } else if (sourceChoice === 'fnconfig') {
166
+ (cli as any).controller!.config!.functions = discovered as any;
167
+ } else {
168
+ const merged = mergeDiscoveredFunctions(central, discovered);
169
+ (cli as any).controller!.config!.functions = merged as any;
170
+ }
146
171
  } catch {}
147
172
 
148
173
  const functions = await (cli as any).selectFunctions(
@@ -12,19 +12,21 @@ export const schemaCommands = {
12
12
  MessageFormatter.progress("Generating schemas...", { prefix: "Schemas" });
13
13
 
14
14
  // Prompt user for schema type preference
15
- const { schemaType } = await inquirer.prompt([
16
- {
17
- type: "list",
18
- name: "schemaType",
19
- message: "What type of schemas would you like to generate?",
20
- choices: [
21
- { name: "TypeScript (Zod) schemas", value: "zod" },
22
- { name: "JSON schemas", value: "json" },
23
- { name: "Both TypeScript and JSON schemas", value: "both" },
24
- ],
25
- default: "both",
26
- },
27
- ]);
15
+ const { schemaType } = await inquirer.prompt([
16
+ {
17
+ type: "list",
18
+ name: "schemaType",
19
+ message: "What type of schemas would you like to generate?",
20
+ choices: [
21
+ { name: "TypeScript (Zod) schemas", value: "zod" },
22
+ { name: "JSON schemas", value: "json" },
23
+ { name: "Python (Pydantic) models", value: "pydantic" },
24
+ { name: "TypeScript + JSON", value: "both" },
25
+ { name: "All (Zod, JSON, Pydantic)", value: "all" },
26
+ ],
27
+ default: "all",
28
+ },
29
+ ]);
28
30
 
29
31
  // Get the config folder path (where the config file is located)
30
32
  const configFolderPath = (cli as any).controller!.getAppwriteFolderPath();
@@ -33,14 +35,27 @@ export const schemaCommands = {
33
35
  return;
34
36
  }
35
37
 
36
- // Create SchemaGenerator with the correct base path and generate schemas
37
- const schemaGenerator = new SchemaGenerator((cli as any).controller!.config!, configFolderPath);
38
- schemaGenerator.generateSchemas({ format: schemaType, verbose: true });
38
+ // Prompt for schema output directory (optional override)
39
+ const defaultSchemaOut = path.join(configFolderPath, (cli as any).controller!.config?.schemaConfig?.outputDirectory || 'schemas');
40
+ const { schemaOutDir } = await inquirer.prompt([
41
+ {
42
+ type: 'input',
43
+ name: 'schemaOutDir',
44
+ message: 'Output directory for schemas:',
45
+ default: defaultSchemaOut,
46
+ validate: (input: string) => input && input.trim().length > 0 ? true : 'Please provide an output directory',
47
+ }
48
+ ]);
49
+
50
+ // Create SchemaGenerator with the correct base path and generate schemas
51
+ const schemaGenerator = new SchemaGenerator((cli as any).controller!.config!, configFolderPath);
52
+ const outDirRel = path.isAbsolute(schemaOutDir) ? schemaOutDir : path.relative(configFolderPath, schemaOutDir);
53
+ await schemaGenerator.generateSchemas({ format: schemaType as any, verbose: true, outputDir: outDirRel });
39
54
 
40
55
  MessageFormatter.success("Schema generation completed", { prefix: "Schemas" });
41
56
  },
42
57
 
43
- async generateConstants(cli: InteractiveCLI): Promise<void> {
58
+ async generateConstants(cli: InteractiveCLI): Promise<void> {
44
59
  MessageFormatter.progress("Generating cross-language constants...", { prefix: "Constants" });
45
60
 
46
61
  if (!(cli as any).controller?.config) {
@@ -48,8 +63,8 @@ export const schemaCommands = {
48
63
  return;
49
64
  }
50
65
 
51
- // Prompt for languages
52
- const { languages } = await inquirer.prompt([
66
+ // Prompt for languages
67
+ const { languages } = await inquirer.prompt([
53
68
  {
54
69
  type: "checkbox",
55
70
  name: "languages",
@@ -70,7 +85,23 @@ export const schemaCommands = {
70
85
  return true;
71
86
  },
72
87
  },
73
- ]);
88
+ ]);
89
+
90
+ // Prompt for which constants to include
91
+ const { includeWhat } = await inquirer.prompt([
92
+ {
93
+ type: 'checkbox',
94
+ name: 'includeWhat',
95
+ message: 'Select which constants to generate:',
96
+ choices: [
97
+ { name: 'Databases', value: 'databases', checked: true },
98
+ { name: 'Collections/Tables', value: 'collections', checked: true },
99
+ { name: 'Buckets', value: 'buckets', checked: true },
100
+ { name: 'Functions', value: 'functions', checked: true },
101
+ ],
102
+ validate: (input) => input.length > 0 ? true : 'Select at least one category',
103
+ }
104
+ ]);
74
105
 
75
106
  // Determine default output directory based on config location
76
107
  const configPath = (cli as any).controller!.getAppwriteFolderPath();
@@ -98,8 +129,14 @@ export const schemaCommands = {
98
129
  const { ConstantsGenerator } = await import("../../utils/constantsGenerator.js");
99
130
  const generator = new ConstantsGenerator((cli as any).controller.config);
100
131
 
101
- MessageFormatter.info(`Generating constants for: ${languages.join(", ")}`, { prefix: "Constants" });
102
- await generator.generateFiles(languages, outputDir);
132
+ const include = {
133
+ databases: includeWhat.includes('databases'),
134
+ collections: includeWhat.includes('collections'),
135
+ buckets: includeWhat.includes('buckets'),
136
+ functions: includeWhat.includes('functions'),
137
+ };
138
+ MessageFormatter.info(`Generating constants for: ${languages.join(", ")}`, { prefix: "Constants" });
139
+ await generator.generateFiles(languages, outputDir, include);
103
140
 
104
141
  MessageFormatter.success(`Constants generated in ${outputDir}`, { prefix: "Constants" });
105
142
  } catch (error) {
@@ -0,0 +1,152 @@
1
+ import inquirer from "inquirer";
2
+ import chalk from "chalk";
3
+ import { Storage, Permission, Role, Compression, type Models } from "node-appwrite";
4
+ import type { InteractiveCLI } from "../../interactiveCLI.js";
5
+ import { MessageFormatter } from "../../shared/messageFormatter.js";
6
+ import { listBuckets, createBucket as createBucketApi, deleteBucket as deleteBucketApi } from "../../storage/methods.js";
7
+ import { writeYamlConfig } from "../../config/yamlConfig.js";
8
+ import { ConfigManager } from "../../config/ConfigManager.js";
9
+
10
+ export const storageCommands = {
11
+ async createBucket(cli: InteractiveCLI): Promise<void> {
12
+ const storage: Storage = (cli as any).controller!.storage!;
13
+ if (!storage) {
14
+ MessageFormatter.error("Storage client not initialized", undefined, { prefix: "Buckets" });
15
+ return;
16
+ }
17
+
18
+ const answers = await inquirer.prompt([
19
+ { type: 'input', name: 'name', message: 'Bucket name:', validate: (v:string)=> v?.trim()?.length>0 || 'Required' },
20
+ { type: 'input', name: 'id', message: 'Bucket ID (optional):' },
21
+ { type: 'confirm', name: 'publicRead', message: 'Public read access?', default: false },
22
+ { type: 'confirm', name: 'fileSecurity', message: 'Enable file-level security?', default: false },
23
+ { type: 'confirm', name: 'enabled', message: 'Enable bucket?', default: true },
24
+ { type: 'number', name: 'maximumFileSize', message: 'Max file size (bytes, optional):', default: undefined },
25
+ { type: 'input', name: 'allowedFileExtensions', message: 'Allowed extensions (comma separated, optional):', default: '' },
26
+ { type: 'list', name: 'compression', message: 'Compression:', choices: ['none','gzip','zstd'], default: 'none' },
27
+ { type: 'confirm', name: 'encryption', message: 'Enable encryption?', default: false },
28
+ { type: 'confirm', name: 'antivirus', message: 'Enable antivirus?', default: false },
29
+ ]);
30
+
31
+ const permissions: string[] = [];
32
+ if (answers.publicRead) permissions.push(Permission.read(Role.any()));
33
+
34
+ const bucketInput: Omit<Models.Bucket, "$id" | "$createdAt" | "$updatedAt"> = {
35
+ name: answers.name,
36
+ $permissions: permissions,
37
+ fileSecurity: !!answers.fileSecurity,
38
+ enabled: !!answers.enabled,
39
+ maximumFileSize: answers.maximumFileSize || undefined,
40
+ allowedFileExtensions: String(answers.allowedFileExtensions || '')
41
+ .split(',')
42
+ .map((s) => s.trim())
43
+ .filter(Boolean),
44
+ compression: answers.compression as Compression,
45
+ encryption: !!answers.encryption,
46
+ antivirus: !!answers.antivirus,
47
+ } as any;
48
+
49
+ try {
50
+ const created = await createBucketApi(storage, bucketInput, answers.id || undefined);
51
+ MessageFormatter.success(`Bucket '${answers.name}' created`, { prefix: 'Buckets' });
52
+
53
+ // Update in-memory config and persist to YAML as a global bucket entry
54
+ const controller = (cli as any).controller!;
55
+ controller.config.buckets = controller.config.buckets || [];
56
+ controller.config.buckets.push({
57
+ $id: (created as any).$id || answers.id || bucketInput.name,
58
+ name: bucketInput.name,
59
+ permissions: [],
60
+ fileSecurity: bucketInput.fileSecurity,
61
+ enabled: bucketInput.enabled,
62
+ maximumFileSize: bucketInput.maximumFileSize,
63
+ allowedFileExtensions: bucketInput.allowedFileExtensions,
64
+ compression: bucketInput.compression,
65
+ encryption: bucketInput.encryption,
66
+ antivirus: bucketInput.antivirus,
67
+ });
68
+
69
+ const cfgMgr = ConfigManager.getInstance();
70
+ const cfgPath = cfgMgr.getConfigPath();
71
+ if (cfgPath && /\.ya?ml$/i.test(cfgPath)) {
72
+ await writeYamlConfig(cfgPath, controller.config);
73
+ MessageFormatter.info(`Added bucket to config.yaml`, { prefix: 'Buckets' });
74
+ } else {
75
+ MessageFormatter.warning(`Config is not YAML; updated in-memory only. Please update your TypeScript config manually.`, { prefix: 'Buckets' });
76
+ }
77
+ } catch (e) {
78
+ MessageFormatter.error('Failed to create bucket', e instanceof Error ? e : new Error(String(e)), { prefix: 'Buckets' });
79
+ }
80
+ },
81
+
82
+ async deleteBuckets(cli: InteractiveCLI): Promise<void> {
83
+ const storage: Storage = (cli as any).controller!.storage!;
84
+ if (!storage) {
85
+ MessageFormatter.error("Storage client not initialized", undefined, { prefix: "Buckets" });
86
+ return;
87
+ }
88
+
89
+ try {
90
+ const res = await listBuckets(storage);
91
+ const buckets: Models.Bucket[] = res.buckets || [];
92
+ if (buckets.length === 0) {
93
+ MessageFormatter.info('No buckets found', { prefix: 'Buckets' });
94
+ return;
95
+ }
96
+
97
+ const { toDelete } = await inquirer.prompt([
98
+ {
99
+ type: 'checkbox',
100
+ name: 'toDelete',
101
+ message: chalk.red('Select buckets to delete:'),
102
+ choices: buckets.map((b) => ({ name: `${b.name} (${b.$id})`, value: b.$id })),
103
+ pageSize: 10,
104
+ }
105
+ ]);
106
+
107
+ if (!toDelete || toDelete.length === 0) {
108
+ MessageFormatter.info('No buckets selected', { prefix: 'Buckets' });
109
+ return;
110
+ }
111
+
112
+ const { confirm } = await inquirer.prompt([
113
+ { type: 'confirm', name: 'confirm', message: `Delete ${toDelete.length} bucket(s)?`, default: false }
114
+ ]);
115
+ if (!confirm) return;
116
+
117
+ const controller = (cli as any).controller!;
118
+ for (const id of toDelete) {
119
+ try {
120
+ await deleteBucketApi(storage, id);
121
+ MessageFormatter.success(`Deleted bucket ${id}`, { prefix: 'Buckets' });
122
+
123
+ // Remove from in-memory config (global buckets)
124
+ if (Array.isArray(controller.config.buckets)) {
125
+ controller.config.buckets = controller.config.buckets.filter((b: any) => b.$id !== id);
126
+ }
127
+ // Clear database-linked bucket references if matching
128
+ if (Array.isArray(controller.config.databases)) {
129
+ controller.config.databases = controller.config.databases.map((db: any) => ({
130
+ ...db,
131
+ bucket: db.bucket && db.bucket.$id === id ? undefined : db.bucket,
132
+ }));
133
+ }
134
+ } catch (e) {
135
+ MessageFormatter.error(`Failed to delete bucket ${id}`, e instanceof Error ? e : new Error(String(e)), { prefix: 'Buckets' });
136
+ }
137
+ }
138
+
139
+ // Persist YAML changes
140
+ const cfgMgr = ConfigManager.getInstance();
141
+ const cfgPath = cfgMgr.getConfigPath();
142
+ if (cfgPath && /\.ya?ml$/i.test(cfgPath)) {
143
+ await writeYamlConfig(cfgPath, controller.config);
144
+ MessageFormatter.info(`Updated config.yaml after deletion`, { prefix: 'Buckets' });
145
+ } else {
146
+ MessageFormatter.warning(`Config is not YAML; updated in-memory only. Please update your TypeScript config manually.`, { prefix: 'Buckets' });
147
+ }
148
+ } catch (e) {
149
+ MessageFormatter.error('Failed to list buckets', e instanceof Error ? e : new Error(String(e)), { prefix: 'Buckets' });
150
+ }
151
+ }
152
+ };