@underlingscloud/cli 0.2.2 → 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
@@ -170,8 +190,10 @@ async function fetchServiceVariables(serviceSlug, branch, token) {
170
190
  query GetServiceVariables($serviceSlug: String!, $branch: String!) {
171
191
  serviceVariables(serviceSlug: $serviceSlug, branch: $branch, includeSystem: true) {
172
192
  key
193
+ valueType
173
194
  value
174
195
  isSecret
196
+ sourceServiceSlug
175
197
  }
176
198
  }
177
199
  `;
@@ -189,8 +211,21 @@ async function fetchServiceVariables(serviceSlug, branch, token) {
189
211
  }
190
212
  const variables = result.data.data.serviceVariables;
191
213
  const envMap = {};
214
+ const overrideProxyUrl = process.env.UL_OVERRIDE_PROXY_URL;
192
215
  variables.forEach((v) => {
193
- 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
+ }
194
229
  });
195
230
  return envMap;
196
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.2",
3
+ "version": "0.2.3",
4
4
  "description": "Underlings Developer Utilities CLI",
5
5
  "main": "dist/index.js",
6
6
  "bin": {