@adobe/aio-cli-plugin-app-storage 1.2.0 → 1.3.0-pre.2026-02-20.sha-df98265c

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 (37) hide show
  1. package/README.md +722 -1
  2. package/package.json +25 -5
  3. package/src/BaseCommand.js +11 -58
  4. package/src/DBBaseCommand.js +112 -0
  5. package/src/StateBaseCommand.js +87 -0
  6. package/src/commands/app/add/db.js +20 -0
  7. package/src/commands/app/db/collection/create.js +119 -0
  8. package/src/commands/app/db/collection/drop.js +90 -0
  9. package/src/commands/app/db/collection/list.js +100 -0
  10. package/src/commands/app/db/collection/rename.js +98 -0
  11. package/src/commands/app/db/collection/stats.js +94 -0
  12. package/src/commands/app/db/delete.js +89 -0
  13. package/src/commands/app/db/document/count.js +96 -0
  14. package/src/commands/app/db/document/delete.js +95 -0
  15. package/src/commands/app/db/document/find.js +133 -0
  16. package/src/commands/app/db/document/insert.js +147 -0
  17. package/src/commands/app/db/document/replace.js +122 -0
  18. package/src/commands/app/db/document/update.js +144 -0
  19. package/src/commands/app/db/index/create.js +170 -0
  20. package/src/commands/app/db/index/drop.js +87 -0
  21. package/src/commands/app/db/index/list.js +82 -0
  22. package/src/commands/app/db/org/stats.js +111 -0
  23. package/src/commands/app/db/ping.js +77 -0
  24. package/src/commands/app/db/provision.js +190 -0
  25. package/src/commands/app/db/stats.js +101 -0
  26. package/src/commands/app/db/status.js +159 -0
  27. package/src/commands/app/state/delete.js +3 -3
  28. package/src/commands/app/state/get.js +2 -2
  29. package/src/commands/app/state/list.js +3 -3
  30. package/src/commands/app/state/put.js +4 -4
  31. package/src/commands/app/state/stats.js +2 -2
  32. package/src/constants/db.js +32 -0
  33. package/src/constants/global.js +14 -0
  34. package/src/{constants.js → constants/state.js} +3 -0
  35. package/src/utils/inputValidation.js +74 -0
  36. package/src/utils/output.js +35 -0
  37. package/oclif.manifest.json +0 -316
@@ -0,0 +1,82 @@
1
+ /*
2
+ Copyright 2025 Adobe. All rights reserved.
3
+ This file is licensed to you under the Apache License, Version 2.0 (the "License");
4
+ you may not use this file except in compliance with the License. You may obtain a copy
5
+ of the License at http://www.apache.org/licenses/LICENSE-2.0
6
+
7
+ Unless required by applicable law or agreed to in writing, software distributed under
8
+ the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
9
+ OF ANY KIND, either express or implied. See the License for the specific language
10
+ governing permissions and limitations under the License.
11
+ */
12
+
13
+ import { DBBaseCommand } from '../../../../DBBaseCommand.js'
14
+ import { Args } from '@oclif/core'
15
+ import chalk from 'chalk'
16
+ import { isNonEmptyString } from '../../../../utils/inputValidation.js'
17
+ import { prettyJson } from '../../../../utils/output.js'
18
+
19
+ export class List extends DBBaseCommand {
20
+ async run () {
21
+ const { collection } = this.args
22
+
23
+ try {
24
+ this.log(chalk.blue(`Getting indexes from collection '${collection}'...`))
25
+
26
+ const client = await this.db.connect()
27
+ const coll = await client.collection(collection)
28
+
29
+ const result = await coll.getIndexes()
30
+
31
+ this.debugLogger?.info?.('Indexes retrieved successfully:', result)
32
+
33
+ const response = {
34
+ collection,
35
+ namespace: this.rtNamespace,
36
+ timestamp: new Date().toISOString(),
37
+ indexes: result
38
+ }
39
+
40
+ this.log(chalk.green('Indexes retrieved successfully'))
41
+ this.log(chalk.dim(` Namespace: ${this.rtNamespace}`))
42
+ this.log(chalk.dim(` Retrieved: ${new Date().toLocaleString()}`))
43
+ if (result && Array.isArray(result) && result.length > 0) {
44
+ this.log(chalk.dim(` Indexes:\n${prettyJson(result)}`))
45
+ } else {
46
+ this.log(chalk.dim(' No indexes found for this collection'))
47
+ }
48
+
49
+ return response
50
+ } catch (error) {
51
+ this.debugLogger?.error?.('Error getting indexes:', error)
52
+
53
+ this.log(chalk.red('Failed to retrieve indexes'))
54
+ this.log(chalk.dim(` Collection: ${collection}`))
55
+ this.log(chalk.dim(` Namespace: ${this.rtNamespace}`))
56
+ this.log(chalk.dim(` Error: ${error.message}`))
57
+
58
+ this.error(`Failed to retrieve indexes from collection '${collection}': ${error.message}`)
59
+ }
60
+ }
61
+ }
62
+
63
+ List.description = 'Get the list of indexes from a collection in the database'
64
+
65
+ List.examples = [
66
+ '$ aio app db index list users',
67
+ '$ aio app db index list products --json',
68
+ '$ aio app db idx list orders'
69
+ ]
70
+
71
+ List.args = {
72
+ collection: Args.string({
73
+ name: 'collection',
74
+ description: 'The name of the collection to retrieve indexes from',
75
+ required: true,
76
+ parse: input => isNonEmptyString(input, 'Collection name')
77
+ })
78
+ }
79
+
80
+ List.flags = DBBaseCommand.flags
81
+
82
+ List.aliases = ['app:db:idx:list']
@@ -0,0 +1,111 @@
1
+ /*
2
+ Copyright 2025 Adobe. All rights reserved.
3
+ This file is licensed to you under the Apache License, Version 2.0 (the "License");
4
+ you may not use this file except in compliance with the License. You may obtain a copy
5
+ of the License at http://www.apache.org/licenses/LICENSE-2.0
6
+
7
+ Unless required by applicable law or agreed to in writing, software distributed under
8
+ the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
9
+ OF ANY KIND, either express or implied. See the License for the specific language
10
+ governing permissions and limitations under the License.
11
+ */
12
+
13
+ import { DBBaseCommand } from '../../../../DBBaseCommand.js'
14
+ import chalk from 'chalk'
15
+ import { prettyJson } from '../../../../utils/output.js'
16
+ import { Flags } from '@oclif/core'
17
+
18
+ export class OrgStats extends DBBaseCommand {
19
+ async run () {
20
+ const { scale } = this.flags
21
+ try {
22
+ this.log(chalk.blue('Fetching organization\'s database statistics...'))
23
+
24
+ const client = await this.db.connect()
25
+ const stats = await client.orgStats({ scale })
26
+
27
+ this.debugLogger?.info?.('Organization database statistics retrieved:', stats)
28
+
29
+ const result = {
30
+ ...stats,
31
+ timestamp: new Date().toISOString()
32
+ }
33
+
34
+ this.displayStats(stats)
35
+
36
+ return result
37
+ } catch (error) {
38
+ this.debugLogger?.error?.('OrgStats command error:', error)
39
+
40
+ this.log(chalk.red('Failed to retrieve organization database statistics'))
41
+ this.log(chalk.dim(` Namespace: ${this.rtNamespace}`))
42
+ this.log(chalk.dim(` Error: ${error.message}`))
43
+
44
+ this.error(`Failed to fetch organization database statistics: ${error.message}`)
45
+ }
46
+ }
47
+
48
+ displayStats (stats) {
49
+ const { ok, databaseStats, ...orgStats } = stats || {}
50
+
51
+ // Display combined stats first
52
+ this.log(chalk.green('Organization Totals:'))
53
+ if (orgStats && typeof orgStats === 'object' && Object.keys(orgStats).length > 0) {
54
+ Object.entries(orgStats).forEach(([key, value]) => {
55
+ this.log(chalk.dim(` ${key}: ${this.formatValue(value)}`))
56
+ })
57
+ } else {
58
+ this.log(chalk.dim(` Raw Stats: ${this.formatValue(orgStats)}`))
59
+ }
60
+
61
+ this.log('')
62
+ if (databaseStats && Array.isArray(databaseStats) && databaseStats.length > 0) {
63
+ this.log(chalk.green('Database Statistics:'))
64
+ databaseStats.forEach((dbStats) => {
65
+ const { namespace, ...otherStats } = dbStats
66
+ this.log(chalk.dim(` Namespace '${namespace}':`))
67
+ Object.entries(otherStats).forEach(([key, value]) => {
68
+ this.log(chalk.dim(` ${key}: ${this.formatValue(value, 3)}`))
69
+ })
70
+ this.log('')
71
+ })
72
+ }
73
+
74
+ this.log(chalk.dim(` Retrieved: ${new Date().toISOString()}`))
75
+ }
76
+
77
+ formatValue (value) {
78
+ if (typeof value === 'number') {
79
+ // Format large numbers with commas
80
+ return value.toLocaleString()
81
+ }
82
+ if (value instanceof Date) {
83
+ return value.toISOString()
84
+ }
85
+ if (typeof value === 'object' && value !== null) {
86
+ return `\n${prettyJson(value)}`
87
+ }
88
+ return String(value)
89
+ }
90
+ }
91
+
92
+ OrgStats.description = 'Get combined statistics about the App Builder databases in your organization'
93
+
94
+ OrgStats.examples = [
95
+ '$ aio app db org stats',
96
+ '$ aio app db org stats --scale 1024',
97
+ '$ aio app db org stats --json'
98
+ ]
99
+
100
+ OrgStats.flags = {
101
+ ...DBBaseCommand.flags,
102
+ scale: Flags.integer({
103
+ char: 's',
104
+ description: 'Scale factor for size-related statistics (e.g. 1024 for KB, 1048576 for MB).',
105
+ required: false,
106
+ default: 1,
107
+ min: 1
108
+ })
109
+ }
110
+
111
+ OrgStats.args = {}
@@ -0,0 +1,77 @@
1
+ /*
2
+ Copyright 2025 Adobe. All rights reserved.
3
+ This file is licensed to you under the Apache License, Version 2.0 (the "License");
4
+ you may not use this file except in compliance with the License. You may obtain a copy
5
+ of the License at http://www.apache.org/licenses/LICENSE-2.0
6
+
7
+ Unless required by applicable law or agreed to in writing, software distributed under
8
+ the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
9
+ OF ANY KIND, either express or implied. See the License for the specific language
10
+ governing permissions and limitations under the License.
11
+ */
12
+
13
+ import { DBBaseCommand } from '../../../DBBaseCommand.js'
14
+ import chalk from 'chalk'
15
+
16
+ export class Ping extends DBBaseCommand {
17
+ async run () {
18
+ try {
19
+ this.log(chalk.blue('Testing database connectivity...'))
20
+
21
+ const startTime = Date.now()
22
+ const pingResult = await this.db.ping()
23
+ const endTime = Date.now()
24
+ const responseTime = endTime - startTime
25
+
26
+ this.debugLogger?.info?.(`Database ping completed in ${responseTime}ms:`, pingResult)
27
+
28
+ this.log(chalk.green('Database connection successful'))
29
+ this.log(chalk.dim(` Namespace: ${this.rtNamespace}`))
30
+ this.log(chalk.dim(` Response time: ${responseTime}ms`))
31
+
32
+ if (typeof pingResult === 'string') {
33
+ this.log(chalk.dim(` Response: ${pingResult}`))
34
+ }
35
+
36
+ const result = {
37
+ status: 'success',
38
+ namespace: this.rtNamespace,
39
+ responseTime,
40
+ response: pingResult,
41
+ timestamp: new Date().toISOString()
42
+ }
43
+
44
+ this.log(chalk.dim('Database is ready for operations'))
45
+
46
+ return result
47
+ } catch (error) {
48
+ this.debugLogger?.error?.('Ping command error:', error)
49
+
50
+ this.log(chalk.red('Database connection failed'))
51
+ this.log(chalk.dim(` Namespace: ${this.rtNamespace}`))
52
+ this.log(chalk.dim(` Error: ${error.message}`))
53
+
54
+ const result = {
55
+ status: 'failed',
56
+ namespace: this.rtNamespace,
57
+ error: error.message,
58
+ timestamp: new Date().toISOString()
59
+ }
60
+
61
+ return result
62
+ }
63
+ }
64
+ }
65
+
66
+ Ping.description = 'Test connectivity to your App Builder database'
67
+
68
+ Ping.examples = [
69
+ '$ aio app db ping',
70
+ '$ aio app db ping --json'
71
+ ]
72
+
73
+ Ping.flags = {
74
+ ...DBBaseCommand.flags
75
+ }
76
+
77
+ Ping.args = {}
@@ -0,0 +1,190 @@
1
+ /*
2
+ Copyright 2025 Adobe. All rights reserved.
3
+ This file is licensed to you under the Apache License, Version 2.0 (the "License");
4
+ you may not use this file except in compliance with the License. You may obtain a copy
5
+ of the License at http://www.apache.org/licenses/LICENSE-2.0
6
+
7
+ Unless required by applicable law or agreed to in writing, software distributed under
8
+ the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
9
+ OF ANY KIND, either express or implied. See the License for the specific language
10
+ governing permissions and limitations under the License.
11
+ */
12
+
13
+ import { DBBaseCommand } from '../../../DBBaseCommand.js'
14
+ import chalk from 'chalk'
15
+ import { DB_STATUS } from '../../../constants/db.js'
16
+ import { Flags } from '@oclif/core'
17
+
18
+ export class Provision extends DBBaseCommand {
19
+ async run () {
20
+ const region = this.db.region
21
+
22
+ try {
23
+ // First check if database is already provisioned
24
+ this.log(chalk.blue('Checking current provisioning status...'))
25
+
26
+ let provisionStatusResponse
27
+ try {
28
+ provisionStatusResponse = await this.db.provisionStatus()
29
+ this.debugLogger?.info?.('Provision status:', provisionStatusResponse)
30
+ } catch (error) {
31
+ this.debugLogger?.info?.('No existing provisioning status found:', error.message)
32
+ provisionStatusResponse = null
33
+ }
34
+
35
+ if (provisionStatusResponse) {
36
+ const currentStatus = provisionStatusResponse.status.toUpperCase()
37
+ const statusRegion = provisionStatusResponse.region
38
+
39
+ if (currentStatus === DB_STATUS.PROVISIONED) {
40
+ this.log(chalk.green('Database is already provisioned and ready for use'))
41
+ this.log(chalk.dim(` Namespace: ${this.rtNamespace}`))
42
+ this.log(chalk.dim(` Region: ${statusRegion}`))
43
+ this.log(chalk.dim(` Status: ${currentStatus}`))
44
+
45
+ return {
46
+ status: 'already_provisioned',
47
+ namespace: this.rtNamespace,
48
+ details: provisionStatusResponse
49
+ }
50
+ } else if (currentStatus === DB_STATUS.REQUESTED) {
51
+ this.log(chalk.yellow('Database provisioning request has been submitted and is pending'))
52
+ this.log(chalk.dim(` Namespace: ${this.rtNamespace}`))
53
+ this.log(chalk.dim(` Region: ${statusRegion}`))
54
+ this.log(chalk.dim(` Status: ${currentStatus}`))
55
+ this.log(chalk.dim('\nUse "aio app db status --watch" to monitor progress'))
56
+
57
+ return {
58
+ status: 'in_progress',
59
+ namespace: this.rtNamespace,
60
+ details: provisionStatusResponse
61
+ }
62
+ } else if (currentStatus === DB_STATUS.PROCESSING) {
63
+ this.log(chalk.yellow('Database is currently being provisioned'))
64
+ this.log(chalk.dim(` Namespace: ${this.rtNamespace}`))
65
+ this.log(chalk.dim(` Region: ${statusRegion}`))
66
+ this.log(chalk.dim(` Status: ${currentStatus}`))
67
+ this.log(chalk.dim('\nUse "aio app db status --watch" to monitor progress'))
68
+ this.log(chalk.red('If provisioning takes unusually long, please contact the App Builder team'))
69
+
70
+ return {
71
+ status: 'in_progress',
72
+ namespace: this.rtNamespace,
73
+ details: provisionStatusResponse
74
+ }
75
+ } else if (currentStatus === DB_STATUS.FAILED) {
76
+ this.log(chalk.red('Previous database provisioning failed'))
77
+ this.log(chalk.dim(` Namespace: ${this.rtNamespace}`))
78
+ this.log(chalk.dim(` Status: ${currentStatus}`))
79
+ this.log(chalk.yellow('\nAttempting to provision again...'))
80
+ this.log(chalk.red('If the problem persists, please contact the App Builder team'))
81
+ } else if (currentStatus === DB_STATUS.REJECTED) {
82
+ this.log(chalk.red('Previous database provisioning request was rejected'))
83
+ this.log(chalk.dim(` Namespace: ${this.rtNamespace}`))
84
+ this.log(chalk.dim(` Status: ${currentStatus}`))
85
+ this.log(chalk.yellow('\nAttempting to provision again...'))
86
+ this.log(chalk.red('If the problem persists, please contact the App Builder team for assistance'))
87
+ } else if (currentStatus !== DB_STATUS.NOT_PROVISIONED) {
88
+ this.log(chalk.yellow(`Database status is '${currentStatus}' - attempting to provision...`))
89
+ this.log(chalk.dim(` Namespace: ${this.rtNamespace}`))
90
+ this.log(chalk.dim(` Status: ${currentStatus}`))
91
+ this.log(chalk.red('If you encounter issues, please contact the App Builder team'))
92
+ }
93
+ }
94
+
95
+ // Create a new database if not yet provisioned
96
+ this.warn('Database provisioning will create new database resources')
97
+
98
+ // Skip confirmation prompt if --yes flag is used
99
+ if (!this.flags.yes) {
100
+ // eslint-disable-next-line node/no-unsupported-features/es-syntax
101
+ const { confirm } = await import('@inquirer/prompts')
102
+
103
+ const confirmed = await confirm({
104
+ message: `Provision database for namespace '${this.rtNamespace}'?`,
105
+ default: false
106
+ })
107
+
108
+ if (!confirmed) {
109
+ this.log('Database provisioning cancelled')
110
+ return { status: 'cancelled' }
111
+ }
112
+ }
113
+
114
+ // Start provisioning
115
+ this.log(chalk.blue(`Submitting a request for a database to be provisioned for the '${this.rtNamespace}' namespace...`))
116
+ this.log(chalk.dim(` Namespace: ${this.rtNamespace}`))
117
+ this.log(chalk.dim(` Region: ${region}`))
118
+
119
+ const provisionResult = await this.db.provisionRequest()
120
+ this.debugLogger?.info?.('Provision request result:', provisionResult)
121
+
122
+ // Handle different provision result statuses
123
+ const resultStatus = provisionResult?.status?.toUpperCase() || DB_STATUS.UNKNOWN
124
+
125
+ if (resultStatus === DB_STATUS.PROVISIONED) {
126
+ this.log(chalk.green('Database provisioned successfully and ready for use!'))
127
+ } else if (resultStatus === DB_STATUS.REQUESTED) {
128
+ this.log(chalk.blue('Database provisioning request submitted successfully'))
129
+ this.log(chalk.dim('Provisioning is now pending...'))
130
+ } else if (resultStatus === DB_STATUS.PROCESSING) {
131
+ this.log(chalk.yellow('Database is being provisioned...'))
132
+ } else if (resultStatus === DB_STATUS.FAILED) {
133
+ this.error(`Database provisioning failed: ${provisionResult.message || 'Unknown error'}`)
134
+ } else if (resultStatus === DB_STATUS.REJECTED) {
135
+ this.error(`Database provisioning request was rejected: ${provisionResult.message || 'Unknown reason'}`)
136
+ } else if (resultStatus === DB_STATUS.UNKNOWN) {
137
+ this.warn(`Database provisioning request returned unrecognized status '${provisionResult.status || 'undefined'}', an update to the aio cli tool may be necessary.`)
138
+ this.warn('If the issue persists, please contact the App Builder team.')
139
+ } else {
140
+ this.warn(`Database provisioning request returned unexpected status '${resultStatus}', an update to the aio cli tool may be necessary.`)
141
+ this.warn('If the issue persists, please contact the App Builder team.')
142
+ }
143
+
144
+ const result = {
145
+ status: resultStatus.toLowerCase(),
146
+ namespace: this.rtNamespace,
147
+ timestamp: new Date().toISOString(),
148
+ details: provisionResult
149
+ }
150
+
151
+ // If region was specified as a CLI flag, include it in the output for next steps
152
+ const { region: regionFlag } = this.flags
153
+ const regionFlagString = regionFlag ? ` --region ${regionFlag}` : ''
154
+
155
+ if (resultStatus !== DB_STATUS.PROVISIONED) {
156
+ this.log(chalk.dim('\nNext steps:'))
157
+ this.log(chalk.dim(` - Monitor progress: aio app db status${regionFlagString} --watch`))
158
+ this.log(chalk.dim(` - Check status: aio app db status${regionFlagString}`))
159
+ } else {
160
+ this.log(chalk.dim('\nNext steps:'))
161
+ this.log(chalk.dim(` - Test connection: aio app db ping${regionFlagString}`))
162
+ }
163
+
164
+ return result
165
+ } catch (error) {
166
+ this.debugLogger?.error?.('Provision command error:', error)
167
+ this.error(`Database provisioning failed: ${error.message}`)
168
+ }
169
+ }
170
+ }
171
+
172
+ Provision.description = 'Provision a new database for your App Builder application'
173
+
174
+ Provision.examples = [
175
+ '$ aio app db provision',
176
+ '$ aio app db provision --region amer',
177
+ '$ aio app db provision --json',
178
+ '$ aio app db provision --yes'
179
+ ]
180
+
181
+ Provision.flags = {
182
+ ...DBBaseCommand.flags,
183
+ yes: Flags.boolean({
184
+ char: 'y',
185
+ description: 'Skip confirmation prompt and provision automatically',
186
+ default: false
187
+ })
188
+ }
189
+
190
+ Provision.args = {}
@@ -0,0 +1,101 @@
1
+ /*
2
+ Copyright 2025 Adobe. All rights reserved.
3
+ This file is licensed to you under the Apache License, Version 2.0 (the "License");
4
+ you may not use this file except in compliance with the License. You may obtain a copy
5
+ of the License at http://www.apache.org/licenses/LICENSE-2.0
6
+
7
+ Unless required by applicable law or agreed to in writing, software distributed under
8
+ the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
9
+ OF ANY KIND, either express or implied. See the License for the specific language
10
+ governing permissions and limitations under the License.
11
+ */
12
+
13
+ import { DBBaseCommand } from '../../../DBBaseCommand.js'
14
+ import chalk from 'chalk'
15
+ import { prettyJson } from '../../../utils/output.js'
16
+ import { Flags } from '@oclif/core'
17
+
18
+ export class Stats extends DBBaseCommand {
19
+ async run () {
20
+ const { scale } = this.flags
21
+ try {
22
+ this.log(chalk.blue('Fetching database statistics...'))
23
+
24
+ const client = await this.db.connect()
25
+ const stats = await client.dbStats({ scale })
26
+
27
+ this.debugLogger?.info?.('Database statistics retrieved:', stats)
28
+
29
+ const result = {
30
+ ...stats,
31
+ namespace: this.rtNamespace,
32
+ timestamp: new Date().toISOString()
33
+ }
34
+
35
+ this.displayStats(stats)
36
+
37
+ return result
38
+ } catch (error) {
39
+ this.debugLogger?.error?.('Stats command error:', error)
40
+
41
+ this.log(chalk.red('Failed to retrieve database statistics'))
42
+ this.log(chalk.dim(` Namespace: ${this.rtNamespace}`))
43
+ this.log(chalk.dim(` Error: ${error.message}`))
44
+
45
+ this.error(`Failed to fetch database statistics: ${error.message}`)
46
+ }
47
+ }
48
+
49
+ displayStats (stats) {
50
+ this.log(chalk.green('Database Statistics:'))
51
+ this.log(chalk.dim(` Namespace: ${this.rtNamespace}`))
52
+
53
+ if (stats && typeof stats === 'object') {
54
+ const { ok, ...statsWithoutOk } = stats
55
+ // Format and display stats in a readable way
56
+ Object.entries(statsWithoutOk).forEach(([key, value]) => {
57
+ this.log(chalk.dim(` ${key}: ${this.formatValue(value)}`))
58
+ })
59
+ } else {
60
+ this.log(chalk.dim(` Raw Stats: ${this.formatValue(stats)}`))
61
+ }
62
+
63
+ this.log('')
64
+ this.log(chalk.dim(` Retrieved: ${new Date().toISOString()}`))
65
+ }
66
+
67
+ formatValue (value) {
68
+ if (typeof value === 'number') {
69
+ // Format large numbers with commas
70
+ return value.toLocaleString()
71
+ }
72
+ if (value instanceof Date) {
73
+ return value.toISOString()
74
+ }
75
+ if (typeof value === 'object' && value !== null) {
76
+ return `\n${prettyJson(value)}`
77
+ }
78
+ return String(value)
79
+ }
80
+ }
81
+
82
+ Stats.description = 'Get statistics about your App Builder database'
83
+
84
+ Stats.examples = [
85
+ '$ aio app db stats',
86
+ '$ aio app db stats --scale 1024',
87
+ '$ aio app db stats --json'
88
+ ]
89
+
90
+ Stats.flags = {
91
+ ...DBBaseCommand.flags,
92
+ scale: Flags.integer({
93
+ char: 's',
94
+ description: 'Scale factor for size-related statistics (e.g. 1024 for KB, 1048576 for MB).',
95
+ required: false,
96
+ default: 1,
97
+ min: 1
98
+ })
99
+ }
100
+
101
+ Stats.args = {}