@griffin-app/griffin-cli 1.0.11 → 1.0.13

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.
@@ -2,6 +2,14 @@ import { loadState } from "../../core/state.js";
2
2
  import { createSdkWithCredentials } from "../../core/sdk.js";
3
3
  import { terminal } from "../../utils/terminal.js";
4
4
  import { withSDKErrorHandling } from "../../utils/sdk-error.js";
5
+ function orExit(value, errorMessage) {
6
+ if (value === undefined) {
7
+ terminal.error(errorMessage);
8
+ terminal.exit(1);
9
+ throw new Error(errorMessage);
10
+ }
11
+ return value;
12
+ }
5
13
  /**
6
14
  * List notification rules (read-only).
7
15
  * Rules are defined in monitor DSL and synced via `griffin hub apply`.
@@ -55,8 +63,102 @@ export async function executeNotificationsList(options) {
55
63
  terminal.exit(1);
56
64
  }
57
65
  }
66
+ async function fetchNotificationIntegrations(sdk) {
67
+ const result = await withSDKErrorHandling(() => sdk.getIntegrations({
68
+ query: { category: "notifications", enabled: true },
69
+ }), "Failed to fetch integrations");
70
+ const list = result?.data?.data ?? [];
71
+ return list
72
+ .filter((i) => i.category === "notifications")
73
+ .map((i) => ({
74
+ id: i.id,
75
+ name: i.name,
76
+ provider: i.provider,
77
+ }));
78
+ }
79
+ function promptRoutingForProvider(provider) {
80
+ switch (provider) {
81
+ case "slack": {
82
+ return terminal
83
+ .input("Slack channel (e.g. #alerts)", "#alerts")
84
+ .then((value) => ({
85
+ provider: "slack",
86
+ channel: (value ?? "").trim() || "#alerts",
87
+ }));
88
+ }
89
+ case "email": {
90
+ return terminal.input("To addresses (comma-separated)").then((value) => {
91
+ const toAddresses = (value ?? "")
92
+ .split(",")
93
+ .map((s) => s.trim())
94
+ .filter(Boolean);
95
+ if (toAddresses.length === 0) {
96
+ terminal.error("At least one email address is required.");
97
+ terminal.exit(1);
98
+ }
99
+ return { provider: "email", toAddresses };
100
+ });
101
+ }
102
+ default:
103
+ return Promise.resolve({ provider: "webhook" });
104
+ }
105
+ }
58
106
  /**
59
- * Test a webhook
107
+ * Build routing object from CLI options. Exactly one of channel, toAddresses, or webhook (none) must be used.
108
+ */
109
+ function buildRoutingFromOptions(options) {
110
+ if (options.channel !== undefined && options.channel !== "") {
111
+ return { provider: "slack", channel: options.channel };
112
+ }
113
+ if (options.toAddresses !== undefined && options.toAddresses !== "") {
114
+ const toAddresses = options.toAddresses
115
+ .split(",")
116
+ .map((s) => s.trim())
117
+ .filter(Boolean);
118
+ if (toAddresses.length > 0) {
119
+ return { provider: "email", toAddresses };
120
+ }
121
+ }
122
+ return { provider: "webhook" };
123
+ }
124
+ /**
125
+ * Run interactive wizard: select integration, then prompt for provider-specific options (channel, to-addresses, or none).
126
+ */
127
+ async function executeNotificationsTestWizard(sdk, options) {
128
+ const integrations = await fetchNotificationIntegrations(sdk);
129
+ if (integrations.length === 0) {
130
+ terminal.error("No notification integrations found.");
131
+ terminal.dim("Connect one with: griffin hub integrations connect notifications <provider>");
132
+ terminal.exit(1);
133
+ }
134
+ if (options.integration) {
135
+ const match = orExit(integrations.find((i) => i.id === options.integration ||
136
+ i.name.toLowerCase() === (options.integration ?? "").toLowerCase()), `Integration not found: ${options.integration}`);
137
+ const routing = await promptRoutingForProvider(match.provider);
138
+ return { integration: match.id, routing };
139
+ }
140
+ const choiceLabels = integrations.map((i) => `${i.name} (${i.provider})`);
141
+ const choice = await terminal.select("Select integration to test", choiceLabels);
142
+ const found = orExit(integrations.find((i) => `${i.name} (${i.provider})` === choice), "No integration selected.");
143
+ const routing = await promptRoutingForProvider(found.provider);
144
+ return { integration: found.id, routing };
145
+ }
146
+ function canBuildRoutingFromOptions(options) {
147
+ if (options.channel !== undefined && options.channel !== "")
148
+ return true;
149
+ if (options.toAddresses !== undefined && options.toAddresses !== "") {
150
+ const toAddresses = options.toAddresses
151
+ .split(",")
152
+ .map((s) => s.trim())
153
+ .filter(Boolean);
154
+ if (toAddresses.length > 0)
155
+ return true;
156
+ }
157
+ return false;
158
+ }
159
+ /**
160
+ * Test a notification integration. Without options, runs a wizard (select integration, then provider-specific prompts).
161
+ * With --integration and routing flags, runs non-interactively.
60
162
  */
61
163
  export async function executeNotificationsTest(options) {
62
164
  try {
@@ -68,14 +170,21 @@ export async function executeNotificationsTest(options) {
68
170
  terminal.exit(1);
69
171
  }
70
172
  const sdk = await createSdkWithCredentials(state.hub.baseUrl);
173
+ const useWizard = !options.integration || !canBuildRoutingFromOptions(options);
174
+ let integration;
175
+ let routing;
176
+ if (useWizard) {
177
+ const wizardResult = await executeNotificationsTestWizard(sdk, options);
178
+ integration = wizardResult.integration;
179
+ routing = wizardResult.routing;
180
+ }
181
+ else {
182
+ integration = options.integration;
183
+ routing = buildRoutingFromOptions(options);
184
+ }
71
185
  const spinner = terminal.spinner("Sending test notification...").start();
72
186
  const response = await withSDKErrorHandling(() => sdk.postNotificationsTest({
73
- body: {
74
- channel: {
75
- type: "webhook",
76
- url: options.webhook,
77
- },
78
- },
187
+ body: { integration, routing },
79
188
  }), "Failed to send test notification");
80
189
  spinner.stop();
81
190
  const result = response?.data?.data;
@@ -1,19 +1,6 @@
1
- import fs from "node:fs/promises";
2
- import path from "node:path";
3
- import { initState, stateExists, getStateFilePath, } from "../core/state.js";
1
+ import { initState, stateExists, getStateFilePath } from "../core/state.js";
4
2
  import { detectProjectId } from "../core/project.js";
5
3
  import { terminal } from "../utils/terminal.js";
6
- const VARIABLES_FILE = "variables.yaml";
7
- const VARIABLES_TEMPLATE = `# Per-environment variables for test monitors. Reference in monitors with variable("key").
8
- # Edit values below for each environment.
9
- environments:
10
- dev:
11
- # api_key: "your-api-key"
12
- staging:
13
- # api_key: "your-staging-key"
14
- production:
15
- # api_key: "your-production-key"
16
- `;
17
4
  /**
18
5
  * Initialize griffin in the current directory
19
6
  */
@@ -31,30 +18,21 @@ export async function executeInit(options) {
31
18
  projectId = await detectProjectId();
32
19
  }
33
20
  spinner.succeed(`Project: ${terminal.colors.cyan(projectId)}`);
34
- // Initialize state file
21
+ // Initialize state file (includes default environment)
35
22
  await initState(projectId);
36
23
  terminal.success(`Created state file: ${terminal.colors.dim(getStateFilePath())}`);
37
- // Create variables.yaml if it doesn't exist
38
- const variablesPath = path.join(process.cwd(), VARIABLES_FILE);
39
- try {
40
- await fs.access(variablesPath);
41
- terminal.dim(`variables.yaml already exists, skipping`);
42
- }
43
- catch {
44
- await fs.writeFile(variablesPath, VARIABLES_TEMPLATE, "utf-8");
45
- terminal.success(`Created ${terminal.colors.dim(VARIABLES_FILE)}`);
46
- }
47
24
  terminal.blank();
48
25
  terminal.success("Initialization complete!");
49
26
  terminal.blank();
50
27
  terminal.info("Next steps:");
51
- terminal.dim(" 1. Edit variables.yaml to set api_host and other variables per environment");
28
+ terminal.dim(" 1. Add variables for your environment (optional):");
29
+ terminal.dim(" griffin variables add API_KEY=your-key --env default");
52
30
  terminal.dim(" 2. Create test monitors (*.ts files in __griffin__/ directories)");
53
- terminal.dim(" 3. Run tests locally (pass environment name):");
54
- terminal.dim(" griffin local run dev");
31
+ terminal.dim(" 3. Run tests locally:");
32
+ terminal.dim(" griffin local run");
55
33
  terminal.dim(" 4. Connect to hub (optional):");
56
34
  terminal.dim(" griffin hub connect --url <url> --token <token>");
57
35
  terminal.dim(" 5. Deploy to hub:");
58
- terminal.dim(" griffin hub apply dev");
36
+ terminal.dim(" griffin hub apply");
59
37
  terminal.blank();
60
38
  }
@@ -0,0 +1,12 @@
1
+ /**
2
+ * List all variables for an environment
3
+ */
4
+ export declare function executeVariablesList(env: string): Promise<void>;
5
+ /**
6
+ * Add or update a variable for an environment
7
+ */
8
+ export declare function executeVariablesAdd(keyValue: string, env: string): Promise<void>;
9
+ /**
10
+ * Remove a variable from an environment
11
+ */
12
+ export declare function executeVariablesRemove(key: string, env: string): Promise<void>;
@@ -0,0 +1,87 @@
1
+ import { loadState, saveState, getEnvironment } from "../core/state.js";
2
+ import { terminal } from "../utils/terminal.js";
3
+ /**
4
+ * List all variables for an environment
5
+ */
6
+ export async function executeVariablesList(env) {
7
+ try {
8
+ const state = await loadState();
9
+ const envConfig = await getEnvironment(env);
10
+ if (!envConfig.variables || Object.keys(envConfig.variables).length === 0) {
11
+ terminal.warn(`No variables defined for environment '${env}'.`);
12
+ terminal.blank();
13
+ terminal.dim(`Add variables with: griffin variables add KEY=VALUE --env ${env}`);
14
+ terminal.blank();
15
+ return;
16
+ }
17
+ terminal.info(`Variables for environment '${env}':`);
18
+ terminal.blank();
19
+ for (const [key, value] of Object.entries(envConfig.variables)) {
20
+ const keyDisplay = terminal.colors.cyan(key);
21
+ const valueDisplay = terminal.colors.dim(value);
22
+ terminal.log(` ${keyDisplay} = ${valueDisplay}`);
23
+ }
24
+ terminal.blank();
25
+ }
26
+ catch (error) {
27
+ terminal.error(error.message);
28
+ terminal.exit(1);
29
+ }
30
+ }
31
+ /**
32
+ * Add or update a variable for an environment
33
+ */
34
+ export async function executeVariablesAdd(keyValue, env) {
35
+ try {
36
+ // Parse KEY=VALUE format
37
+ const match = keyValue.match(/^([^=]+)=(.*)$/);
38
+ if (!match) {
39
+ throw new Error("Invalid format. Use: griffin variables add KEY=VALUE --env <env>");
40
+ }
41
+ const [, key, value] = match;
42
+ const trimmedKey = key.trim();
43
+ const trimmedValue = value.trim();
44
+ if (!trimmedKey) {
45
+ throw new Error("Variable key cannot be empty");
46
+ }
47
+ const state = await loadState();
48
+ const envConfig = await getEnvironment(env);
49
+ // Initialize variables object if it doesn't exist
50
+ if (!envConfig.variables) {
51
+ envConfig.variables = {};
52
+ }
53
+ // Add or update the variable
54
+ envConfig.variables[trimmedKey] = trimmedValue;
55
+ // Update the environment in state
56
+ state.environments[env] = envConfig;
57
+ await saveState(state);
58
+ terminal.success(`Variable '${trimmedKey}' set for environment '${env}'.`);
59
+ terminal.blank();
60
+ }
61
+ catch (error) {
62
+ terminal.error(error.message);
63
+ terminal.exit(1);
64
+ }
65
+ }
66
+ /**
67
+ * Remove a variable from an environment
68
+ */
69
+ export async function executeVariablesRemove(key, env) {
70
+ try {
71
+ const state = await loadState();
72
+ const envConfig = await getEnvironment(env);
73
+ if (!envConfig.variables || !(key in envConfig.variables)) {
74
+ throw new Error(`Variable '${key}' not found in environment '${env}'`);
75
+ }
76
+ delete envConfig.variables[key];
77
+ // Update the environment in state
78
+ state.environments[env] = envConfig;
79
+ await saveState(state);
80
+ terminal.success(`Variable '${key}' removed from environment '${env}'.`);
81
+ terminal.blank();
82
+ }
83
+ catch (error) {
84
+ terminal.error(error.message);
85
+ terminal.exit(1);
86
+ }
87
+ }
@@ -9,6 +9,7 @@ function createMonitor(name) {
9
9
  environment: "test",
10
10
  version: "1.0",
11
11
  frequency: { every: 5, unit: "MINUTE" },
12
+ notifications: [],
12
13
  nodes: [],
13
14
  edges: [],
14
15
  };
@@ -11,6 +11,7 @@ function createMonitor(name, overrides) {
11
11
  frequency: { every: 5, unit: "MINUTE" },
12
12
  nodes: [],
13
13
  edges: [],
14
+ notifications: [],
14
15
  ...overrides,
15
16
  };
16
17
  }
@@ -24,6 +25,7 @@ function createResolvedMonitor(name, overrides) {
24
25
  frequency: { every: 5, unit: "MINUTE" },
25
26
  nodes: [],
26
27
  edges: [],
28
+ notifications: [],
27
29
  ...overrides,
28
30
  };
29
31
  }
@@ -1,3 +1,4 @@
1
+ import "tsx";
1
2
  import type { MonitorDSL } from "@griffin-app/griffin-ts/types";
2
3
  export interface DiscoveredMonitor {
3
4
  monitor: MonitorDSL;
@@ -1,3 +1,4 @@
1
+ import "tsx";
1
2
  import { glob } from "glob";
2
3
  import path from "node:path";
3
4
  import { pathToFileURL } from "node:url";
@@ -45,16 +46,25 @@ async function loadMonitorsFromFile(filePath) {
45
46
  const module = await import(fileUrl);
46
47
  // Check default export
47
48
  if (module.default) {
48
- if (isMonitor(module.default)) {
49
+ // Handle potential double-wrapping from tsx/esModuleInterop
50
+ // When tsx transpiles TS files with esModuleInterop, it may wrap the default export
51
+ let defaultExport = module.default;
52
+ if (typeof defaultExport === "object" &&
53
+ defaultExport !== null &&
54
+ "default" in defaultExport &&
55
+ typeof defaultExport.default === "object") {
56
+ defaultExport = defaultExport.default;
57
+ }
58
+ if (isMonitor(defaultExport)) {
49
59
  monitors.push({
50
- monitor: module.default,
60
+ monitor: defaultExport,
51
61
  filePath,
52
62
  exportName: "default",
53
63
  });
54
64
  }
55
65
  else {
56
- const errors = Value.Errors(MonitorDSLSchema, module.default);
57
- throw new Error(`Default export is not a valid TestMonitor. Got: ${JSON.stringify(errors, null, 2)}`);
66
+ const errors = Value.Errors(MonitorDSLSchema, defaultExport);
67
+ throw new Error(`Default export is not a valid TestMonitor. Got: ${JSON.stringify([...errors], null, 2)}`);
58
68
  }
59
69
  }
60
70
  if (monitors.length === 0) {
@@ -247,6 +247,21 @@ function compareTopLevel(local, remote) {
247
247
  newValue: localLocations,
248
248
  });
249
249
  }
250
+ // Compare notifications (normalize empty array to undefined)
251
+ const localNotifications = local.notifications && local.notifications.length > 0
252
+ ? local.notifications
253
+ : undefined;
254
+ const remoteNotifications = remote.notifications && remote.notifications.length > 0
255
+ ? remote.notifications
256
+ : undefined;
257
+ console.log("REMOTE NOTIFICATIONS", remoteNotifications);
258
+ if (!deepEqual(localNotifications, remoteNotifications)) {
259
+ changes.push({
260
+ field: "notifications",
261
+ oldValue: remoteNotifications,
262
+ newValue: localNotifications,
263
+ });
264
+ }
250
265
  return changes;
251
266
  }
252
267
  /**
@@ -1,9 +1,9 @@
1
1
  /**
2
- * Load variables from variables.yaml for a specific environment.
2
+ * Load variables from state file for a specific environment.
3
3
  *
4
4
  * @param envName - The environment name to load variables for
5
5
  * @returns Record of variable key-value pairs
6
- * @throws Error if variables.yaml doesn't exist or environment not found
6
+ * @throws Error if environment not found or has no variables
7
7
  */
8
8
  export declare function loadVariables(envName: string): Promise<Record<string, string>>;
9
9
  /**
@@ -1,33 +1,15 @@
1
- import fs from "node:fs/promises";
2
- import path from "node:path";
3
- import { parse } from "yaml";
1
+ import { getEnvironment } from "./state.js";
4
2
  /**
5
- * Load variables from variables.yaml for a specific environment.
3
+ * Load variables from state file for a specific environment.
6
4
  *
7
5
  * @param envName - The environment name to load variables for
8
6
  * @returns Record of variable key-value pairs
9
- * @throws Error if variables.yaml doesn't exist or environment not found
7
+ * @throws Error if environment not found or has no variables
10
8
  */
11
9
  export async function loadVariables(envName) {
12
- const yamlPath = path.join(process.cwd(), "variables.yaml");
13
- try {
14
- const content = await fs.readFile(yamlPath, "utf-8");
15
- const config = parse(content);
16
- if (!config.environments) {
17
- throw new Error(`Invalid variables.yaml: missing "environments" key.\nExpected format:\nenvironments:\n ${envName}:\n key: value`);
18
- }
19
- if (!config.environments[envName]) {
20
- const availableEnvs = Object.keys(config.environments);
21
- throw new Error(`Environment "${envName}" not found in variables.yaml.\nAvailable environments: ${availableEnvs.join(", ")}`);
22
- }
23
- return config.environments[envName];
24
- }
25
- catch (error) {
26
- if (error.code === "ENOENT") {
27
- throw new Error(`variables.yaml not found in ${process.cwd()}.\nCreate one with:\nenvironments:\n ${envName}:\n my-variable: my-value`);
28
- }
29
- throw error;
30
- }
10
+ const envConfig = await getEnvironment(envName);
11
+ // Return empty object if no variables defined (allows monitors with no variable refs to work)
12
+ return envConfig.variables || {};
31
13
  }
32
14
  /**
33
15
  * Check if a value is a variable reference.
@@ -0,0 +1,24 @@
1
+ /**
2
+ * Hardcoded provider registry for integration connect.
3
+ * Defines categories, providers, auth methods, and required fields.
4
+ */
5
+ export type AuthMethod = "oauth" | "credentials";
6
+ export interface ProviderField {
7
+ key: string;
8
+ label: string;
9
+ secret?: boolean;
10
+ required?: boolean;
11
+ default?: string;
12
+ }
13
+ export interface ProviderDefinition {
14
+ category: string;
15
+ provider: string;
16
+ displayName: string;
17
+ authMethod: AuthMethod;
18
+ configFields?: ProviderField[];
19
+ credentialFields?: ProviderField[];
20
+ }
21
+ export declare function getCategories(): string[];
22
+ export declare function getProviders(category: string): ProviderDefinition[];
23
+ export declare function getProvider(category: string, provider: string): ProviderDefinition | null;
24
+ export declare function getAllProvidersByCategory(): Map<string, ProviderDefinition[]>;
@@ -0,0 +1,123 @@
1
+ /**
2
+ * Hardcoded provider registry for integration connect.
3
+ * Defines categories, providers, auth methods, and required fields.
4
+ */
5
+ const providers = [
6
+ // Notifications - OAuth
7
+ {
8
+ category: "notifications",
9
+ provider: "slack",
10
+ displayName: "Slack",
11
+ authMethod: "oauth",
12
+ },
13
+ {
14
+ category: "notifications",
15
+ provider: "pagerduty",
16
+ displayName: "PagerDuty",
17
+ authMethod: "oauth",
18
+ },
19
+ // Notifications - Credentials
20
+ {
21
+ category: "notifications",
22
+ provider: "webhook",
23
+ displayName: "Webhook",
24
+ authMethod: "credentials",
25
+ configFields: [{ key: "url", label: "Webhook URL", required: true }],
26
+ },
27
+ {
28
+ category: "notifications",
29
+ provider: "slack_webhook",
30
+ displayName: "Slack Webhook",
31
+ authMethod: "credentials",
32
+ credentialFields: [
33
+ {
34
+ key: "webhook_url",
35
+ label: "Webhook URL",
36
+ secret: true,
37
+ required: true,
38
+ },
39
+ ],
40
+ },
41
+ {
42
+ category: "notifications",
43
+ provider: "email",
44
+ displayName: "Email (SES)",
45
+ authMethod: "credentials",
46
+ configFields: [
47
+ { key: "fromAddress", label: "From Address", required: true },
48
+ { key: "sesRegion", label: "SES Region", required: true },
49
+ {
50
+ key: "toAddresses",
51
+ label: "To Addresses (comma-separated)",
52
+ required: true,
53
+ },
54
+ ],
55
+ },
56
+ // Secrets - Credentials
57
+ {
58
+ category: "secrets",
59
+ provider: "env",
60
+ displayName: "Environment Variables",
61
+ authMethod: "credentials",
62
+ configFields: [{ key: "prefix", label: "Env Prefix", required: false }],
63
+ },
64
+ {
65
+ category: "secrets",
66
+ provider: "aws",
67
+ displayName: "AWS Secrets Manager",
68
+ authMethod: "credentials",
69
+ configFields: [{ key: "region", label: "AWS Region", required: true }],
70
+ credentialFields: [
71
+ { key: "accessKeyId", label: "Access Key ID", secret: true },
72
+ { key: "secretAccessKey", label: "Secret Access Key", secret: true },
73
+ ],
74
+ },
75
+ {
76
+ category: "secrets",
77
+ provider: "vault",
78
+ displayName: "HashiCorp Vault",
79
+ authMethod: "credentials",
80
+ configFields: [{ key: "address", label: "Vault Address", required: true }],
81
+ credentialFields: [{ key: "token", label: "Vault Token", secret: true }],
82
+ },
83
+ // Metrics - Credentials
84
+ {
85
+ category: "metrics",
86
+ provider: "datadog",
87
+ displayName: "Datadog",
88
+ authMethod: "credentials",
89
+ configFields: [
90
+ {
91
+ key: "site",
92
+ label: "Datadog Site",
93
+ default: "datadoghq.com",
94
+ },
95
+ ],
96
+ credentialFields: [
97
+ {
98
+ key: "api_key",
99
+ label: "API Key",
100
+ secret: true,
101
+ required: true,
102
+ },
103
+ ],
104
+ },
105
+ ];
106
+ const categories = ["notifications", "secrets", "metrics"];
107
+ export function getCategories() {
108
+ return [...categories];
109
+ }
110
+ export function getProviders(category) {
111
+ return providers.filter((p) => p.category === category);
112
+ }
113
+ export function getProvider(category, provider) {
114
+ return (providers.find((p) => p.category === category && p.provider === provider) ??
115
+ null);
116
+ }
117
+ export function getAllProvidersByCategory() {
118
+ const map = new Map();
119
+ for (const cat of categories) {
120
+ map.set(cat, getProviders(cat));
121
+ }
122
+ return map;
123
+ }
@@ -1,5 +1,7 @@
1
1
  import { Type, type Static } from "typebox";
2
- export declare const EnvironmentConfigSchema: Type.TObject<{}>;
2
+ export declare const EnvironmentConfigSchema: Type.TObject<{
3
+ variables: Type.TOptional<Type.TRecord<"^.*$", Type.TString>>;
4
+ }>;
3
5
  export type EnvironmentConfig = Static<typeof EnvironmentConfigSchema>;
4
6
  export declare const CloudConfigSchema: Type.TObject<{
5
7
  authUrl: Type.TString;
@@ -15,14 +17,22 @@ export declare const DiscoveryConfigSchema: Type.TObject<{
15
17
  }>;
16
18
  export type DiscoveryConfig = Static<typeof DiscoveryConfigSchema>;
17
19
  export declare const EnvironmentsConfigSchema: Type.TIntersect<[Type.TObject<{
18
- default: Type.TObject<{}>;
19
- }>, Type.TRecord<"^.*$", Type.TObject<{}>>]>;
20
+ default: Type.TObject<{
21
+ variables: Type.TOptional<Type.TRecord<"^.*$", Type.TString>>;
22
+ }>;
23
+ }>, Type.TRecord<"^.*$", Type.TObject<{
24
+ variables: Type.TOptional<Type.TRecord<"^.*$", Type.TString>>;
25
+ }>>]>;
20
26
  export declare const StateFileSchema: Type.TObject<{
21
27
  stateVersion: Type.TLiteral<1>;
22
28
  projectId: Type.TString;
23
29
  environments: Type.TIntersect<[Type.TObject<{
24
- default: Type.TObject<{}>;
25
- }>, Type.TRecord<"^.*$", Type.TObject<{}>>]>;
30
+ default: Type.TObject<{
31
+ variables: Type.TOptional<Type.TRecord<"^.*$", Type.TString>>;
32
+ }>;
33
+ }>, Type.TRecord<"^.*$", Type.TObject<{
34
+ variables: Type.TOptional<Type.TRecord<"^.*$", Type.TString>>;
35
+ }>>]>;
26
36
  hub: Type.TObject<{
27
37
  baseUrl: Type.TString;
28
38
  clientId: Type.TOptional<Type.TString>;
@@ -8,7 +8,9 @@ import { Type } from "typebox";
8
8
  */
9
9
  const authUrl = "https://www.getgriffinapp.com/api/auth";
10
10
  const hubBaseUrl = "https://griffin-hub.fly.dev";
11
- export const EnvironmentConfigSchema = Type.Object({});
11
+ export const EnvironmentConfigSchema = Type.Object({
12
+ variables: Type.Optional(Type.Record(Type.String(), Type.String())),
13
+ });
12
14
  export const CloudConfigSchema = Type.Object({
13
15
  authUrl: Type.String(),
14
16
  });
@@ -63,6 +63,10 @@ export declare class Terminal {
63
63
  * Prompt user for text input
64
64
  */
65
65
  input(message: string, initial?: string): Promise<string>;
66
+ /**
67
+ * Prompt user for hidden input (password/secret)
68
+ */
69
+ password(message: string): Promise<string>;
66
70
  /**
67
71
  * Create a formatted table
68
72
  */
@@ -105,6 +105,17 @@ export class Terminal {
105
105
  });
106
106
  return response.value;
107
107
  }
108
+ /**
109
+ * Prompt user for hidden input (password/secret)
110
+ */
111
+ async password(message) {
112
+ const response = await enquirer.prompt({
113
+ type: "password",
114
+ name: "value",
115
+ message,
116
+ });
117
+ return response.value;
118
+ }
108
119
  /**
109
120
  * Create a formatted table
110
121
  */