relight-cli 0.2.0 → 0.3.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.
@@ -59,7 +59,7 @@ export async function mintAccessToken(clientEmail, privateKey) {
59
59
  var res = await fetch(TOKEN_URI, {
60
60
  method: "POST",
61
61
  headers: { "Content-Type": "application/x-www-form-urlencoded" },
62
- body: `grant_type=${encodeURIComponent("urn:ietf:params:oauth:grant_type:jwt-bearer")}&assertion=${encodeURIComponent(jwt)}`,
62
+ body: `grant_type=${encodeURIComponent("urn:ietf:params:oauth:grant-type:jwt-bearer")}&assertion=${encodeURIComponent(jwt)}`,
63
63
  });
64
64
 
65
65
  if (!res.ok) {
@@ -302,6 +302,26 @@ export async function updateSqlUser(token, project, instanceName, userName, pass
302
302
  return waitForSqlOperation(token, project, op.name);
303
303
  }
304
304
 
305
+ export async function deleteSqlUser(token, project, instanceName, userName) {
306
+ var op = await gcpApi(
307
+ "DELETE",
308
+ `${SQLADMIN_API}/projects/${project}/instances/${instanceName}/users?name=${encodeURIComponent(userName)}`,
309
+ null,
310
+ token
311
+ );
312
+ return waitForSqlOperation(token, project, op.name);
313
+ }
314
+
315
+ export async function listSqlDatabases(token, project, instanceName) {
316
+ var res = await gcpApi(
317
+ "GET",
318
+ `${SQLADMIN_API}/projects/${project}/instances/${instanceName}/databases`,
319
+ null,
320
+ token
321
+ );
322
+ return res.items || [];
323
+ }
324
+
305
325
  // --- Cloud DNS ---
306
326
 
307
327
  export async function listManagedZones(token, project) {
@@ -0,0 +1,147 @@
1
+ var API_BASE = "https://console.neon.tech/api/v2";
2
+
3
+ export async function neonApi(apiKey, method, path, body) {
4
+ var headers = {
5
+ Authorization: `Bearer ${apiKey}`,
6
+ Accept: "application/json",
7
+ };
8
+
9
+ if (body && typeof body === "object") {
10
+ headers["Content-Type"] = "application/json";
11
+ body = JSON.stringify(body);
12
+ }
13
+
14
+ var res = await fetch(`${API_BASE}${path}`, {
15
+ method,
16
+ headers,
17
+ body: method === "GET" || method === "DELETE" ? undefined : body,
18
+ });
19
+
20
+ if (!res.ok) {
21
+ var text = await res.text();
22
+ throw new Error(`Neon API ${method} ${path}: ${res.status} ${text}`);
23
+ }
24
+
25
+ if (res.status === 204) return null;
26
+ return res.json();
27
+ }
28
+
29
+ // --- Projects ---
30
+
31
+ export async function listProjects(apiKey) {
32
+ var data = await neonApi(apiKey, "GET", "/projects");
33
+ return data.projects || [];
34
+ }
35
+
36
+ export async function createProject(apiKey, opts = {}) {
37
+ var body = {
38
+ project: {
39
+ name: opts.name || "relight",
40
+ pg_version: opts.pgVersion || 16,
41
+ },
42
+ };
43
+ if (opts.regionId) body.project.region_id = opts.regionId;
44
+ return neonApi(apiKey, "POST", "/projects", body);
45
+ }
46
+
47
+ export async function getProject(apiKey, projectId) {
48
+ return neonApi(apiKey, "GET", `/projects/${projectId}`);
49
+ }
50
+
51
+ export async function deleteProject(apiKey, projectId) {
52
+ return neonApi(apiKey, "DELETE", `/projects/${projectId}`);
53
+ }
54
+
55
+ // --- Branches ---
56
+
57
+ export async function listBranches(apiKey, projectId) {
58
+ var data = await neonApi(apiKey, "GET", `/projects/${projectId}/branches`);
59
+ return data.branches || [];
60
+ }
61
+
62
+ // --- Databases ---
63
+
64
+ export async function listDatabases(apiKey, projectId, branchId) {
65
+ var data = await neonApi(
66
+ apiKey,
67
+ "GET",
68
+ `/projects/${projectId}/branches/${branchId}/databases`
69
+ );
70
+ return data.databases || [];
71
+ }
72
+
73
+ export async function createDatabase(apiKey, projectId, branchId, dbName, ownerName) {
74
+ return neonApi(
75
+ apiKey,
76
+ "POST",
77
+ `/projects/${projectId}/branches/${branchId}/databases`,
78
+ { database: { name: dbName, owner_name: ownerName } }
79
+ );
80
+ }
81
+
82
+ export async function deleteDatabase(apiKey, projectId, branchId, dbName) {
83
+ return neonApi(
84
+ apiKey,
85
+ "DELETE",
86
+ `/projects/${projectId}/branches/${branchId}/databases/${dbName}`
87
+ );
88
+ }
89
+
90
+ // --- Roles ---
91
+
92
+ export async function createRole(apiKey, projectId, branchId, roleName) {
93
+ return neonApi(
94
+ apiKey,
95
+ "POST",
96
+ `/projects/${projectId}/branches/${branchId}/roles`,
97
+ { role: { name: roleName } }
98
+ );
99
+ }
100
+
101
+ export async function deleteRole(apiKey, projectId, branchId, roleName) {
102
+ return neonApi(
103
+ apiKey,
104
+ "DELETE",
105
+ `/projects/${projectId}/branches/${branchId}/roles/${roleName}`
106
+ );
107
+ }
108
+
109
+ export async function getRolePassword(apiKey, projectId, branchId, roleName) {
110
+ var data = await neonApi(
111
+ apiKey,
112
+ "GET",
113
+ `/projects/${projectId}/branches/${branchId}/roles/${roleName}/reveal_password`
114
+ );
115
+ return data.password;
116
+ }
117
+
118
+ export async function resetRolePassword(apiKey, projectId, branchId, roleName) {
119
+ var data = await neonApi(
120
+ apiKey,
121
+ "POST",
122
+ `/projects/${projectId}/branches/${branchId}/roles/${roleName}/reset_password`
123
+ );
124
+ return data.password;
125
+ }
126
+
127
+ // --- Connection URI ---
128
+
129
+ export async function getConnectionUri(apiKey, projectId, dbName, roleName) {
130
+ var params = new URLSearchParams({
131
+ database_name: dbName,
132
+ role_name: roleName,
133
+ });
134
+ var data = await neonApi(
135
+ apiKey,
136
+ "GET",
137
+ `/projects/${projectId}/connection_uri?${params}`
138
+ );
139
+ return data.uri;
140
+ }
141
+
142
+ // --- Verification ---
143
+
144
+ export async function verifyApiKey(apiKey) {
145
+ var projects = await listProjects(apiKey);
146
+ return projects;
147
+ }
package/src/lib/config.js CHANGED
@@ -11,23 +11,55 @@ export var CLOUD_NAMES = {
11
11
  cf: "Cloudflare",
12
12
  gcp: "GCP",
13
13
  aws: "AWS",
14
- slicervm: "SlicerVM",
15
14
  };
16
15
 
17
16
  export var CLOUD_IDS = Object.keys(CLOUD_NAMES);
18
17
 
18
+ export var SERVICE_TYPES = {
19
+ slicervm: { layer: "compute", name: "SlicerVM" },
20
+ neon: { layer: "db", name: "Neon" },
21
+ };
22
+
19
23
  export function getConfig() {
20
24
  if (!existsSync(CONFIG_PATH)) {
21
25
  console.error("Not authenticated. Run `relight auth` first.");
22
26
  process.exit(1);
23
27
  }
24
- return JSON.parse(readFileSync(CONFIG_PATH, "utf-8"));
28
+ var config = JSON.parse(readFileSync(CONFIG_PATH, "utf-8"));
29
+ return migrateSlicervm(config);
30
+ }
31
+
32
+ function migrateSlicervm(config) {
33
+ if (config.clouds && config.clouds.slicervm) {
34
+ if (!config.services) config.services = {};
35
+ if (!config.services.slicervm) {
36
+ var old = config.clouds.slicervm;
37
+ config.services.slicervm = {
38
+ layer: "compute",
39
+ type: "slicervm",
40
+ ...old,
41
+ };
42
+ }
43
+ delete config.clouds.slicervm;
44
+ if (config.default_cloud === "slicervm") {
45
+ delete config.default_cloud;
46
+ }
47
+ saveConfig(config);
48
+ }
49
+ // Migrate old "addons" key to "services"
50
+ if (config.addons && !config.services) {
51
+ config.services = config.addons;
52
+ delete config.addons;
53
+ saveConfig(config);
54
+ }
55
+ return config;
25
56
  }
26
57
 
27
58
  export function tryGetConfig() {
28
59
  if (!existsSync(CONFIG_PATH)) return null;
29
60
  try {
30
- return JSON.parse(readFileSync(CONFIG_PATH, "utf-8"));
61
+ var config = JSON.parse(readFileSync(CONFIG_PATH, "utf-8"));
62
+ return migrateSlicervm(config);
31
63
  } catch {
32
64
  return null;
33
65
  }
@@ -84,15 +116,141 @@ export function resolveCloudConfig(cloudId) {
84
116
  if (cloudId === "aws") {
85
117
  return { accessKeyId: cloud.accessKeyId, secretAccessKey: cloud.secretAccessKey, region: cloud.region };
86
118
  }
87
- if (cloudId === "slicervm") {
88
- var slicerCfg = { hostGroup: cloud.hostGroup, baseDomain: cloud.baseDomain };
89
- if (cloud.socketPath) {
90
- slicerCfg.socketPath = cloud.socketPath;
119
+ return cloud;
120
+ }
121
+
122
+ export function getServiceConfig(name) {
123
+ var config = getConfig();
124
+ var service = config.services && config.services[name];
125
+ if (!service) {
126
+ console.error(
127
+ `Service '${name}' not found. Run \`relight service add\` to register it.`
128
+ );
129
+ process.exit(1);
130
+ }
131
+ return service;
132
+ }
133
+
134
+ export function tryGetServiceConfig(name) {
135
+ var config = tryGetConfig();
136
+ if (!config || !config.services) return null;
137
+ return config.services[name] || null;
138
+ }
139
+
140
+ export function getRegisteredServices() {
141
+ var config = tryGetConfig();
142
+ if (!config || !config.services) return [];
143
+ return Object.entries(config.services).map(([name, service]) => ({
144
+ name,
145
+ ...service,
146
+ }));
147
+ }
148
+
149
+ export function saveServiceConfig(name, data) {
150
+ var config = tryGetConfig() || { clouds: {} };
151
+ if (!config.services) config.services = {};
152
+ config.services[name] = data;
153
+ saveConfig(config);
154
+ }
155
+
156
+ export function removeServiceConfig(name) {
157
+ var config = tryGetConfig();
158
+ if (!config || !config.services) return;
159
+ delete config.services[name];
160
+ saveConfig(config);
161
+ }
162
+
163
+ export function getCloudMeta(cloudId, key) {
164
+ var config = tryGetConfig();
165
+ if (!config || !config.clouds || !config.clouds[cloudId]) return undefined;
166
+ var meta = config.clouds[cloudId]._meta;
167
+ if (!meta) return undefined;
168
+ return key ? meta[key] : meta;
169
+ }
170
+
171
+ export function setCloudMeta(cloudId, key, value) {
172
+ var config = getConfig();
173
+ if (!config.clouds[cloudId]._meta) config.clouds[cloudId]._meta = {};
174
+ if (value === undefined) {
175
+ delete config.clouds[cloudId]._meta[key];
176
+ if (Object.keys(config.clouds[cloudId]._meta).length === 0) {
177
+ delete config.clouds[cloudId]._meta;
178
+ }
179
+ } else {
180
+ config.clouds[cloudId]._meta[key] = value;
181
+ }
182
+ saveConfig(config);
183
+ }
184
+
185
+ export function getServiceMeta(serviceName, key) {
186
+ var config = tryGetConfig();
187
+ if (!config || !config.services || !config.services[serviceName]) return undefined;
188
+ var meta = config.services[serviceName]._meta;
189
+ if (!meta) return undefined;
190
+ return key ? meta[key] : meta;
191
+ }
192
+
193
+ export function setServiceMeta(serviceName, key, value) {
194
+ var config = getConfig();
195
+ if (!config.services || !config.services[serviceName]) {
196
+ throw new Error(`Service '${serviceName}' not found.`);
197
+ }
198
+ if (!config.services[serviceName]._meta) config.services[serviceName]._meta = {};
199
+ if (value === undefined) {
200
+ delete config.services[serviceName]._meta[key];
201
+ if (Object.keys(config.services[serviceName]._meta).length === 0) {
202
+ delete config.services[serviceName]._meta;
203
+ }
204
+ } else {
205
+ config.services[serviceName]._meta[key] = value;
206
+ }
207
+ saveConfig(config);
208
+ }
209
+
210
+ // --- Database registry ---
211
+
212
+ export function getDatabaseConfig(name) {
213
+ var config = tryGetConfig();
214
+ if (!config || !config.databases) return null;
215
+ return config.databases[name] || null;
216
+ }
217
+
218
+ export function saveDatabaseConfig(name, data) {
219
+ var config = tryGetConfig() || { clouds: {} };
220
+ if (!config.databases) config.databases = {};
221
+ config.databases[name] = data;
222
+ saveConfig(config);
223
+ }
224
+
225
+ export function removeDatabaseConfig(name) {
226
+ var config = tryGetConfig();
227
+ if (!config || !config.databases) return;
228
+ delete config.databases[name];
229
+ saveConfig(config);
230
+ }
231
+
232
+ export function listDatabases() {
233
+ var config = tryGetConfig();
234
+ if (!config || !config.databases) return [];
235
+ return Object.entries(config.databases).map(([name, data]) => ({
236
+ name,
237
+ ...data,
238
+ }));
239
+ }
240
+
241
+ export function normalizeServiceConfig(service) {
242
+ if (service.type === "slicervm") {
243
+ var cfg = { hostGroup: service.hostGroup, baseDomain: service.baseDomain };
244
+ if (service.socketPath) {
245
+ cfg.socketPath = service.socketPath;
91
246
  } else {
92
- slicerCfg.apiUrl = cloud.apiUrl;
93
- slicerCfg.apiToken = cloud.token;
247
+ cfg.apiUrl = service.apiUrl;
248
+ cfg.apiToken = service.token;
94
249
  }
95
- return slicerCfg;
250
+ return cfg;
96
251
  }
97
- return cloud;
252
+ if (service.type === "neon") {
253
+ return { apiKey: service.apiKey };
254
+ }
255
+ return service;
98
256
  }
package/src/lib/link.js CHANGED
@@ -14,8 +14,14 @@ export function readLink() {
14
14
  }
15
15
  }
16
16
 
17
- export function linkApp(name, cloud, dns, db) {
18
- var data = { app: name, cloud };
17
+ export function linkApp(name, cloud, dns, db, compute) {
18
+ var data = { app: name };
19
+ if (compute) {
20
+ data.compute = compute;
21
+ }
22
+ if (cloud) {
23
+ data.cloud = cloud;
24
+ }
19
25
  if (dns && dns !== cloud) data.dns = dns;
20
26
  if (db && db !== cloud) data.db = db;
21
27
  writeFileSync(LINK_FILE, YAML.stringify(data));
@@ -53,3 +59,8 @@ export function resolveDb() {
53
59
  var linked = readLink();
54
60
  return linked?.db || null;
55
61
  }
62
+
63
+ export function resolveCompute() {
64
+ var linked = readLink();
65
+ return linked?.compute || null;
66
+ }