@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.
- package/dist/commands/project-run.d.ts +8 -0
- package/dist/commands/project-run.js +193 -0
- package/dist/commands/run.js +41 -12
- package/dist/index.js +17 -0
- package/package.json +2 -2
|
@@ -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
|
+
}
|
package/dist/commands/run.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
140
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
+
}
|