@@ -2,23 +2,26 @@ import { Command } from "@cliffy/command";
22import VTConfig , { globalConfig } from "~/vt/VTConfig.ts" ;
33import { findVtRoot } from "~/vt/vt/utils.ts" ;
44import { doWithSpinner } from "~/cmd/utils.ts" ;
5- import { getNestedProperty , setNestedProperty } from "~/utils.ts" ;
5+ import { removeNestedProperty , setNestedProperty } from "~/utils.ts" ;
66import { stringify as stringifyYaml } from "@std/yaml" ;
77import { VTConfigSchema } from "~/vt/vt/schemas.ts" ;
8- import { zodToJsonSchema } from "zod-to-json-schema" ;
98import { printYaml } from "~/cmd/styles.ts" ;
109import { fromError } from "zod-validation-error" ;
1110import z from "zod" ;
1211import { colors } from "@cliffy/ansi/colors" ;
13- import { DEFAULT_WRAP_AMOUNT , GLOBAL_VT_CONFIG_PATH } from "~/consts.ts" ;
12+ import {
13+ DEFAULT_WRAP_AMOUNT ,
14+ GLOBAL_VT_CONFIG_PATH ,
15+ LOCAL_VT_CONFIG_PATH ,
16+ } from "~/consts.ts" ;
1417import { join } from "@std/path" ;
1518import wrap from "word-wrap" ;
1619import { openEditorAt } from "~/cmd/lib/utils/openEditorAt.ts" ;
17- import { Select } from "@cliffy/prompt" ;
20+ import { Confirm , Select } from "@cliffy/prompt" ;
21+ import { execSync } from "node:child_process" ;
1822
1923function showConfigOptions ( ) {
20- // deno-lint-ignore no-explicit-any
21- const jsonSchema = zodToJsonSchema ( VTConfigSchema ) as any ;
24+ const jsonSchema = VTConfigSchema . toJSONSchema ( { unrepresentable : "any" } ) ;
2225 delete jsonSchema [ "$schema" ] ;
2326
2427 // deno-lint-ignore no-explicit-any
@@ -42,6 +45,46 @@ function showConfigOptions() {
4245 printYaml ( stringifyYaml ( jsonSchema [ "properties" ] ) ) ;
4346}
4447
48+ /**
49+ * If the user tries to add an API secret to their local vt config file, offer
50+ * to add the config file to their local gitignore.
51+ */
52+ async function offerToAddToGitignore ( ) {
53+ const gitRoot = execSync ( "git rev-parse --show-toplevel" , {
54+ encoding : "utf-8" ,
55+ stdio : [ "pipe" , "pipe" , "ignore" ] ,
56+ } ) . trim ( ) ;
57+
58+ const addToIgnore = await Confirm . prompt (
59+ "You are adding an API secret to your local config file, and we noticed you have a Git repo set up for this folder.\n" +
60+ `Would you like to add \`${ LOCAL_VT_CONFIG_PATH } \` to your \`.gitignore\`?` ,
61+ ) ;
62+
63+ if ( addToIgnore ) {
64+ let gitignoreContent = "" ;
65+ const gitignorePath = join ( gitRoot , ".gitignore" ) ;
66+
67+ try {
68+ gitignoreContent = await Deno . readTextFile ( gitignorePath ) ;
69+ // Add a newline if the file doesn't end with one
70+ if ( gitignoreContent . length > 0 && ! gitignoreContent . endsWith ( "\n" ) ) {
71+ gitignoreContent += "\n" ;
72+ }
73+ } catch ( e ) {
74+ if ( ! ( e instanceof Deno . errors . NotFound ) ) {
75+ // If error is something other than "file not found", rethrow it
76+ throw e ;
77+ }
78+ // If file doesn't exist, we'll create it with empty content
79+ }
80+
81+ // Add the path to gitignore
82+ gitignoreContent += LOCAL_VT_CONFIG_PATH + "\n" ;
83+ await Deno . writeTextFile ( gitignorePath , gitignoreContent ) ;
84+ console . log ( `Added ${ LOCAL_VT_CONFIG_PATH } to .gitignore` ) ;
85+ }
86+ }
87+
4588export const configWhereCmd = new Command ( )
4689 . name ( "where" )
4790 . description ( "Show the config file locations" )
@@ -70,7 +113,10 @@ export const configWhereCmd = new Command()
70113
71114export const configSetCmd = new Command ( )
72115 . description ( "Set a configuration value" )
73- . option ( "--local" , "Set in the local configuration (val-specific)" )
116+ . option (
117+ "--local" ,
118+ 'Set in the local configuration (val-specific). Leave value blank "" to unset.' ,
119+ )
74120 . arguments ( "<key:string> <value:string>" )
75121 . example (
76122 "Set your valtown API key (global)" ,
@@ -90,40 +136,28 @@ export const configSetCmd = new Command()
90136 const vtConfig = new VTConfig ( vtRoot ) ;
91137
92138 const config = await vtConfig . loadConfig ( ) ;
93- const updatedConfig = setNestedProperty ( config , key , value ) ;
94- const oldProperty = getNestedProperty ( config , key , null ) as
95- | string
96- | null ;
97-
98- if ( oldProperty !== null && oldProperty . toString ( ) === value ) {
99- throw new Error (
100- `Property ${ colors . bold ( key ) } is already set to ${
101- colors . bold ( oldProperty )
102- } `,
103- ) ;
104- }
105139
106- let validatedConfig : z . infer < typeof VTConfigSchema > ;
140+ const updatedConfig = value === ""
141+ ? removeNestedProperty ( config , key )
142+ : setNestedProperty ( config , key , value ) ;
143+
107144 try {
108145 if ( useGlobal ) {
109- validatedConfig = await vtConfig . saveGlobalConfig ( updatedConfig ) ;
146+ await vtConfig . saveGlobalConfig ( updatedConfig ) ;
110147 } else {
111- validatedConfig = await vtConfig . saveLocalConfig ( updatedConfig ) ;
148+ await vtConfig . saveLocalConfig ( updatedConfig ) ;
112149 }
113150
114- if ( JSON . stringify ( config ) !== JSON . stringify ( validatedConfig ) ) {
115- spinner . succeed (
116- `Set ${ colors . bold ( `${ key } =${ value } ` ) } in ${
151+ spinner . succeed (
152+ value === ""
153+ ? `Unset ${ colors . bold ( `${ key } ` ) } `
154+ : `Set ${ colors . bold ( `${ key } =${ value } ` ) } in ${
117155 useGlobal ? "global" : "local"
118156 } configuration`,
119- ) ;
120- } else {
121- throw new Error (
122- `Property ${ colors . bold ( key ) } is not valid.` +
123- `\n Use \`${
124- colors . bold ( "vt config options" )
125- } \` to view config options`,
126- ) ;
157+ ) ;
158+
159+ if ( key === "apiKey" && value !== "" && ! useGlobal ) {
160+ await offerToAddToGitignore ( ) ;
127161 }
128162 } catch ( e ) {
129163 if ( e instanceof z . ZodError ) {
0 commit comments