11import { execSync } from 'node:child_process' ;
22import fs from 'node:fs' ;
33import path from 'node:path' ;
4- import readline from 'node:readline' ;
54import { parseArgs } from 'node:util' ;
5+ import { confirm , intro , isCancel , log , outro , select , text } from '@clack/prompts' ;
66import Ajv from 'ajv' ;
77
88const HELP_TEXT = `
@@ -29,17 +29,6 @@ Examples:
2929 yarn run create:attribute --key http.route --description "The route pattern of the request" --type string --has_pii false --is_in_otel true --example "/users/:id" --alias "url.template"
3030` ;
3131
32- const rl = readline . createInterface ( {
33- input : process . stdin ,
34- output : process . stdout ,
35- } ) ;
36-
37- const question = ( query : string ) : Promise < string > => {
38- return new Promise ( ( resolve ) => {
39- rl . question ( query , resolve ) ;
40- } ) ;
41- } ;
42-
4332const validateSchema = ( data : unknown ) => {
4433 const schema = JSON . parse ( fs . readFileSync ( 'schemas/attribute.schema.json' , 'utf-8' ) ) ;
4534 const ajv = new Ajv ( ) ;
@@ -74,6 +63,8 @@ const createAttribute = async () => {
7463 process . exit ( 0 ) ;
7564 }
7665
66+ intro ( 'Create new attribute' ) ;
67+
7768 // If any required option is provided, we'll use non-interactive mode
7869 const isInteractive = ! ( values . key || values . description || values . type || values . has_pii || values . is_in_otel ) ;
7970
@@ -87,14 +78,14 @@ const createAttribute = async () => {
8778 let sdks : string | undefined ;
8879
8980 if ( isInteractive ) {
90- key = await question ( 'Enter the attribute key (e.g. http.route): ' ) ;
91- description = await question ( 'Enter a description: ' ) ;
92- type = await question ( 'Enter the type (string/boolean/integer/double/string[]/boolean[]/integer[]/double[]): ' ) ;
93- piiKey = await question ( 'Does this attribute contain PII? (true/maybe/false): ' ) ;
94- isInOtel = await question ( 'Is this attribute in OpenTelemetry? (true/false): ' ) ;
95- example = await question ( 'Enter an example value (optional): ' ) ;
96- alias = await question ( 'Enter attributes that alias to this attribute (comma-separated, optional): ' ) ;
97- sdks = await question ( 'Enter SDKs that use this attribute (comma-separated, optional): ' ) ;
81+ key = await askForAttributeName ( ) ;
82+ description = await askForAttributeDescription ( ) ;
83+ type = await askForAttributeType ( ) ;
84+ piiKey = await askForAttributePii ( ) ;
85+ isInOtel = String ( await askForAttributeIsInOtel ( ) ) ;
86+ example = await askForAttributeExample ( ) ;
87+ alias = await askForAttributeAlias ( ) ;
88+ sdks = await askForAttributeSdks ( ) ;
9889 } else {
9990 key = values . key ;
10091 description = values . description ;
@@ -163,26 +154,140 @@ const createAttribute = async () => {
163154 }
164155
165156 fs . writeFileSync ( filePath , `${ JSON . stringify ( attribute , null , 2 ) } \n` ) ;
166- console . log ( `Successfully created attribute file at: ${ filePath } ` ) ;
157+ log . success ( `Successfully created attribute file at: ${ filePath } ` ) ;
167158
168159 // Ask if user wants to generate docs
169- const generateDocs = await question ( '\nWould you like to generate documentation? (y/n): ' ) ;
170- if ( generateDocs . toLowerCase ( ) === 'y' ) {
171- console . log ( 'Generating documentation...' ) ;
160+ const generateDocs = await askForGenerateDocs ( ) ;
161+ if ( generateDocs ) {
162+ log . info ( 'Generating documentation...' ) ;
172163 try {
173164 execSync ( 'yarn run generate' , { stdio : 'inherit' } ) ;
174- console . log ( 'Documentation generated successfully!' ) ;
165+ log . success ( 'Documentation generated successfully!' ) ;
175166 } catch ( error ) {
176- console . error ( ' Error generating documentation:' , error ) ;
167+ log . error ( ` Error generating documentation: ${ error } ` ) ;
177168 process . exit ( 1 ) ;
178169 }
179170 }
180171 } catch ( error ) {
181- console . error ( ' Error creating attribute:' , error ) ;
172+ log . error ( ` Error creating attribute: ${ error } ` ) ;
182173 process . exit ( 1 ) ;
183174 } finally {
184- rl . close ( ) ;
175+ outro ( 'Attribute creation done!' ) ;
185176 }
186177} ;
187178
188179createAttribute ( ) ;
180+
181+ async function askForAttributeName ( ) {
182+ return abortIfCancelled (
183+ text ( {
184+ message : 'Enter the attribute key' ,
185+ placeholder : 'http.route' ,
186+ validate : ( value ) => {
187+ if ( ! value ) {
188+ return 'Attribute key is required' ;
189+ }
190+ return undefined ;
191+ } ,
192+ } ) ,
193+ ) ;
194+ }
195+
196+ async function askForAttributeDescription ( ) {
197+ return abortIfCancelled (
198+ text ( {
199+ message : 'Enter the attribute description' ,
200+ placeholder : 'The route pattern of the request' ,
201+ validate : ( value ) => {
202+ if ( ! value ) {
203+ return 'Attribute description is required' ;
204+ }
205+ return undefined ;
206+ } ,
207+ } ) ,
208+ ) ;
209+ }
210+
211+ async function askForAttributeType ( ) {
212+ return abortIfCancelled (
213+ select ( {
214+ message : 'Enter the type' ,
215+ options : [
216+ { value : 'string' , label : 'String' } ,
217+ { value : 'integer' , label : 'Integer' } ,
218+ { value : 'boolean' , label : 'Boolean' } ,
219+ { value : 'double' , label : 'Double' } ,
220+ { value : 'string[]' , label : 'String Array' } ,
221+ { value : 'integer[]' , label : 'Integer Array' } ,
222+ { value : 'boolean[]' , label : 'Boolean Array' } ,
223+ { value : 'double[]' , label : 'Double Array' } ,
224+ ] ,
225+ } ) ,
226+ ) ;
227+ }
228+
229+ async function askForAttributePii ( ) {
230+ return abortIfCancelled (
231+ select ( {
232+ message : 'Does the attribute contain PII?' ,
233+ options : [
234+ { value : 'true' , label : 'Yes' } ,
235+ { value : 'false' , label : 'No' } ,
236+ { value : 'maybe' , label : 'Maybe' } ,
237+ ] ,
238+ } ) ,
239+ ) ;
240+ }
241+
242+ async function askForAttributeIsInOtel ( ) {
243+ return abortIfCancelled (
244+ confirm ( {
245+ message : 'Is the attribute in OpenTelemetry?' ,
246+ initialValue : true ,
247+ } ) ,
248+ ) ;
249+ }
250+
251+ async function askForAttributeExample ( ) {
252+ return abortIfCancelled (
253+ text ( {
254+ message : 'Enter an example value (optional)' ,
255+ placeholder : 'GET /users/:id' ,
256+ } ) ,
257+ ) ;
258+ }
259+
260+ async function askForAttributeAlias ( ) {
261+ return abortIfCancelled (
262+ text ( {
263+ message : 'Enter attributes that alias to this attribute (comma-separated, optional)' ,
264+ placeholder : 'url.route,http.routename' ,
265+ } ) ,
266+ ) ;
267+ }
268+
269+ async function askForAttributeSdks ( ) {
270+ return abortIfCancelled (
271+ text ( {
272+ message : 'Enter SDKs that use this attribute (comma-separated, optional)' ,
273+ placeholder : 'javascript-browser,javascript-node' ,
274+ } ) ,
275+ ) ;
276+ }
277+
278+ async function askForGenerateDocs ( ) {
279+ return abortIfCancelled (
280+ confirm ( {
281+ message : 'Would you like to generate documentation?' ,
282+ initialValue : true ,
283+ } ) ,
284+ ) ;
285+ }
286+
287+ async function abortIfCancelled < T > ( input : T | Promise < T > ) : Promise < Exclude < T , symbol > > {
288+ if ( isCancel ( await input ) ) {
289+ process . exit ( 0 ) ;
290+ } else {
291+ return input as Exclude < T , symbol > ;
292+ }
293+ }
0 commit comments