@underlingscloud/cli 0.2.1 → 0.2.3

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.
@@ -0,0 +1,8 @@
1
+ export declare const runProject: (commandParts: string[]) => Promise<void>;
2
+ export declare const envProject: (options: {
3
+ project?: string;
4
+ branch?: string;
5
+ format?: string;
6
+ filter?: string;
7
+ }) => Promise<void>;
8
+ export declare function fetchProjectVariables(projectSlug: string, branch: string, token: string): Promise<Record<string, string>>;
@@ -0,0 +1,193 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.envProject = exports.runProject = void 0;
7
+ exports.fetchProjectVariables = fetchProjectVariables;
8
+ const cross_spawn_1 = __importDefault(require("cross-spawn"));
9
+ const child_process_1 = require("child_process");
10
+ const chalk_1 = __importDefault(require("chalk"));
11
+ const axios_1 = __importDefault(require("axios"));
12
+ const constants_1 = require("../constants");
13
+ const config_1 = require("../utils/config");
14
+ const auth_1 = require("../utils/auth");
15
+ const runProject = async (commandParts) => {
16
+ if (commandParts.length === 0) {
17
+ console.error(chalk_1.default.red('Error: No command provided to run.'));
18
+ console.log('Usage: underlings project run <command>');
19
+ return;
20
+ }
21
+ if (!(0, auth_1.isAuthenticated)()) {
22
+ console.error(chalk_1.default.red('Error: You are not logged in.'));
23
+ console.log('Run "underlings auth login" to authenticate.');
24
+ return;
25
+ }
26
+ const token = (0, auth_1.getAuthToken)();
27
+ // Read project config
28
+ if (!(0, config_1.hasProjectConfig)()) {
29
+ console.error(chalk_1.default.red('Error: No ulproject.json (or ccproject.json) found in the ancestry tree.'));
30
+ console.log('Please run "underlings project init" in your repository root.');
31
+ return;
32
+ }
33
+ let config;
34
+ try {
35
+ config = (0, config_1.readProjectConfig)();
36
+ }
37
+ catch (e) {
38
+ console.error(chalk_1.default.red('Error reading project config file.'));
39
+ return;
40
+ }
41
+ if (!config.project) {
42
+ console.error(chalk_1.default.red('Error: Invalid project config. Missing "project" identifier.'));
43
+ return;
44
+ }
45
+ // Get git branch
46
+ let branch;
47
+ try {
48
+ branch = (0, child_process_1.execSync)('git rev-parse --abbrev-ref HEAD').toString().trim();
49
+ }
50
+ catch (e) {
51
+ console.error(chalk_1.default.red('Error: Could not determine git branch. Are you in a git repository?'));
52
+ return;
53
+ }
54
+ if (!branch) {
55
+ console.error(chalk_1.default.red('Error: Current branch is empty.'));
56
+ return;
57
+ }
58
+ console.log(chalk_1.default.dim(`Detected branch: ${branch}`));
59
+ // Fetch project variables
60
+ console.log(chalk_1.default.blue('Fetching project variables...'));
61
+ let envVars = {};
62
+ try {
63
+ envVars = await fetchProjectVariables(config.project, branch, token);
64
+ console.log(chalk_1.default.green(`Successfully loaded ${Object.keys(envVars).length} variables for project: ${config.project}`));
65
+ }
66
+ catch (error) {
67
+ const msg = error.message || '';
68
+ const isNetworkError = error.code === 'ECONNREFUSED' || error.code === 'ECONNABORTED'
69
+ || error.code === 'ENOTFOUND' || error.code === 'ERR_NETWORK'
70
+ || msg.includes('Network Error') || msg.includes('ECONNREFUSED');
71
+ if (isNetworkError) {
72
+ console.error('');
73
+ console.error(chalk_1.default.yellow('⚠️ Could not reach the Underlings API'));
74
+ console.error('');
75
+ }
76
+ else {
77
+ console.error(chalk_1.default.red('Failed to fetch variables:'), msg);
78
+ }
79
+ return;
80
+ }
81
+ // Execute command
82
+ const [cmd, ...args] = commandParts;
83
+ console.log(chalk_1.default.dim(`> Executing: ${cmd} ${args.join(' ')}`));
84
+ const child = (0, cross_spawn_1.default)(cmd, args, {
85
+ stdio: 'inherit',
86
+ env: {
87
+ ...process.env,
88
+ ...envVars
89
+ }
90
+ });
91
+ child.on('exit', (code) => {
92
+ process.exit(code || 0);
93
+ });
94
+ };
95
+ exports.runProject = runProject;
96
+ const envProject = async (options) => {
97
+ if (!(0, auth_1.isAuthenticated)()) {
98
+ console.error(chalk_1.default.red('Error: You are not logged in.'));
99
+ process.exit(1);
100
+ }
101
+ const token = (0, auth_1.getAuthToken)();
102
+ // Determine project slug
103
+ let projectSlug = options.project;
104
+ if (!projectSlug) {
105
+ if (!(0, config_1.hasProjectConfig)()) {
106
+ console.error(chalk_1.default.red('Error: No project config found. Use --project <slug> or run from a directory with ulproject.json.'));
107
+ process.exit(1);
108
+ }
109
+ projectSlug = (0, config_1.readProjectConfig)().project;
110
+ }
111
+ // Determine branch
112
+ let branch = options.branch || 'main';
113
+ if (!options.branch) {
114
+ try {
115
+ branch = (0, child_process_1.execSync)('git rev-parse --abbrev-ref HEAD').toString().trim();
116
+ }
117
+ catch {
118
+ // Fall back to 'main' if not in a git repo
119
+ }
120
+ }
121
+ // Fetch vars
122
+ let envVars;
123
+ try {
124
+ envVars = await fetchProjectVariables(projectSlug, branch, token);
125
+ }
126
+ catch (error) {
127
+ console.error(chalk_1.default.red('Failed to fetch variables:'), error.message);
128
+ process.exit(1);
129
+ }
130
+ // Apply filter
131
+ if (options.filter) {
132
+ const prefix = options.filter;
133
+ envVars = Object.fromEntries(Object.entries(envVars).filter(([key]) => key.startsWith(prefix)));
134
+ }
135
+ const format = options.format || 'shell';
136
+ const sorted = Object.entries(envVars).sort(([a], [b]) => a.localeCompare(b));
137
+ switch (format) {
138
+ case 'yaml':
139
+ for (const [key, value] of sorted) {
140
+ const escaped = value.replace(/\\/g, '\\\\').replace(/"/g, '\\"').replace(/\n/g, '\\n');
141
+ console.log(`${key}: "${escaped}"`);
142
+ }
143
+ break;
144
+ case 'docker-build-args':
145
+ for (const [key, value] of sorted) {
146
+ console.log(`--build-arg ${key}=${value}`);
147
+ }
148
+ break;
149
+ case 'dotenv':
150
+ for (const [key, value] of sorted) {
151
+ const escaped = value.replace(/"/g, '\\"');
152
+ console.log(`${key}="${escaped}"`);
153
+ }
154
+ break;
155
+ case 'shell':
156
+ default:
157
+ for (const [key, value] of sorted) {
158
+ console.log(`${chalk_1.default.cyan(key)}=${value}`);
159
+ }
160
+ break;
161
+ }
162
+ };
163
+ exports.envProject = envProject;
164
+ async function fetchProjectVariables(projectSlug, branch, token) {
165
+ const query = `
166
+ query GetProjectVariables($projectSlug: String!, $branch: String!) {
167
+ projectVariables(projectSlug: $projectSlug, branch: $branch, includeSystem: true) {
168
+ key
169
+ valueType
170
+ value
171
+ isSecret
172
+ }
173
+ }
174
+ `;
175
+ const result = await axios_1.default.post((0, constants_1.getApiUrl)(), {
176
+ query,
177
+ variables: { projectSlug, branch }
178
+ }, {
179
+ headers: {
180
+ 'Content-Type': 'application/json',
181
+ 'Authorization': `Bearer ${token}`
182
+ }
183
+ });
184
+ if (result.data.errors) {
185
+ throw new Error(result.data.errors[0].message);
186
+ }
187
+ const variables = result.data.data.projectVariables;
188
+ const envMap = {};
189
+ variables.forEach((v) => {
190
+ envMap[v.key] = v.value;
191
+ });
192
+ return envMap;
193
+ }
@@ -64,7 +64,27 @@ const run = async (commandParts) => {
64
64
  console.log(chalk_1.default.green(`Successfully loaded ${Object.keys(envVars).length} variables for service: ${config.service}`));
65
65
  }
66
66
  catch (error) {
67
- console.error(chalk_1.default.red('Failed to fetch variables:'), error.message);
67
+ const msg = error.message || '';
68
+ const isNetworkError = error.code === 'ECONNREFUSED' || error.code === 'ECONNABORTED'
69
+ || error.code === 'ENOTFOUND' || error.code === 'ERR_NETWORK'
70
+ || msg.includes('Network Error') || msg.includes('ECONNREFUSED');
71
+ if (isNetworkError) {
72
+ console.error('');
73
+ console.error(chalk_1.default.yellow('⚠️ Could not reach the Underlings API'));
74
+ console.error('');
75
+ console.error(chalk_1.default.white(" Don't panic! Here's what to do:"));
76
+ console.error('');
77
+ console.error(chalk_1.default.dim(' 1. Check if the API is running (is it deployed? is your internet on?)'));
78
+ console.error(chalk_1.default.dim(' 2. If you need to work offline, copy your .env file and edit DATABASE_URL manually'));
79
+ console.error(chalk_1.default.dim(` 3. Then run the command directly without underlings run:`));
80
+ console.error(chalk_1.default.dim(` ${chalk_1.default.cyan(commandParts.join(' '))}`));
81
+ console.error('');
82
+ console.error(chalk_1.default.dim(` API URL: ${(0, constants_1.getApiUrl)()}`));
83
+ console.error('');
84
+ }
85
+ else {
86
+ console.error(chalk_1.default.red('Failed to fetch variables:'), msg);
87
+ }
68
88
  return;
69
89
  }
70
90
  // Execute command
@@ -135,17 +155,11 @@ const env = async (options) => {
135
155
  switch (format) {
136
156
  case 'yaml':
137
157
  // YAML format for gcloud --env-vars-file
158
+ // Always quote values to prevent gcloud from interpreting
159
+ // numeric strings as integers (e.g. phone numbers, numeric IDs)
138
160
  for (const [key, value] of sorted) {
139
- // YAML values with special chars need quoting
140
- const needsQuote = /[:{}\[\],&*#?|\-<>=!%@`"'\n\\]/.test(value) || value.includes(' ');
141
- if (needsQuote) {
142
- // Use double-quoted YAML string, escaping backslashes and double quotes
143
- const escaped = value.replace(/\\/g, '\\\\').replace(/"/g, '\\"').replace(/\n/g, '\\n');
144
- console.log(`${key}: "${escaped}"`);
145
- }
146
- else {
147
- console.log(`${key}: ${value}`);
148
- }
161
+ const escaped = value.replace(/\\/g, '\\\\').replace(/"/g, '\\"').replace(/\n/g, '\\n');
162
+ console.log(`${key}: "${escaped}"`);
149
163
  }
150
164
  break;
151
165
  case 'docker-build-args':
@@ -176,8 +190,10 @@ async function fetchServiceVariables(serviceSlug, branch, token) {
176
190
  query GetServiceVariables($serviceSlug: String!, $branch: String!) {
177
191
  serviceVariables(serviceSlug: $serviceSlug, branch: $branch, includeSystem: true) {
178
192
  key
193
+ valueType
179
194
  value
180
195
  isSecret
196
+ sourceServiceSlug
181
197
  }
182
198
  }
183
199
  `;
@@ -195,8 +211,21 @@ async function fetchServiceVariables(serviceSlug, branch, token) {
195
211
  }
196
212
  const variables = result.data.data.serviceVariables;
197
213
  const envMap = {};
214
+ const overrideProxyUrl = process.env.UL_OVERRIDE_PROXY_URL;
198
215
  variables.forEach((v) => {
199
- envMap[v.key] = v.value;
216
+ // Precise granular override for service-to-service mapping (slugs like ul-api become UL_OVERRIDE_SERVICE_UL_API)
217
+ const envKey = v.sourceServiceSlug ? `UL_OVERRIDE_SERVICE_${v.sourceServiceSlug.toUpperCase().replace(/-/g, '_')}` : null;
218
+ const granularOverride = envKey ? process.env[envKey] : null;
219
+ if (v.valueType === 'SERVICE_URL' && granularOverride) {
220
+ envMap[v.key] = granularOverride;
221
+ }
222
+ else if (v.valueType === 'SERVICE_URL' && overrideProxyUrl) {
223
+ // Fallback to legacy global proxy if set
224
+ envMap[v.key] = overrideProxyUrl;
225
+ }
226
+ else {
227
+ envMap[v.key] = v.value;
228
+ }
200
229
  });
201
230
  return envMap;
202
231
  }
package/dist/index.js CHANGED
@@ -75,6 +75,23 @@ project.command('init')
75
75
  const { initProject } = await Promise.resolve().then(() => __importStar(require('./commands/project')));
76
76
  await initProject();
77
77
  });
78
+ project.command('run [command...]')
79
+ .description('Run a command with injected project-level variables')
80
+ .allowUnknownOption()
81
+ .action(async (commandParts) => {
82
+ const { runProject } = await Promise.resolve().then(() => __importStar(require('./commands/project-run')));
83
+ await runProject(commandParts);
84
+ });
85
+ project.command('env')
86
+ .description('Print resolved project-level environment variables (like Neon DATABASE_URL)')
87
+ .option('-p, --project <slug>', 'Project slug (overrides ulproject.json)')
88
+ .option('-b, --branch <name>', 'Branch name (overrides git detection)')
89
+ .option('-f, --format <fmt>', 'Output format: shell, yaml, docker-build-args, dotenv', 'shell')
90
+ .option('--filter <prefix>', 'Only include vars matching prefix (e.g. DATABASE_URL)')
91
+ .action(async (options) => {
92
+ const { envProject } = await Promise.resolve().then(() => __importStar(require('./commands/project-run')));
93
+ await envProject(options);
94
+ });
78
95
  // ─── Service ────────────────────────────────────────────────────────────────
79
96
  const service = program.command('service').description('Service management commands');
80
97
  service.command('init')
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@underlingscloud/cli",
3
- "version": "0.2.1",
3
+ "version": "0.2.3",
4
4
  "description": "Underlings Developer Utilities CLI",
5
5
  "main": "dist/index.js",
6
6
  "bin": {
@@ -37,4 +37,4 @@
37
37
  "directory": "packages/cli"
38
38
  },
39
39
  "license": "MIT"
40
- }
40
+ }