@dataformer/env-service 2.3.0 → 3.0.0

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.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,17 @@
1
1
  # @dataformer/env-service
2
2
 
3
+ ## 3.0.0
4
+
5
+ ### Major Changes
6
+
7
+ - change setSearchEngineId method to setCustomSearchEngineId
8
+
9
+ ## 2.4.0
10
+
11
+ ### Minor Changes
12
+
13
+ - fix circular dependency with LoggerService, EnvService, and SecretManagerClient
14
+
3
15
  ## 2.3.0
4
16
 
5
17
  ### Minor Changes
package/dist/index.d.ts CHANGED
@@ -2,7 +2,7 @@ declare const askQuestion: (question: string, defaultValue?: string) => Promise<
2
2
  declare function obfuscateCred(cred: string | null): string;
3
3
  declare function getEnvVar(baseSecretKey: string): Promise<string | null>;
4
4
  declare function getCustomSearchApiKey(): Promise<string>;
5
- declare function getSearchEngineId(): Promise<string>;
5
+ declare function getCustomSearchEngineId(): Promise<string>;
6
6
  declare function getGcpProjectId(): Promise<string>;
7
7
  declare function setEnvVar(baseSecretKey: string, value: string): Promise<boolean>;
8
8
  declare const setLogToConsole: () => Promise<void>;
@@ -23,6 +23,7 @@ declare const setJiraUserEmail: () => Promise<void>;
23
23
  declare const setGeminiApiKey: () => Promise<void>;
24
24
  declare const setNpmToken: () => Promise<void>;
25
25
  declare const setCustomSearchApiKey: () => Promise<void>;
26
+ declare const setCustomSearchEngineId: () => Promise<void>;
26
27
  declare const setFirestoreProjectId: () => Promise<void>;
27
28
  declare const getLogToConsole: () => Promise<string | null>;
28
29
  declare const getPostgresUser: () => Promise<string | null>;
@@ -46,4 +47,4 @@ declare const getProjectRoot: () => string;
46
47
  declare const printEnv: () => Promise<void>;
47
48
  declare const run: () => Promise<void>;
48
49
 
49
- export { askQuestion, getBigtableInstanceName, getBigtableProjectId, getBigtableTableName, getCustomSearchApiKey, getEnvVar, getFirestoreProjectId, getGcpProjectId, getGeminiApiKey, getJiraApiBaseUrl, getJiraApiToken, getJiraUserEmail, getLogToConsole, getNpmToken, getPostgresAuthType, getPostgresDatabase, getPostgresHostName, getPostgresInstanceConnectionName, getPostgresIpType, getPostgresPassword, getPostgresPoolSizeMax, getPostgresUser, getProjectRoot, getSearchEngineId, obfuscateCred, printEnv, run, setBigtableInstanceName, setBigtableProjectId, setBigtableTableName, setCustomSearchApiKey, setEnvVar, setFirestoreProjectId, setGeminiApiKey, setJiraApiBaseUrl, setJiraApiToken, setJiraUserEmail, setLogToConsole, setNpmToken, setPostgresAuthType, setPostgresDatabase, setPostgresHostName, setPostgresInstanceConnectionName, setPostgresIpType, setPostgresPassword, setPostgresPoolSizeMax, setPostgresUser };
50
+ export { askQuestion, getBigtableInstanceName, getBigtableProjectId, getBigtableTableName, getCustomSearchApiKey, getCustomSearchEngineId, getEnvVar, getFirestoreProjectId, getGcpProjectId, getGeminiApiKey, getJiraApiBaseUrl, getJiraApiToken, getJiraUserEmail, getLogToConsole, getNpmToken, getPostgresAuthType, getPostgresDatabase, getPostgresHostName, getPostgresInstanceConnectionName, getPostgresIpType, getPostgresPassword, getPostgresPoolSizeMax, getPostgresUser, getProjectRoot, obfuscateCred, printEnv, run, setBigtableInstanceName, setBigtableProjectId, setBigtableTableName, setCustomSearchApiKey, setCustomSearchEngineId, setEnvVar, setFirestoreProjectId, setGeminiApiKey, setJiraApiBaseUrl, setJiraApiToken, setJiraUserEmail, setLogToConsole, setNpmToken, setPostgresAuthType, setPostgresDatabase, setPostgresHostName, setPostgresInstanceConnectionName, setPostgresIpType, setPostgresPassword, setPostgresPoolSizeMax, setPostgresUser };
package/dist/index.js CHANGED
@@ -1,24 +1,60 @@
1
1
  // src/index.ts
2
2
  import readline from "readline";
3
3
  import path from "path";
4
- import { SecretManagerClient } from "@dataformer/secret-manager-client";
5
- var secretManager = null;
4
+ import { SecretManagerServiceClient } from "@google-cloud/secret-manager";
5
+ import { execSync } from "child_process";
6
+ function getGcpProjectIdFromGcloud() {
7
+ try {
8
+ const cmd = "gcloud config get-value core/project";
9
+ const projectId2 = execSync(cmd, { encoding: "utf8", stdio: "pipe" }).trim();
10
+ if (projectId2 && projectId2 !== "(unset)") {
11
+ return projectId2;
12
+ }
13
+ return null;
14
+ } catch {
15
+ return null;
16
+ }
17
+ }
18
+ function getProjectId() {
19
+ if (process.env.GCP_PROJECT_ID) {
20
+ return process.env.GCP_PROJECT_ID;
21
+ }
22
+ const gcloudProjectId = getGcpProjectIdFromGcloud();
23
+ if (gcloudProjectId) {
24
+ return gcloudProjectId;
25
+ }
26
+ throw new Error(
27
+ "\u{1F6A8} GCP project ID is required for Secret Manager.\n\nCI/CD DEPLOYMENT (REQUIRED):\n Set GCP_PROJECT_ID as environment variable\n Example: GCP_PROJECT_ID=dataformer-prod\n\nLOCAL DEVELOPMENT:\n Set your gcloud project: gcloud config set project YOUR_PROJECT_ID\n Verify with: gcloud config get-value project"
28
+ );
29
+ }
30
+ var secretManagerClient = null;
6
31
  var secretManagerInitialized = false;
7
- async function getSecretManager() {
32
+ var projectId = null;
33
+ async function getSecretManagerClient() {
8
34
  if (secretManagerInitialized) {
9
- return secretManager;
35
+ return secretManagerClient && projectId ? { client: secretManagerClient, projectId } : null;
10
36
  }
11
37
  try {
12
- secretManager = await SecretManagerClient.create();
38
+ projectId = getProjectId();
39
+ secretManagerClient = new SecretManagerServiceClient({ projectId });
13
40
  secretManagerInitialized = true;
14
- return secretManager;
41
+ return { client: secretManagerClient, projectId };
15
42
  } catch (error) {
16
- console.error("[EnvService.ts] Failed to initialize SecretManagerClient:", error.message);
43
+ console.error("[EnvService.ts] Failed to initialize SecretManagerServiceClient:", error.message);
17
44
  console.warn("[EnvService.ts] Secret Manager functionality will be unavailable.");
18
45
  secretManagerInitialized = true;
19
46
  return null;
20
47
  }
21
48
  }
49
+ function buildSecretName(projectId2, baseSecretId) {
50
+ return `${projectId2}_${baseSecretId}`;
51
+ }
52
+ function buildSecretPath(projectId2, secretName) {
53
+ return `projects/${projectId2}/secrets/${secretName}`;
54
+ }
55
+ function buildSecretVersionPath(projectId2, secretName, version = "latest") {
56
+ return `projects/${projectId2}/secrets/${secretName}/versions/${version}`;
57
+ }
22
58
  var rl = readline.createInterface({
23
59
  input: process.stdin,
24
60
  output: process.stdout
@@ -40,12 +76,26 @@ function obfuscateCred(cred) {
40
76
  }
41
77
  }
42
78
  async function getEnvVar(baseSecretKey) {
43
- const sm = await getSecretManager();
44
- if (!sm) {
79
+ const smClient = await getSecretManagerClient();
80
+ if (!smClient) {
45
81
  console.warn(`[EnvService.ts] SecretManager not available for getEnvVar (key: ${baseSecretKey})`);
46
82
  return null;
47
83
  }
48
- return sm.getSecret(baseSecretKey);
84
+ try {
85
+ const secretName = buildSecretName(smClient.projectId, baseSecretKey);
86
+ const secretVersionPath = buildSecretVersionPath(smClient.projectId, secretName);
87
+ const [response] = await smClient.client.accessSecretVersion({
88
+ name: secretVersionPath
89
+ });
90
+ const secretValue = response.payload?.data?.toString();
91
+ return secretValue || null;
92
+ } catch (error) {
93
+ if (error.code === 5) {
94
+ return null;
95
+ }
96
+ console.warn(`[EnvService.ts] Failed to get secret ${baseSecretKey}: ${error.message}`);
97
+ return null;
98
+ }
49
99
  }
50
100
  async function getCustomSearchApiKey() {
51
101
  const apiKey = await getEnvVar("CUSTOM_SEARCH_API_KEY");
@@ -54,10 +104,10 @@ async function getCustomSearchApiKey() {
54
104
  }
55
105
  return apiKey;
56
106
  }
57
- async function getSearchEngineId() {
58
- const searchEngineId = await getEnvVar("SEARCH_ENGINE_ID");
107
+ async function getCustomSearchEngineId() {
108
+ const searchEngineId = await getEnvVar("CUSTOM_SEARCH_ENGINE_ID");
59
109
  if (!searchEngineId) {
60
- throw new Error("SEARCH_ENGINE_ID not found in environment variables.");
110
+ throw new Error("CUSTOM_SEARCH_ENGINE_ID not found in environment variables.");
61
111
  }
62
112
  return searchEngineId;
63
113
  }
@@ -67,15 +117,15 @@ async function getGcpProjectId() {
67
117
  return envProjectId;
68
118
  }
69
119
  try {
70
- const { execSync } = await import("child_process");
71
- const projectId = execSync("gcloud config get-value project", {
120
+ const { execSync: execSync2 } = await import("child_process");
121
+ const projectId2 = execSync2("gcloud config get-value project", {
72
122
  encoding: "utf8",
73
123
  stdio: ["ignore", "pipe", "ignore"]
74
124
  }).trim();
75
- if (!projectId || projectId === "(unset)") {
125
+ if (!projectId2 || projectId2 === "(unset)") {
76
126
  throw new Error("No GCP project configured. Run `gcloud config set project YOUR_PROJECT_ID`");
77
127
  }
78
- return projectId;
128
+ return projectId2;
79
129
  } catch (error) {
80
130
  throw new Error(
81
131
  "GCP_PROJECT_ID not found. Either:\n1. Set GCP_PROJECT_ID environment variable (for deployed environments), or\n2. Configure gcloud: `gcloud config set project YOUR_PROJECT_ID` (for local development)"
@@ -83,17 +133,58 @@ async function getGcpProjectId() {
83
133
  }
84
134
  }
85
135
  async function setEnvVar(baseSecretKey, value) {
86
- const sm = await getSecretManager();
87
- if (!sm) {
136
+ const smClient = await getSecretManagerClient();
137
+ if (!smClient) {
88
138
  console.warn(`[EnvService.ts] SecretManager not available for setEnvVar (key: ${baseSecretKey})`);
89
139
  return false;
90
140
  }
91
- return sm.setSecret(baseSecretKey, value);
141
+ try {
142
+ const secretName = buildSecretName(smClient.projectId, baseSecretKey);
143
+ const addVersion = async () => {
144
+ const secretPath = buildSecretPath(smClient.projectId, secretName);
145
+ await smClient.client.addSecretVersion({
146
+ parent: secretPath,
147
+ payload: {
148
+ data: Buffer.from(value, "utf8")
149
+ }
150
+ });
151
+ console.log(`\u2705 Secret '${baseSecretKey}' saved in GCP project: ${smClient.projectId}`);
152
+ };
153
+ try {
154
+ await addVersion();
155
+ return true;
156
+ } catch (err) {
157
+ if (err.code === 5) {
158
+ await smClient.client.createSecret({
159
+ parent: `projects/${smClient.projectId}`,
160
+ secretId: secretName,
161
+ secret: {
162
+ replication: {
163
+ automatic: {}
164
+ }
165
+ }
166
+ });
167
+ console.log(`[EnvService.ts] Created secret resource: ${secretName}`);
168
+ await addVersion();
169
+ return true;
170
+ }
171
+ console.warn(`[EnvService.ts] setEnvVar failed: ${err.message}`);
172
+ return false;
173
+ }
174
+ } catch (error) {
175
+ console.warn(`[EnvService.ts] Failed to set secret ${baseSecretKey}: ${error.message}`);
176
+ return false;
177
+ }
92
178
  }
93
179
  var setLogToConsole = async () => {
94
180
  const logToConsole = await askQuestion("Use consoleLogger to log to console? (yes, no)", "no");
95
181
  if (logToConsole) {
96
- await setEnvVar("LOG_TO_CONSOLE", logToConsole);
182
+ const normalizedValue = logToConsole.toLowerCase();
183
+ if (normalizedValue === "yes" || normalizedValue === "no") {
184
+ await setEnvVar("LOG_TO_CONSOLE", normalizedValue);
185
+ } else {
186
+ console.warn(`Invalid LOG_TO_CONSOLE value: '${logToConsole}'. Only 'yes' or 'no' are valid.`);
187
+ }
97
188
  }
98
189
  };
99
190
  var setPostgresUser = async () => {
@@ -194,6 +285,12 @@ var setCustomSearchApiKey = async () => {
194
285
  await setEnvVar("CUSTOM_SEARCH_API_KEY", customSearchApiKey);
195
286
  }
196
287
  };
288
+ var setCustomSearchEngineId = async () => {
289
+ const customSearchEngineId = await askQuestion("Enter Custom Search Engine ID", "");
290
+ if (customSearchEngineId) {
291
+ await setEnvVar("CUSTOM_SEARCH_ENGINE_ID", customSearchEngineId);
292
+ }
293
+ };
197
294
  var setFirestoreProjectId = async () => {
198
295
  const firestoreProjectId = await askQuestion("Enter Firestore Project ID", "dataformer-prod");
199
296
  if (firestoreProjectId) {
@@ -245,6 +342,7 @@ var printEnv = async () => {
245
342
  const jiraUserEmail = await getJiraUserEmail();
246
343
  const geminiApiKey = await getGeminiApiKey();
247
344
  const customSearchApiKey = await getCustomSearchApiKey();
345
+ const customSearchEngineId = await getCustomSearchEngineId();
248
346
  const gcpProjectIdVal = await getGcpProjectId();
249
347
  const npmToken = await getNpmToken();
250
348
  console.log(`Project Root: ${getProjectRoot()}`);
@@ -266,6 +364,7 @@ var printEnv = async () => {
266
364
  console.log(`Jira API Base URL: ${jiraApiBaseUrl}`);
267
365
  console.log(`Jira User Email: ${jiraUserEmail}`);
268
366
  console.log(`Custom Search API Key: ${obfuscateCred(customSearchApiKey)}`);
367
+ console.log(`Custom Search Engine ID: ${customSearchEngineId}`);
269
368
  console.log(`Gemini API Key: ${obfuscateCred(geminiApiKey)}`);
270
369
  console.log(`NPM Token: ${obfuscateCred(npmToken)}`);
271
370
  };
@@ -329,6 +428,9 @@ var run = async () => {
329
428
  case "setCustomSearchApiKey":
330
429
  await setCustomSearchApiKey();
331
430
  break;
431
+ case "setCustomSearchEngineId":
432
+ await setCustomSearchEngineId();
433
+ break;
332
434
  case "printEnv":
333
435
  await printEnv();
334
436
  break;
@@ -350,6 +452,7 @@ export {
350
452
  getBigtableProjectId,
351
453
  getBigtableTableName,
352
454
  getCustomSearchApiKey,
455
+ getCustomSearchEngineId,
353
456
  getEnvVar,
354
457
  getFirestoreProjectId,
355
458
  getGcpProjectId,
@@ -368,7 +471,6 @@ export {
368
471
  getPostgresPoolSizeMax,
369
472
  getPostgresUser,
370
473
  getProjectRoot,
371
- getSearchEngineId,
372
474
  obfuscateCred,
373
475
  printEnv,
374
476
  run,
@@ -376,6 +478,7 @@ export {
376
478
  setBigtableProjectId,
377
479
  setBigtableTableName,
378
480
  setCustomSearchApiKey,
481
+ setCustomSearchEngineId,
379
482
  setEnvVar,
380
483
  setFirestoreProjectId,
381
484
  setGeminiApiKey,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dataformer/env-service",
3
- "version": "2.3.0",
3
+ "version": "3.0.0",
4
4
  "description": "Environment service for Dataformer",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -12,8 +12,7 @@
12
12
  }
13
13
  },
14
14
  "dependencies": {
15
- "@google-cloud/secret-manager": "^5.4.0",
16
- "@dataformer/secret-manager-client": "^2.3.0"
15
+ "@google-cloud/secret-manager": "^5.4.0"
17
16
  },
18
17
  "devDependencies": {
19
18
  "tsup": "^8.0.0",
package/src/index.ts CHANGED
@@ -39,29 +39,83 @@
39
39
 
40
40
  import readline from 'readline';
41
41
  import path from 'path';
42
- import { SecretManagerClient } from '@dataformer/secret-manager-client';
42
+ import { SecretManagerServiceClient } from '@google-cloud/secret-manager';
43
+ import { execSync } from 'child_process';
43
44
 
44
- // SecretManager client will be initialized lazily on first use
45
- let secretManager: SecretManagerClient | null = null;
45
+ // Helper functions for GCP project ID detection
46
+ function getGcpProjectIdFromGcloud(): string | null {
47
+ try {
48
+ const cmd = 'gcloud config get-value core/project';
49
+ const projectId = execSync(cmd, { encoding: 'utf8', stdio: 'pipe' }).trim();
50
+ if (projectId && projectId !== '(unset)') {
51
+ return projectId;
52
+ }
53
+ return null;
54
+ } catch {
55
+ return null;
56
+ }
57
+ }
58
+
59
+ function getProjectId(): string {
60
+ // Project ID resolution priority:
61
+ // 1. GCP_PROJECT_ID env var (REQUIRED for CI/CD)
62
+ // 2. gcloud config project (for local development)
63
+ if (process.env.GCP_PROJECT_ID) {
64
+ return process.env.GCP_PROJECT_ID;
65
+ }
66
+
67
+ const gcloudProjectId = getGcpProjectIdFromGcloud();
68
+ if (gcloudProjectId) {
69
+ return gcloudProjectId;
70
+ }
71
+
72
+ throw new Error(
73
+ '🚨 GCP project ID is required for Secret Manager.\n\n' +
74
+ 'CI/CD DEPLOYMENT (REQUIRED):\n' +
75
+ ' Set GCP_PROJECT_ID as environment variable\n' +
76
+ ' Example: GCP_PROJECT_ID=dataformer-prod\n\n' +
77
+ 'LOCAL DEVELOPMENT:\n' +
78
+ ' Set your gcloud project: gcloud config set project YOUR_PROJECT_ID\n' +
79
+ ' Verify with: gcloud config get-value project'
80
+ );
81
+ }
82
+
83
+ // Secret Manager direct implementation
84
+ let secretManagerClient: SecretManagerServiceClient | null = null;
46
85
  let secretManagerInitialized = false;
86
+ let projectId: string | null = null;
47
87
 
48
- async function getSecretManager(): Promise<SecretManagerClient | null> {
88
+ async function getSecretManagerClient(): Promise<{ client: SecretManagerServiceClient; projectId: string } | null> {
49
89
  if (secretManagerInitialized) {
50
- return secretManager;
90
+ return secretManagerClient && projectId ? { client: secretManagerClient, projectId } : null;
51
91
  }
52
92
 
53
93
  try {
54
- secretManager = await SecretManagerClient.create();
94
+ projectId = getProjectId();
95
+ secretManagerClient = new SecretManagerServiceClient({ projectId });
55
96
  secretManagerInitialized = true;
56
- return secretManager;
97
+ return { client: secretManagerClient, projectId };
57
98
  } catch (error) {
58
- console.error('[EnvService.ts] Failed to initialize SecretManagerClient:', (error as Error).message);
99
+ console.error('[EnvService.ts] Failed to initialize SecretManagerServiceClient:', (error as Error).message);
59
100
  console.warn('[EnvService.ts] Secret Manager functionality will be unavailable.');
60
101
  secretManagerInitialized = true; // Prevent retrying
61
102
  return null;
62
103
  }
63
104
  }
64
105
 
106
+ // Helper functions for secret operations
107
+ function buildSecretName(projectId: string, baseSecretId: string): string {
108
+ return `${projectId}_${baseSecretId}`;
109
+ }
110
+
111
+ function buildSecretPath(projectId: string, secretName: string): string {
112
+ return `projects/${projectId}/secrets/${secretName}`;
113
+ }
114
+
115
+ function buildSecretVersionPath(projectId: string, secretName: string, version: string = 'latest'): string {
116
+ return `projects/${projectId}/secrets/${secretName}/versions/${version}`;
117
+ }
118
+
65
119
  const rl = readline.createInterface({
66
120
  input: process.stdin,
67
121
  output: process.stdout,
@@ -87,12 +141,29 @@ function obfuscateCred(cred: string | null): string {
87
141
 
88
142
  // --- Generic Environment Variable Accessors ---
89
143
  async function getEnvVar(baseSecretKey: string): Promise<string | null> {
90
- const sm = await getSecretManager();
91
- if (!sm) {
144
+ const smClient = await getSecretManagerClient();
145
+ if (!smClient) {
92
146
  console.warn(`[EnvService.ts] SecretManager not available for getEnvVar (key: ${baseSecretKey})`);
93
147
  return null;
94
148
  }
95
- return sm.getSecret(baseSecretKey);
149
+
150
+ try {
151
+ const secretName = buildSecretName(smClient.projectId, baseSecretKey);
152
+ const secretVersionPath = buildSecretVersionPath(smClient.projectId, secretName);
153
+
154
+ const [response] = await smClient.client.accessSecretVersion({
155
+ name: secretVersionPath,
156
+ });
157
+
158
+ const secretValue = response.payload?.data?.toString();
159
+ return secretValue || null;
160
+ } catch (error: any) {
161
+ if (error.code === 5) { // NOT_FOUND
162
+ return null;
163
+ }
164
+ console.warn(`[EnvService.ts] Failed to get secret ${baseSecretKey}: ${error.message}`);
165
+ return null;
166
+ }
96
167
  }
97
168
 
98
169
  export async function getCustomSearchApiKey(): Promise<string> {
@@ -103,10 +174,10 @@ export async function getCustomSearchApiKey(): Promise<string> {
103
174
  return apiKey;
104
175
  }
105
176
 
106
- export async function getSearchEngineId(): Promise<string> {
107
- const searchEngineId = await getEnvVar('SEARCH_ENGINE_ID');
177
+ export async function getCustomSearchEngineId(): Promise<string> {
178
+ const searchEngineId = await getEnvVar('CUSTOM_SEARCH_ENGINE_ID');
108
179
  if (!searchEngineId) {
109
- throw new Error('SEARCH_ENGINE_ID not found in environment variables.');
180
+ throw new Error('CUSTOM_SEARCH_ENGINE_ID not found in environment variables.');
110
181
  }
111
182
  return searchEngineId;
112
183
  }
@@ -141,21 +212,67 @@ export async function getGcpProjectId(): Promise<string> {
141
212
  }
142
213
 
143
214
  async function setEnvVar(baseSecretKey: string, value: string): Promise<boolean> {
144
- const sm = await getSecretManager();
145
- if (!sm) {
215
+ const smClient = await getSecretManagerClient();
216
+ if (!smClient) {
146
217
  console.warn(`[EnvService.ts] SecretManager not available for setEnvVar (key: ${baseSecretKey})`);
147
218
  return false; // Indicate failure
148
219
  }
149
- // TODO: need to get change iamPolicy permissions
150
- // const currentUserOnly = true;
151
- return sm.setSecret(baseSecretKey, value);
220
+
221
+ try {
222
+ const secretName = buildSecretName(smClient.projectId, baseSecretKey);
223
+
224
+ // Try to add a version first
225
+ const addVersion = async (): Promise<void> => {
226
+ const secretPath = buildSecretPath(smClient.projectId, secretName);
227
+ await smClient.client.addSecretVersion({
228
+ parent: secretPath,
229
+ payload: {
230
+ data: Buffer.from(value, 'utf8'),
231
+ },
232
+ });
233
+ console.log(`✅ Secret '${baseSecretKey}' saved in GCP project: ${smClient.projectId}`);
234
+ };
235
+
236
+ try {
237
+ await addVersion();
238
+ return true;
239
+ } catch (err: any) {
240
+ if (err.code === 5) { // NOT_FOUND → create then retry
241
+ // Create the secret resource first
242
+ await smClient.client.createSecret({
243
+ parent: `projects/${smClient.projectId}`,
244
+ secretId: secretName,
245
+ secret: {
246
+ replication: {
247
+ automatic: {},
248
+ },
249
+ },
250
+ });
251
+ console.log(`[EnvService.ts] Created secret resource: ${secretName}`);
252
+
253
+ await addVersion();
254
+ return true;
255
+ }
256
+ console.warn(`[EnvService.ts] setEnvVar failed: ${err.message}`);
257
+ return false;
258
+ }
259
+ } catch (error: any) {
260
+ console.warn(`[EnvService.ts] Failed to set secret ${baseSecretKey}: ${error.message}`);
261
+ return false;
262
+ }
152
263
  }
153
264
 
154
265
  // --- Specific Setters (Now use generic setEnvVar) ---
155
266
  const setLogToConsole = async (): Promise<void> => {
156
267
  const logToConsole = await askQuestion('Use consoleLogger to log to console? (yes, no)', 'no');
157
268
  if (logToConsole) {
158
- await setEnvVar('LOG_TO_CONSOLE', logToConsole);
269
+ // Normalize to lowercase and validate - only 'yes' or 'no' are valid
270
+ const normalizedValue = logToConsole.toLowerCase();
271
+ if (normalizedValue === 'yes' || normalizedValue === 'no') {
272
+ await setEnvVar('LOG_TO_CONSOLE', normalizedValue);
273
+ } else {
274
+ console.warn(`Invalid LOG_TO_CONSOLE value: '${logToConsole}'. Only 'yes' or 'no' are valid.`);
275
+ }
159
276
  }
160
277
  };
161
278
 
@@ -274,6 +391,13 @@ const setCustomSearchApiKey = async (): Promise<void> => {
274
391
  }
275
392
  };
276
393
 
394
+ const setCustomSearchEngineId = async (): Promise<void> => {
395
+ const customSearchEngineId = await askQuestion('Enter Custom Search Engine ID', '');
396
+ if (customSearchEngineId) {
397
+ await setEnvVar('CUSTOM_SEARCH_ENGINE_ID', customSearchEngineId);
398
+ }
399
+ };
400
+
277
401
  const setFirestoreProjectId = async (): Promise<void> => {
278
402
  const firestoreProjectId = await askQuestion('Enter Firestore Project ID', 'dataformer-prod');
279
403
  if (firestoreProjectId) {
@@ -315,12 +439,12 @@ const getProjectRoot = (): string => {
315
439
  };
316
440
 
317
441
  const _getResolvedGcpProjectId = async (): Promise<string | null> => {
318
- const sm = await getSecretManager();
319
- if (!sm) {
442
+ const smClient = await getSecretManagerClient();
443
+ if (!smClient) {
320
444
  console.warn('[EnvService.ts] SecretManager not available for _getResolvedGcpProjectId');
321
445
  return null;
322
446
  }
323
- return sm.getResolvedProjectId();
447
+ return smClient.projectId;
324
448
  };
325
449
 
326
450
  const printEnv = async (): Promise<void> => {
@@ -342,6 +466,7 @@ const printEnv = async (): Promise<void> => {
342
466
  const jiraUserEmail = await getJiraUserEmail();
343
467
  const geminiApiKey = await getGeminiApiKey();
344
468
  const customSearchApiKey = await getCustomSearchApiKey();
469
+ const customSearchEngineId = await getCustomSearchEngineId();
345
470
  const gcpProjectIdVal = await getGcpProjectId();
346
471
  const npmToken = await getNpmToken();
347
472
 
@@ -364,6 +489,7 @@ const printEnv = async (): Promise<void> => {
364
489
  console.log(`Jira API Base URL: ${jiraApiBaseUrl}`);
365
490
  console.log(`Jira User Email: ${jiraUserEmail}`);
366
491
  console.log(`Custom Search API Key: ${obfuscateCred(customSearchApiKey)}`);
492
+ console.log(`Custom Search Engine ID: ${customSearchEngineId}`);
367
493
  console.log(`Gemini API Key: ${obfuscateCred(geminiApiKey)}`);
368
494
  console.log(`NPM Token: ${obfuscateCred(npmToken)}`);
369
495
  };
@@ -390,6 +516,7 @@ const run = async (): Promise<void> => {
390
516
  case 'setGeminiApiKey': await setGeminiApiKey(); break;
391
517
  case 'setNpmToken': await setNpmToken(); break;
392
518
  case 'setCustomSearchApiKey': await setCustomSearchApiKey(); break;
519
+ case 'setCustomSearchEngineId': await setCustomSearchEngineId(); break;
393
520
  case 'printEnv': await printEnv(); break;
394
521
  default: console.log('Unknown command. Available commands: setEnv, setBearerToken, ..., printEnv');
395
522
  }
@@ -431,6 +558,7 @@ export {
431
558
  setGeminiApiKey,
432
559
  setNpmToken,
433
560
  setCustomSearchApiKey,
561
+ setCustomSearchEngineId,
434
562
 
435
563
  // Getters
436
564
  getLogToConsole,