@spacelr/cli 0.1.4 → 0.1.6
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/index.js +615 -161
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -24,7 +24,7 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
|
|
|
24
24
|
));
|
|
25
25
|
|
|
26
26
|
// libs/cli/src/index.ts
|
|
27
|
-
var
|
|
27
|
+
var import_commander14 = require("commander");
|
|
28
28
|
|
|
29
29
|
// libs/cli/src/lib/output.ts
|
|
30
30
|
var import_ora = __toESM(require("ora"));
|
|
@@ -106,47 +106,245 @@ var import_commander = require("commander");
|
|
|
106
106
|
var import_open = __toESM(require("open"));
|
|
107
107
|
|
|
108
108
|
// libs/cli/src/lib/auth.ts
|
|
109
|
+
var fs2 = __toESM(require("fs"));
|
|
110
|
+
var path2 = __toESM(require("path"));
|
|
111
|
+
var os = __toESM(require("os"));
|
|
112
|
+
var crypto = __toESM(require("crypto"));
|
|
113
|
+
|
|
114
|
+
// libs/cli/src/lib/config.ts
|
|
109
115
|
var fs = __toESM(require("fs"));
|
|
110
116
|
var path = __toESM(require("path"));
|
|
111
|
-
var
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
117
|
+
var CONFIG_FILENAME = "spacelr.json";
|
|
118
|
+
var RULES_FILENAME = "spacelr.rules.json";
|
|
119
|
+
var INDEXES_FILENAME = "spacelr.indexes.json";
|
|
120
|
+
function readJsonFile(filePath, label) {
|
|
121
|
+
const content = fs.readFileSync(filePath, "utf-8");
|
|
122
|
+
try {
|
|
123
|
+
return JSON.parse(content);
|
|
124
|
+
} catch (err) {
|
|
125
|
+
const detail = err instanceof Error ? `: ${err.message}` : "";
|
|
126
|
+
throw new Error(
|
|
127
|
+
`Invalid JSON in ${label}: ${filePath}${detail}
|
|
128
|
+
Please check the file for syntax errors.`
|
|
129
|
+
);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
function writeJsonFile(filePath, data) {
|
|
133
|
+
fs.writeFileSync(filePath, JSON.stringify(data, null, 2) + "\n", "utf-8");
|
|
134
|
+
}
|
|
135
|
+
function resolveSiblingPath(filename, configPath) {
|
|
136
|
+
const resolved = configPath ?? findConfigPath();
|
|
137
|
+
const dir = resolved ? path.dirname(resolved) : process.cwd();
|
|
138
|
+
return path.join(dir, filename);
|
|
139
|
+
}
|
|
140
|
+
function findConfigPath(startDir) {
|
|
141
|
+
let dir = startDir ?? process.cwd();
|
|
142
|
+
while (true) {
|
|
143
|
+
const candidate = path.join(dir, CONFIG_FILENAME);
|
|
144
|
+
if (fs.existsSync(candidate)) {
|
|
145
|
+
return candidate;
|
|
146
|
+
}
|
|
147
|
+
const parent = path.dirname(dir);
|
|
148
|
+
if (parent === dir) {
|
|
149
|
+
return null;
|
|
150
|
+
}
|
|
151
|
+
dir = parent;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
function loadConfig(configPath) {
|
|
155
|
+
const resolved = configPath ?? findConfigPath();
|
|
156
|
+
if (!resolved || !fs.existsSync(resolved)) return null;
|
|
157
|
+
return readJsonFile(resolved, "config file");
|
|
158
|
+
}
|
|
159
|
+
function saveConfig(config, configPath) {
|
|
160
|
+
const resolved = configPath ?? findConfigPath() ?? path.join(process.cwd(), CONFIG_FILENAME);
|
|
161
|
+
writeJsonFile(resolved, config);
|
|
162
|
+
}
|
|
163
|
+
function resolveProjectId(flagValue, config) {
|
|
164
|
+
const projectId = flagValue ?? config?.projectId;
|
|
165
|
+
if (!projectId) {
|
|
166
|
+
throw new Error(
|
|
167
|
+
'Project ID is required. Provide --project <id> or set "projectId" in spacelr.json'
|
|
168
|
+
);
|
|
169
|
+
}
|
|
170
|
+
return projectId;
|
|
171
|
+
}
|
|
172
|
+
function resolveApiUrl(flagValue, config) {
|
|
173
|
+
return flagValue ?? config?.apiUrl ?? process.env["SPACELR_API_URL"] ?? "https://api.spacelr.com/api/v1";
|
|
174
|
+
}
|
|
175
|
+
function loadRules(configPath) {
|
|
176
|
+
const filePath = resolveSiblingPath(RULES_FILENAME, configPath);
|
|
177
|
+
if (!fs.existsSync(filePath)) return null;
|
|
178
|
+
return readJsonFile(filePath, "rules file");
|
|
179
|
+
}
|
|
180
|
+
function saveRules(rules, configPath) {
|
|
181
|
+
writeJsonFile(resolveSiblingPath(RULES_FILENAME, configPath), rules);
|
|
182
|
+
}
|
|
183
|
+
function loadIndexes(configPath) {
|
|
184
|
+
const filePath = resolveSiblingPath(INDEXES_FILENAME, configPath);
|
|
185
|
+
if (!fs.existsSync(filePath)) return null;
|
|
186
|
+
return readJsonFile(filePath, "indexes file");
|
|
187
|
+
}
|
|
188
|
+
function saveIndexes(indexes, configPath) {
|
|
189
|
+
writeJsonFile(resolveSiblingPath(INDEXES_FILENAME, configPath), indexes);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// libs/cli/src/lib/auth.ts
|
|
193
|
+
function getGlobalCredentialsDir() {
|
|
194
|
+
return path2.join(
|
|
195
|
+
process.env["HOME"] ?? process.env["USERPROFILE"] ?? os.homedir(),
|
|
115
196
|
".spacelr"
|
|
116
197
|
);
|
|
117
198
|
}
|
|
118
|
-
function
|
|
119
|
-
return
|
|
199
|
+
function getGlobalCredentialsFile() {
|
|
200
|
+
return path2.join(getGlobalCredentialsDir(), "credentials.json");
|
|
201
|
+
}
|
|
202
|
+
function resolveCredentialsFile(forceGlobal = false) {
|
|
203
|
+
if (forceGlobal) {
|
|
204
|
+
return { path: getGlobalCredentialsFile(), scope: "global" };
|
|
205
|
+
}
|
|
206
|
+
const configPath = findConfigPath();
|
|
207
|
+
if (configPath) {
|
|
208
|
+
const projectDir = path2.dirname(configPath);
|
|
209
|
+
return {
|
|
210
|
+
path: path2.join(projectDir, ".spacelr", "credentials.json"),
|
|
211
|
+
scope: "project",
|
|
212
|
+
projectDir
|
|
213
|
+
};
|
|
214
|
+
}
|
|
215
|
+
return { path: getGlobalCredentialsFile(), scope: "global" };
|
|
120
216
|
}
|
|
121
|
-
function
|
|
217
|
+
function ensureGitignored(projectDir) {
|
|
218
|
+
const gitignorePath = path2.join(projectDir, ".gitignore");
|
|
219
|
+
const entry = ".spacelr/";
|
|
220
|
+
const matchesExisting = (line) => {
|
|
221
|
+
const trimmed = line.trim();
|
|
222
|
+
if (!trimmed || trimmed.startsWith("#")) return false;
|
|
223
|
+
return trimmed === ".spacelr" || trimmed === ".spacelr/" || trimmed === ".spacelr/*" || trimmed === ".spacelr/**" || trimmed === ".spacelr/credentials.json" || // Leading-slash forms anchor the pattern to the repo root
|
|
224
|
+
trimmed === "/.spacelr" || trimmed === "/.spacelr/" || trimmed === "/.spacelr/*" || trimmed === "/.spacelr/**" || trimmed === "/.spacelr/credentials.json";
|
|
225
|
+
};
|
|
226
|
+
const hasNegation = (line) => {
|
|
227
|
+
const trimmed = line.trim();
|
|
228
|
+
if (!trimmed || trimmed.startsWith("#")) return false;
|
|
229
|
+
return trimmed.startsWith("!") && trimmed.includes(".spacelr");
|
|
230
|
+
};
|
|
231
|
+
let content = "";
|
|
232
|
+
let existed = false;
|
|
122
233
|
try {
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
234
|
+
content = fs2.readFileSync(gitignorePath, "utf-8");
|
|
235
|
+
existed = true;
|
|
236
|
+
} catch (err) {
|
|
237
|
+
if (err.code !== "ENOENT") return { safe: true };
|
|
238
|
+
}
|
|
239
|
+
const lines = content.split(/\r?\n/);
|
|
240
|
+
if (lines.some(hasNegation)) {
|
|
241
|
+
const alreadyHasMatch = lines.some(matchesExisting);
|
|
242
|
+
warn(
|
|
243
|
+
alreadyHasMatch ? ".gitignore already contains .spacelr/ but ALSO contains a negation pattern that un-ignores credentials. Remove the negation or store credentials globally with `spacelr login --global`." : ".gitignore contains a negation pattern for .spacelr/ \u2014 refusing to write credentials safety entry. Remove the negation or store credentials globally with `spacelr login --global`."
|
|
244
|
+
);
|
|
245
|
+
return { safe: false };
|
|
246
|
+
}
|
|
247
|
+
if (lines.some(matchesExisting)) return { safe: true };
|
|
248
|
+
const needsLeadingNewline = existed && content.length > 0 && !content.endsWith("\n");
|
|
249
|
+
const newContent = (existed ? content : "") + (needsLeadingNewline ? "\n" : "") + entry + "\n";
|
|
250
|
+
try {
|
|
251
|
+
writeFileAtomic(gitignorePath, newContent, 420);
|
|
252
|
+
info(`Added ${entry} to .gitignore (credentials must never be committed)`);
|
|
253
|
+
return { safe: true };
|
|
127
254
|
} catch {
|
|
128
|
-
return
|
|
255
|
+
return { safe: true };
|
|
129
256
|
}
|
|
130
257
|
}
|
|
131
|
-
function
|
|
132
|
-
const
|
|
133
|
-
|
|
134
|
-
|
|
258
|
+
function getStoredCredentialsWithLocation(forceGlobal = false) {
|
|
259
|
+
const primary = resolveCredentialsFile(forceGlobal);
|
|
260
|
+
const fromPrimary = readCredentialsFromDisk(primary.path);
|
|
261
|
+
if (fromPrimary) return { credentials: fromPrimary, location: primary };
|
|
262
|
+
if (!forceGlobal && primary.scope === "project") {
|
|
263
|
+
const globalPath = getGlobalCredentialsFile();
|
|
264
|
+
const fromGlobal = readCredentialsFromDisk(globalPath);
|
|
265
|
+
if (fromGlobal) {
|
|
266
|
+
return {
|
|
267
|
+
credentials: fromGlobal,
|
|
268
|
+
location: { path: globalPath, scope: "global" }
|
|
269
|
+
};
|
|
270
|
+
}
|
|
135
271
|
}
|
|
136
|
-
|
|
137
|
-
getCredentialsFile(),
|
|
138
|
-
JSON.stringify(credentials, null, 2),
|
|
139
|
-
{ mode: 384 }
|
|
140
|
-
);
|
|
272
|
+
return null;
|
|
141
273
|
}
|
|
142
|
-
function
|
|
274
|
+
function readCredentialsFromDisk(filePath) {
|
|
143
275
|
try {
|
|
144
|
-
const
|
|
145
|
-
|
|
146
|
-
|
|
276
|
+
const content = fs2.readFileSync(filePath, "utf-8");
|
|
277
|
+
const parsed = JSON.parse(content);
|
|
278
|
+
if (typeof parsed !== "object" || parsed === null) return null;
|
|
279
|
+
const obj = parsed;
|
|
280
|
+
if (typeof obj["accessToken"] !== "string" || obj["accessToken"].length === 0 || !Number.isFinite(obj["expiresAt"]) || typeof obj["apiUrl"] !== "string" || obj["apiUrl"].length === 0) {
|
|
281
|
+
return null;
|
|
282
|
+
}
|
|
283
|
+
if ("refreshToken" in obj && typeof obj["refreshToken"] !== "string") {
|
|
284
|
+
return null;
|
|
285
|
+
}
|
|
286
|
+
return parsed;
|
|
287
|
+
} catch {
|
|
288
|
+
return null;
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
function writeFileAtomic(filePath, data, mode) {
|
|
292
|
+
const tmpPath = `${filePath}.tmp.${process.pid}.${crypto.randomBytes(6).toString("hex")}`;
|
|
293
|
+
try {
|
|
294
|
+
fs2.writeFileSync(tmpPath, data, { mode });
|
|
295
|
+
fs2.renameSync(tmpPath, filePath);
|
|
296
|
+
} catch (err) {
|
|
297
|
+
try {
|
|
298
|
+
fs2.unlinkSync(tmpPath);
|
|
299
|
+
} catch {
|
|
300
|
+
}
|
|
301
|
+
throw err;
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
function storeCredentials(credentials, target = false) {
|
|
305
|
+
const location = typeof target === "boolean" ? resolveCredentialsFile(target) : target;
|
|
306
|
+
if (location.scope === "project" && location.projectDir) {
|
|
307
|
+
const { safe } = ensureGitignored(location.projectDir);
|
|
308
|
+
if (!safe) {
|
|
309
|
+
throw new Error(
|
|
310
|
+
"Refusing to write project-local credentials: .gitignore contains a negation pattern that would un-ignore .spacelr/. Run `spacelr login --global` to store credentials in ~/.spacelr/."
|
|
311
|
+
);
|
|
147
312
|
}
|
|
313
|
+
}
|
|
314
|
+
const dir = path2.dirname(location.path);
|
|
315
|
+
fs2.mkdirSync(dir, { recursive: true, mode: 448 });
|
|
316
|
+
try {
|
|
317
|
+
fs2.chmodSync(dir, 448);
|
|
148
318
|
} catch {
|
|
149
319
|
}
|
|
320
|
+
writeFileAtomic(location.path, JSON.stringify(credentials, null, 2), 384);
|
|
321
|
+
return location;
|
|
322
|
+
}
|
|
323
|
+
function clearCredentials(scope = "auto") {
|
|
324
|
+
const cleared = [];
|
|
325
|
+
const tryDelete = (loc) => {
|
|
326
|
+
try {
|
|
327
|
+
fs2.unlinkSync(loc.path);
|
|
328
|
+
cleared.push(loc);
|
|
329
|
+
} catch (err) {
|
|
330
|
+
const code = err.code;
|
|
331
|
+
if (code !== "ENOENT") {
|
|
332
|
+
warn(
|
|
333
|
+
`Failed to remove ${loc.path} (${code ?? "unknown error"}) \u2014 file may still be on disk`
|
|
334
|
+
);
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
};
|
|
338
|
+
if (scope === "all") {
|
|
339
|
+
const primary = resolveCredentialsFile(false);
|
|
340
|
+
if (primary.scope === "project") tryDelete(primary);
|
|
341
|
+
tryDelete(
|
|
342
|
+
primary.scope === "global" ? primary : resolveCredentialsFile(true)
|
|
343
|
+
);
|
|
344
|
+
} else {
|
|
345
|
+
tryDelete(resolveCredentialsFile(scope === "global"));
|
|
346
|
+
}
|
|
347
|
+
return { cleared };
|
|
150
348
|
}
|
|
151
349
|
function isTokenExpired(credentials) {
|
|
152
350
|
return Date.now() >= credentials.expiresAt - 6e4;
|
|
@@ -154,17 +352,29 @@ function isTokenExpired(credentials) {
|
|
|
154
352
|
async function resolveToken(flagToken) {
|
|
155
353
|
if (flagToken) return flagToken;
|
|
156
354
|
if (process.env["SPACELR_TOKEN"]) return process.env["SPACELR_TOKEN"];
|
|
157
|
-
const
|
|
158
|
-
if (!
|
|
355
|
+
const loaded = getStoredCredentialsWithLocation();
|
|
356
|
+
if (!loaded) return null;
|
|
357
|
+
const { credentials, location } = loaded;
|
|
159
358
|
if (!isTokenExpired(credentials)) return credentials.accessToken;
|
|
160
|
-
if (credentials.refreshToken
|
|
161
|
-
const newToken = await refreshStoredToken(credentials);
|
|
359
|
+
if (credentials.refreshToken) {
|
|
360
|
+
const newToken = await refreshStoredToken(credentials, location);
|
|
162
361
|
if (newToken) return newToken;
|
|
163
362
|
}
|
|
164
363
|
return null;
|
|
165
364
|
}
|
|
166
365
|
var REFRESH_TIMEOUT_MS = 1e4;
|
|
167
|
-
async function refreshStoredToken(credentials) {
|
|
366
|
+
async function refreshStoredToken(credentials, sourceLocation) {
|
|
367
|
+
if (!credentials.refreshToken) return null;
|
|
368
|
+
try {
|
|
369
|
+
const parsed = new URL(credentials.apiUrl);
|
|
370
|
+
if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
|
|
371
|
+
verbose(`Refusing to refresh against non-http(s) URL: ${parsed.protocol}`);
|
|
372
|
+
return null;
|
|
373
|
+
}
|
|
374
|
+
} catch {
|
|
375
|
+
verbose("Refusing to refresh: stored apiUrl is not a valid URL");
|
|
376
|
+
return null;
|
|
377
|
+
}
|
|
168
378
|
const apiUrl = credentials.apiUrl.replace(/\/+$/, "");
|
|
169
379
|
const controller = new AbortController();
|
|
170
380
|
const timer = setTimeout(() => controller.abort(), REFRESH_TIMEOUT_MS);
|
|
@@ -173,21 +383,34 @@ async function refreshStoredToken(credentials) {
|
|
|
173
383
|
const response = await fetch(`${apiUrl}/auth/refresh`, {
|
|
174
384
|
method: "POST",
|
|
175
385
|
headers: { "Content-Type": "application/json" },
|
|
176
|
-
body: JSON.stringify({ refreshToken: credentials.refreshToken
|
|
386
|
+
body: JSON.stringify({ refreshToken: credentials.refreshToken }),
|
|
177
387
|
signal: controller.signal
|
|
178
388
|
});
|
|
179
389
|
if (!response.ok) {
|
|
180
390
|
verbose(`Token refresh failed (HTTP ${response.status})`);
|
|
391
|
+
if (response.status === 401) {
|
|
392
|
+
const fresh = readCredentialsFromDisk(sourceLocation.path);
|
|
393
|
+
if (fresh && (fresh.accessToken !== credentials.accessToken || fresh.refreshToken !== credentials.refreshToken) && !isTokenExpired(fresh)) {
|
|
394
|
+
verbose("Using credentials refreshed by concurrent process");
|
|
395
|
+
return fresh.accessToken;
|
|
396
|
+
}
|
|
397
|
+
}
|
|
181
398
|
return null;
|
|
182
399
|
}
|
|
183
400
|
const data = await response.json();
|
|
401
|
+
if (typeof data.access_token !== "string") {
|
|
402
|
+
verbose("Token refresh returned unexpected response shape");
|
|
403
|
+
return null;
|
|
404
|
+
}
|
|
405
|
+
const expiresInRaw = data.expires_in;
|
|
406
|
+
const expiresIn = typeof expiresInRaw === "number" && Number.isFinite(expiresInRaw) && expiresInRaw > 0 ? expiresInRaw : 3600;
|
|
184
407
|
const newCredentials = {
|
|
185
408
|
accessToken: data.access_token,
|
|
186
409
|
refreshToken: data.refresh_token ?? credentials.refreshToken,
|
|
187
|
-
expiresAt: Date.now() +
|
|
410
|
+
expiresAt: Date.now() + expiresIn * 1e3,
|
|
188
411
|
apiUrl: credentials.apiUrl
|
|
189
412
|
};
|
|
190
|
-
storeCredentials(newCredentials);
|
|
413
|
+
storeCredentials(newCredentials, sourceLocation);
|
|
191
414
|
verbose("Token refreshed successfully");
|
|
192
415
|
return data.access_token;
|
|
193
416
|
} catch (err) {
|
|
@@ -220,100 +443,35 @@ function generatePKCE() {
|
|
|
220
443
|
return { codeVerifier, codeChallenge };
|
|
221
444
|
}
|
|
222
445
|
|
|
223
|
-
// libs/cli/src/lib/config.ts
|
|
224
|
-
var fs2 = __toESM(require("fs"));
|
|
225
|
-
var path2 = __toESM(require("path"));
|
|
226
|
-
var CONFIG_FILENAME = "spacelr.json";
|
|
227
|
-
var RULES_FILENAME = "spacelr.rules.json";
|
|
228
|
-
var INDEXES_FILENAME = "spacelr.indexes.json";
|
|
229
|
-
function readJsonFile(filePath, label) {
|
|
230
|
-
const content = fs2.readFileSync(filePath, "utf-8");
|
|
231
|
-
try {
|
|
232
|
-
return JSON.parse(content);
|
|
233
|
-
} catch (err) {
|
|
234
|
-
const detail = err instanceof Error ? `: ${err.message}` : "";
|
|
235
|
-
throw new Error(
|
|
236
|
-
`Invalid JSON in ${label}: ${filePath}${detail}
|
|
237
|
-
Please check the file for syntax errors.`
|
|
238
|
-
);
|
|
239
|
-
}
|
|
240
|
-
}
|
|
241
|
-
function writeJsonFile(filePath, data) {
|
|
242
|
-
fs2.writeFileSync(filePath, JSON.stringify(data, null, 2) + "\n", "utf-8");
|
|
243
|
-
}
|
|
244
|
-
function resolveSiblingPath(filename, configPath) {
|
|
245
|
-
const resolved = configPath ?? findConfigPath();
|
|
246
|
-
const dir = resolved ? path2.dirname(resolved) : process.cwd();
|
|
247
|
-
return path2.join(dir, filename);
|
|
248
|
-
}
|
|
249
|
-
function findConfigPath(startDir) {
|
|
250
|
-
let dir = startDir ?? process.cwd();
|
|
251
|
-
while (true) {
|
|
252
|
-
const candidate = path2.join(dir, CONFIG_FILENAME);
|
|
253
|
-
if (fs2.existsSync(candidate)) {
|
|
254
|
-
return candidate;
|
|
255
|
-
}
|
|
256
|
-
const parent = path2.dirname(dir);
|
|
257
|
-
if (parent === dir) {
|
|
258
|
-
return null;
|
|
259
|
-
}
|
|
260
|
-
dir = parent;
|
|
261
|
-
}
|
|
262
|
-
}
|
|
263
|
-
function loadConfig(configPath) {
|
|
264
|
-
const resolved = configPath ?? findConfigPath();
|
|
265
|
-
if (!resolved || !fs2.existsSync(resolved)) return null;
|
|
266
|
-
return readJsonFile(resolved, "config file");
|
|
267
|
-
}
|
|
268
|
-
function saveConfig(config, configPath) {
|
|
269
|
-
const resolved = configPath ?? findConfigPath() ?? path2.join(process.cwd(), CONFIG_FILENAME);
|
|
270
|
-
writeJsonFile(resolved, config);
|
|
271
|
-
}
|
|
272
|
-
function resolveProjectId(flagValue, config) {
|
|
273
|
-
const projectId = flagValue ?? config?.projectId;
|
|
274
|
-
if (!projectId) {
|
|
275
|
-
throw new Error(
|
|
276
|
-
'Project ID is required. Provide --project <id> or set "projectId" in spacelr.json'
|
|
277
|
-
);
|
|
278
|
-
}
|
|
279
|
-
return projectId;
|
|
280
|
-
}
|
|
281
|
-
function resolveApiUrl(flagValue, config) {
|
|
282
|
-
return flagValue ?? config?.apiUrl ?? process.env["SPACELR_API_URL"] ?? "https://api.spacelr.io";
|
|
283
|
-
}
|
|
284
|
-
function loadRules(configPath) {
|
|
285
|
-
const filePath = resolveSiblingPath(RULES_FILENAME, configPath);
|
|
286
|
-
if (!fs2.existsSync(filePath)) return null;
|
|
287
|
-
return readJsonFile(filePath, "rules file");
|
|
288
|
-
}
|
|
289
|
-
function saveRules(rules, configPath) {
|
|
290
|
-
writeJsonFile(resolveSiblingPath(RULES_FILENAME, configPath), rules);
|
|
291
|
-
}
|
|
292
|
-
function loadIndexes(configPath) {
|
|
293
|
-
const filePath = resolveSiblingPath(INDEXES_FILENAME, configPath);
|
|
294
|
-
if (!fs2.existsSync(filePath)) return null;
|
|
295
|
-
return readJsonFile(filePath, "indexes file");
|
|
296
|
-
}
|
|
297
|
-
function saveIndexes(indexes, configPath) {
|
|
298
|
-
writeJsonFile(resolveSiblingPath(INDEXES_FILENAME, configPath), indexes);
|
|
299
|
-
}
|
|
300
|
-
|
|
301
446
|
// libs/cli/src/commands/login.ts
|
|
302
447
|
var CLI_CLIENT_ID = "spacelr-cli";
|
|
303
448
|
function createLoginCommand() {
|
|
304
|
-
return new import_commander.Command("login").description("Authenticate with Spacelr via browser").option("--auth-url <url>", "OAuth server URL (defaults to --api-url)").
|
|
449
|
+
return new import_commander.Command("login").description("Authenticate with Spacelr via browser").option("--auth-url <url>", "OAuth server URL (defaults to --api-url)").option(
|
|
450
|
+
"--global",
|
|
451
|
+
"Store credentials globally (~/.spacelr/credentials.json) instead of in the current project"
|
|
452
|
+
).action(async (opts, cmd) => {
|
|
305
453
|
const globalOpts = cmd.optsWithGlobals();
|
|
306
454
|
const config = loadConfig();
|
|
307
455
|
const apiUrl = resolveApiUrl(globalOpts.apiUrl, config);
|
|
308
456
|
const authUrl = opts.authUrl ?? apiUrl;
|
|
457
|
+
const forceGlobal = opts.global === true;
|
|
309
458
|
try {
|
|
310
459
|
const credentials = await performLogin(authUrl, apiUrl);
|
|
311
|
-
storeCredentials(credentials);
|
|
460
|
+
const location = storeCredentials(credentials, forceGlobal);
|
|
312
461
|
if (isJsonMode()) {
|
|
313
|
-
json({
|
|
462
|
+
json({
|
|
463
|
+
success: true,
|
|
464
|
+
expiresAt: credentials.expiresAt,
|
|
465
|
+
credentialsPath: location.path,
|
|
466
|
+
scope: location.scope
|
|
467
|
+
});
|
|
314
468
|
} else {
|
|
315
469
|
success("Logged in successfully");
|
|
316
|
-
|
|
470
|
+
if (location.scope === "project") {
|
|
471
|
+
info(`Credentials stored in ${location.path} (project-local)`);
|
|
472
|
+
} else {
|
|
473
|
+
info(`Credentials stored in ${location.path} (global)`);
|
|
474
|
+
}
|
|
317
475
|
}
|
|
318
476
|
} catch (err) {
|
|
319
477
|
error(err instanceof Error ? err.message : "Login failed");
|
|
@@ -340,10 +498,12 @@ async function performLogin(authUrl, apiUrl) {
|
|
|
340
498
|
port
|
|
341
499
|
);
|
|
342
500
|
spin.succeed("Authentication complete");
|
|
501
|
+
const expiresInRaw = tokenResponse.expires_in;
|
|
502
|
+
const expiresIn = typeof expiresInRaw === "number" && Number.isFinite(expiresInRaw) && expiresInRaw > 0 ? expiresInRaw : 3600;
|
|
343
503
|
return {
|
|
344
504
|
accessToken: tokenResponse.access_token,
|
|
345
505
|
refreshToken: tokenResponse.refresh_token,
|
|
346
|
-
expiresAt: Date.now() +
|
|
506
|
+
expiresAt: Date.now() + expiresIn * 1e3,
|
|
347
507
|
apiUrl
|
|
348
508
|
};
|
|
349
509
|
} catch (err) {
|
|
@@ -369,8 +529,8 @@ async function startCallbackServer(expectedState) {
|
|
|
369
529
|
return new Promise((resolveServer) => {
|
|
370
530
|
let resolveCode;
|
|
371
531
|
let rejectCode;
|
|
372
|
-
const waitForCode = new Promise((
|
|
373
|
-
resolveCode =
|
|
532
|
+
const waitForCode = new Promise((resolve4, reject) => {
|
|
533
|
+
resolveCode = resolve4;
|
|
374
534
|
rejectCode = reject;
|
|
375
535
|
});
|
|
376
536
|
const server = http.createServer((req, res) => {
|
|
@@ -446,7 +606,11 @@ async function exchangeCode(apiUrl, code, codeVerifier, port) {
|
|
|
446
606
|
}
|
|
447
607
|
throw new Error(`Token exchange failed: ${message}`);
|
|
448
608
|
}
|
|
449
|
-
|
|
609
|
+
const parsed = await response.json();
|
|
610
|
+
if (typeof parsed.access_token !== "string" || parsed.access_token.length === 0) {
|
|
611
|
+
throw new Error("Token exchange returned no access_token");
|
|
612
|
+
}
|
|
613
|
+
return parsed;
|
|
450
614
|
}
|
|
451
615
|
function successPage() {
|
|
452
616
|
return `<!DOCTYPE html>
|
|
@@ -469,16 +633,29 @@ function errorPage(message) {
|
|
|
469
633
|
// libs/cli/src/commands/logout.ts
|
|
470
634
|
var import_commander2 = require("commander");
|
|
471
635
|
function createLogoutCommand() {
|
|
472
|
-
return new import_commander2.Command("logout").description("Clear stored credentials").
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
636
|
+
return new import_commander2.Command("logout").description("Clear stored credentials").option("--global", "Clear only the global credentials file").option(
|
|
637
|
+
"--all",
|
|
638
|
+
"Clear the current project credentials file (if any) and the global credentials file (other projects are not touched \u2014 they cannot be enumerated)"
|
|
639
|
+
).action(async (opts) => {
|
|
640
|
+
if (opts.global === true && opts.all === true) {
|
|
641
|
+
warn("--global and --all cannot be combined; --all already removes the global file");
|
|
642
|
+
process.exitCode = 1;
|
|
643
|
+
return;
|
|
644
|
+
}
|
|
645
|
+
const scope = opts.all === true ? "all" : opts.global === true ? "global" : "auto";
|
|
646
|
+
const { cleared } = clearCredentials(scope);
|
|
476
647
|
if (isJsonMode()) {
|
|
477
|
-
json({
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
648
|
+
json({
|
|
649
|
+
success: true,
|
|
650
|
+
cleared: cleared.map((c) => ({ path: c.path, scope: c.scope }))
|
|
651
|
+
});
|
|
652
|
+
} else if (cleared.length === 0) {
|
|
481
653
|
warn("No credentials found");
|
|
654
|
+
} else {
|
|
655
|
+
success("Logged out successfully");
|
|
656
|
+
for (const loc of cleared) {
|
|
657
|
+
info(`Removed ${loc.path} (${loc.scope})`);
|
|
658
|
+
}
|
|
482
659
|
}
|
|
483
660
|
});
|
|
484
661
|
}
|
|
@@ -499,19 +676,20 @@ var ApiClient = class {
|
|
|
499
676
|
if (this.explicitToken) return this.explicitToken;
|
|
500
677
|
const envToken = process.env["SPACELR_TOKEN"];
|
|
501
678
|
if (envToken) return envToken;
|
|
502
|
-
const
|
|
503
|
-
if (!
|
|
679
|
+
const loaded = getStoredCredentialsWithLocation();
|
|
680
|
+
if (!loaded) return null;
|
|
681
|
+
const { credentials, location } = loaded;
|
|
504
682
|
if (!isTokenExpired(credentials)) {
|
|
505
683
|
return credentials.accessToken;
|
|
506
684
|
}
|
|
507
685
|
if (credentials.refreshToken) {
|
|
508
|
-
return this.refreshAccessToken(credentials);
|
|
686
|
+
return this.refreshAccessToken(credentials, location);
|
|
509
687
|
}
|
|
510
688
|
return null;
|
|
511
689
|
}
|
|
512
|
-
async refreshAccessToken(credentials) {
|
|
690
|
+
async refreshAccessToken(credentials, location) {
|
|
513
691
|
if (this.refreshing) return this.refreshing;
|
|
514
|
-
this.refreshing = refreshStoredToken(credentials).finally(() => {
|
|
692
|
+
this.refreshing = refreshStoredToken(credentials, location).finally(() => {
|
|
515
693
|
this.refreshing = null;
|
|
516
694
|
});
|
|
517
695
|
return this.refreshing;
|
|
@@ -523,9 +701,9 @@ var ApiClient = class {
|
|
|
523
701
|
}
|
|
524
702
|
return body;
|
|
525
703
|
}
|
|
526
|
-
async request(
|
|
704
|
+
async request(path7, options = {}) {
|
|
527
705
|
const { method = "GET", body, headers = {}, timeout = 3e4 } = options;
|
|
528
|
-
const url = `${this.apiUrl}${
|
|
706
|
+
const url = `${this.apiUrl}${path7}`;
|
|
529
707
|
const token = await this.getToken();
|
|
530
708
|
if (token) {
|
|
531
709
|
headers["Authorization"] = `Bearer ${token}`;
|
|
@@ -549,9 +727,17 @@ var ApiClient = class {
|
|
|
549
727
|
signal: controller.signal
|
|
550
728
|
});
|
|
551
729
|
if (response.status === 401 && !this.explicitToken) {
|
|
552
|
-
const
|
|
553
|
-
if (
|
|
554
|
-
|
|
730
|
+
const loaded = getStoredCredentialsWithLocation();
|
|
731
|
+
if (loaded) {
|
|
732
|
+
let newToken = null;
|
|
733
|
+
if (!isTokenExpired(loaded.credentials)) {
|
|
734
|
+
newToken = loaded.credentials.accessToken;
|
|
735
|
+
} else if (loaded.credentials.refreshToken) {
|
|
736
|
+
newToken = await this.refreshAccessToken(
|
|
737
|
+
loaded.credentials,
|
|
738
|
+
loaded.location
|
|
739
|
+
);
|
|
740
|
+
}
|
|
555
741
|
if (newToken) {
|
|
556
742
|
headers["Authorization"] = `Bearer ${newToken}`;
|
|
557
743
|
const retryController = new AbortController();
|
|
@@ -564,6 +750,11 @@ var ApiClient = class {
|
|
|
564
750
|
});
|
|
565
751
|
clearTimeout(retryTimer);
|
|
566
752
|
if (!retryResponse.ok) {
|
|
753
|
+
if (retryResponse.status === 401) {
|
|
754
|
+
throw new Error(
|
|
755
|
+
'Authentication required. Run "spacelr login" to authenticate.'
|
|
756
|
+
);
|
|
757
|
+
}
|
|
567
758
|
await this.throwApiError(retryResponse);
|
|
568
759
|
}
|
|
569
760
|
return await retryResponse.json();
|
|
@@ -583,7 +774,7 @@ var ApiClient = class {
|
|
|
583
774
|
return await response.text();
|
|
584
775
|
} catch (err) {
|
|
585
776
|
if (err instanceof Error && err.name === "AbortError") {
|
|
586
|
-
throw new Error(`Request timed out: ${method} ${
|
|
777
|
+
throw new Error(`Request timed out: ${method} ${path7}`);
|
|
587
778
|
}
|
|
588
779
|
throw err;
|
|
589
780
|
} finally {
|
|
@@ -600,24 +791,28 @@ var ApiClient = class {
|
|
|
600
791
|
}
|
|
601
792
|
throw new Error(`API error (${response.status}): ${message}`);
|
|
602
793
|
}
|
|
603
|
-
async get(
|
|
604
|
-
return this.request(
|
|
794
|
+
async get(path7, timeout) {
|
|
795
|
+
return this.request(path7, { timeout });
|
|
796
|
+
}
|
|
797
|
+
async post(path7, body, timeout) {
|
|
798
|
+
return this.request(path7, { method: "POST", body, timeout });
|
|
605
799
|
}
|
|
606
|
-
async
|
|
607
|
-
return this.request(
|
|
800
|
+
async put(path7, body, timeout) {
|
|
801
|
+
return this.request(path7, { method: "PUT", body, timeout });
|
|
608
802
|
}
|
|
609
|
-
async
|
|
610
|
-
return this.request(
|
|
803
|
+
async patch(path7, body, timeout) {
|
|
804
|
+
return this.request(path7, { method: "PATCH", body, timeout });
|
|
611
805
|
}
|
|
612
|
-
async delete(
|
|
613
|
-
return this.request(
|
|
806
|
+
async delete(path7, timeout) {
|
|
807
|
+
return this.request(path7, { method: "DELETE", timeout });
|
|
614
808
|
}
|
|
615
|
-
async uploadFile(
|
|
809
|
+
async uploadFile(path7, fileBuffer, filename) {
|
|
616
810
|
const boundary = `----spacelr${Date.now()}`;
|
|
617
811
|
const parts = [];
|
|
812
|
+
const safeFilename = filename.replace(/[\r\n"]/g, "_");
|
|
618
813
|
const header = Buffer.from(
|
|
619
814
|
`--${boundary}\r
|
|
620
|
-
Content-Disposition: form-data; name="file"; filename="${
|
|
815
|
+
Content-Disposition: form-data; name="file"; filename="${safeFilename}"\r
|
|
621
816
|
Content-Type: application/zip\r
|
|
622
817
|
\r
|
|
623
818
|
`
|
|
@@ -627,7 +822,7 @@ Content-Type: application/zip\r
|
|
|
627
822
|
`);
|
|
628
823
|
parts.push(header, fileBuffer, footer);
|
|
629
824
|
const body = Buffer.concat(parts);
|
|
630
|
-
return this.request(
|
|
825
|
+
return this.request(path7, {
|
|
631
826
|
method: "POST",
|
|
632
827
|
body,
|
|
633
828
|
headers: {
|
|
@@ -728,7 +923,7 @@ ${message}
|
|
|
728
923
|
console.error(` ${i + 1}) ${choice.label}`);
|
|
729
924
|
});
|
|
730
925
|
console.error();
|
|
731
|
-
return new Promise((
|
|
926
|
+
return new Promise((resolve4, reject) => {
|
|
732
927
|
rl.question(" Enter number: ", (answer) => {
|
|
733
928
|
rl.close();
|
|
734
929
|
const index = parseInt(answer, 10) - 1;
|
|
@@ -736,7 +931,7 @@ ${message}
|
|
|
736
931
|
reject(new Error("Invalid selection"));
|
|
737
932
|
return;
|
|
738
933
|
}
|
|
739
|
-
|
|
934
|
+
resolve4(choices[index]);
|
|
740
935
|
});
|
|
741
936
|
});
|
|
742
937
|
}
|
|
@@ -745,10 +940,10 @@ async function promptConfirm(message) {
|
|
|
745
940
|
input: process.stdin,
|
|
746
941
|
output: process.stderr
|
|
747
942
|
});
|
|
748
|
-
return new Promise((
|
|
943
|
+
return new Promise((resolve4) => {
|
|
749
944
|
rl.question(`${message} (y/N) `, (answer) => {
|
|
750
945
|
rl.close();
|
|
751
|
-
|
|
946
|
+
resolve4(answer.toLowerCase() === "y" || answer.toLowerCase() === "yes");
|
|
752
947
|
});
|
|
753
948
|
});
|
|
754
949
|
}
|
|
@@ -758,10 +953,10 @@ async function promptInput(message, defaultValue) {
|
|
|
758
953
|
output: process.stderr
|
|
759
954
|
});
|
|
760
955
|
const suffix = defaultValue ? ` (${defaultValue})` : "";
|
|
761
|
-
return new Promise((
|
|
956
|
+
return new Promise((resolve4) => {
|
|
762
957
|
rl.question(`${message}${suffix}: `, (answer) => {
|
|
763
958
|
rl.close();
|
|
764
|
-
|
|
959
|
+
resolve4(answer.trim() || defaultValue || "");
|
|
765
960
|
});
|
|
766
961
|
});
|
|
767
962
|
}
|
|
@@ -860,7 +1055,7 @@ function createInitCommand() {
|
|
|
860
1055
|
);
|
|
861
1056
|
const config = {
|
|
862
1057
|
projectId: selected.id,
|
|
863
|
-
apiUrl: apiUrl !== "https://api.spacelr.
|
|
1058
|
+
apiUrl: apiUrl !== "https://api.spacelr.com/api/v1" ? apiUrl : void 0,
|
|
864
1059
|
hosting: {
|
|
865
1060
|
directory: hostingDir
|
|
866
1061
|
}
|
|
@@ -901,11 +1096,11 @@ async function createZipBuffer(directory) {
|
|
|
901
1096
|
if (!fs4.statSync(absDir).isDirectory()) {
|
|
902
1097
|
throw new Error(`Not a directory: ${absDir}`);
|
|
903
1098
|
}
|
|
904
|
-
return new Promise((
|
|
1099
|
+
return new Promise((resolve4, reject) => {
|
|
905
1100
|
const chunks = [];
|
|
906
1101
|
const archive = (0, import_archiver.default)("zip", { zlib: { level: 9 } });
|
|
907
1102
|
archive.on("data", (chunk) => chunks.push(chunk));
|
|
908
|
-
archive.on("end", () =>
|
|
1103
|
+
archive.on("end", () => resolve4(Buffer.concat(chunks)));
|
|
909
1104
|
archive.on("error", reject);
|
|
910
1105
|
archive.directory(absDir, false);
|
|
911
1106
|
archive.finalize();
|
|
@@ -1034,7 +1229,7 @@ async function pollDeploymentStatus(client, projectId, deploymentId, spin) {
|
|
|
1034
1229
|
throw new Error("Deployment timed out after 10 minutes");
|
|
1035
1230
|
}
|
|
1036
1231
|
function sleep(ms) {
|
|
1037
|
-
return new Promise((
|
|
1232
|
+
return new Promise((resolve4) => setTimeout(resolve4, ms));
|
|
1038
1233
|
}
|
|
1039
1234
|
|
|
1040
1235
|
// libs/cli/src/commands/db/index.ts
|
|
@@ -1267,8 +1462,266 @@ function createDbCommand() {
|
|
|
1267
1462
|
return db;
|
|
1268
1463
|
}
|
|
1269
1464
|
|
|
1465
|
+
// libs/cli/src/commands/functions.ts
|
|
1466
|
+
var path6 = __toESM(require("path"));
|
|
1467
|
+
var import_commander13 = require("commander");
|
|
1468
|
+
function createFunctionsCommand() {
|
|
1469
|
+
const functions = new import_commander13.Command("functions").description("Manage serverless functions");
|
|
1470
|
+
functions.command("deploy [directory]").description("Deploy a function").requiredOption("--name <name>", "Function name").option("--entry <file>", "Entry point (default: index.js)").option("--cron <expression>", "Cron schedule expression").option("--timezone <tz>", "Timezone for cron (default: UTC)").option("--timeout <ms>", "Execution timeout in ms").option("--memory <mb>", "Memory limit in MB").option("--env <KEY=VALUE...>", "Environment variables", collectEnvVars, {}).option("--project <id>", "Project ID").action(async (directory, opts, cmd) => {
|
|
1471
|
+
const globalOpts = cmd.optsWithGlobals();
|
|
1472
|
+
const config = loadConfig();
|
|
1473
|
+
const apiUrl = resolveApiUrl(globalOpts.apiUrl, config);
|
|
1474
|
+
try {
|
|
1475
|
+
const token = await requireAuth(globalOpts.token);
|
|
1476
|
+
const projectId = resolveProjectId(opts.project, config);
|
|
1477
|
+
const deployDir = directory ?? ".";
|
|
1478
|
+
const absDir = path6.resolve(deployDir);
|
|
1479
|
+
const client = new ApiClient({ apiUrl, token, projectId });
|
|
1480
|
+
const spin = spinner("Preparing deployment...");
|
|
1481
|
+
const fileCount = countFiles(absDir);
|
|
1482
|
+
verbose(`Found ${fileCount} files in ${absDir}`);
|
|
1483
|
+
spin.text = "Creating archive...";
|
|
1484
|
+
const zipBuffer = await createZipBuffer(absDir);
|
|
1485
|
+
verbose(`Archive size: ${formatBytes(zipBuffer.length)}`);
|
|
1486
|
+
spin.text = "Checking for existing function...";
|
|
1487
|
+
const envVars = opts.env;
|
|
1488
|
+
const existingFns = await client.get(`/projects/${projectId}/functions`);
|
|
1489
|
+
const fnList = Array.isArray(existingFns) ? existingFns : existingFns.data || [];
|
|
1490
|
+
const existing = fnList.find(
|
|
1491
|
+
(f) => f.name === opts.name
|
|
1492
|
+
);
|
|
1493
|
+
let functionId;
|
|
1494
|
+
const functionBody = {
|
|
1495
|
+
entryPoint: opts.entry,
|
|
1496
|
+
cronExpression: opts.cron,
|
|
1497
|
+
cronTimezone: opts.timezone,
|
|
1498
|
+
timeout: opts.timeout ? parseInt(opts.timeout, 10) : void 0,
|
|
1499
|
+
memoryLimitMb: opts.memory ? parseInt(opts.memory, 10) : void 0,
|
|
1500
|
+
environmentVariables: Object.keys(envVars).length > 0 ? envVars : void 0
|
|
1501
|
+
};
|
|
1502
|
+
if (existing) {
|
|
1503
|
+
spin.text = "Updating function...";
|
|
1504
|
+
await client.patch(
|
|
1505
|
+
`/projects/${projectId}/functions/${existing.id}`,
|
|
1506
|
+
functionBody
|
|
1507
|
+
);
|
|
1508
|
+
functionId = existing.id;
|
|
1509
|
+
} else {
|
|
1510
|
+
spin.text = "Creating function...";
|
|
1511
|
+
const createResult = await client.post(
|
|
1512
|
+
`/projects/${projectId}/functions`,
|
|
1513
|
+
{ name: opts.name, ...functionBody }
|
|
1514
|
+
);
|
|
1515
|
+
const createdId = createResult.id || createResult.data?.id;
|
|
1516
|
+
if (!createdId) {
|
|
1517
|
+
throw new Error("Failed to create function");
|
|
1518
|
+
}
|
|
1519
|
+
functionId = createdId;
|
|
1520
|
+
}
|
|
1521
|
+
spin.text = "Uploading code...";
|
|
1522
|
+
await client.uploadFile(
|
|
1523
|
+
`/projects/${projectId}/functions/${functionId}/deploy`,
|
|
1524
|
+
zipBuffer,
|
|
1525
|
+
"code.zip"
|
|
1526
|
+
);
|
|
1527
|
+
spin.succeed(
|
|
1528
|
+
`Function "${opts.name}" deployed (${formatBytes(zipBuffer.length)}, ${fileCount} files)`
|
|
1529
|
+
);
|
|
1530
|
+
if (opts.cron) {
|
|
1531
|
+
info(`Cron schedule: ${opts.cron}`);
|
|
1532
|
+
}
|
|
1533
|
+
} catch (err) {
|
|
1534
|
+
error(err instanceof Error ? err.message : "Deploy failed");
|
|
1535
|
+
process.exitCode = 1;
|
|
1536
|
+
}
|
|
1537
|
+
});
|
|
1538
|
+
functions.command("list").description("List functions").option("--project <id>", "Project ID").action(async (opts, cmd) => {
|
|
1539
|
+
const globalOpts = cmd.optsWithGlobals();
|
|
1540
|
+
const config = loadConfig();
|
|
1541
|
+
const apiUrl = resolveApiUrl(globalOpts.apiUrl, config);
|
|
1542
|
+
try {
|
|
1543
|
+
const token = await requireAuth(globalOpts.token);
|
|
1544
|
+
const projectId = resolveProjectId(opts.project, config);
|
|
1545
|
+
const client = new ApiClient({ apiUrl, token, projectId });
|
|
1546
|
+
const result = await client.get(
|
|
1547
|
+
`/projects/${projectId}/functions`
|
|
1548
|
+
);
|
|
1549
|
+
const fns = Array.isArray(result) ? result : result.data || [];
|
|
1550
|
+
if (isJsonMode()) {
|
|
1551
|
+
json(fns);
|
|
1552
|
+
return;
|
|
1553
|
+
}
|
|
1554
|
+
if (fns.length === 0) {
|
|
1555
|
+
info("No functions found");
|
|
1556
|
+
return;
|
|
1557
|
+
}
|
|
1558
|
+
for (const fn of fns) {
|
|
1559
|
+
const status = fn.enabled ? "enabled" : "disabled";
|
|
1560
|
+
const cron = fn.cronExpression ? ` [${fn.cronExpression}]` : "";
|
|
1561
|
+
info(`${fn.name} (${fn.id}) - ${status}${cron}`);
|
|
1562
|
+
}
|
|
1563
|
+
} catch (err) {
|
|
1564
|
+
error(err instanceof Error ? err.message : "Failed to list functions");
|
|
1565
|
+
process.exitCode = 1;
|
|
1566
|
+
}
|
|
1567
|
+
});
|
|
1568
|
+
functions.command("delete <name>").description("Delete a function").option("--project <id>", "Project ID").action(async (name, opts, cmd) => {
|
|
1569
|
+
const globalOpts = cmd.optsWithGlobals();
|
|
1570
|
+
const config = loadConfig();
|
|
1571
|
+
const apiUrl = resolveApiUrl(globalOpts.apiUrl, config);
|
|
1572
|
+
try {
|
|
1573
|
+
const token = await requireAuth(globalOpts.token);
|
|
1574
|
+
const projectId = resolveProjectId(opts.project, config);
|
|
1575
|
+
const client = new ApiClient({ apiUrl, token, projectId });
|
|
1576
|
+
const fn = await findFunctionByName(client, projectId, name);
|
|
1577
|
+
await client.delete(
|
|
1578
|
+
`/projects/${projectId}/functions/${fn.id}`
|
|
1579
|
+
);
|
|
1580
|
+
info(`Function "${name}" deleted`);
|
|
1581
|
+
} catch (err) {
|
|
1582
|
+
error(err instanceof Error ? err.message : "Failed to delete function");
|
|
1583
|
+
process.exitCode = 1;
|
|
1584
|
+
}
|
|
1585
|
+
});
|
|
1586
|
+
functions.command("trigger <name>").description("Trigger a function execution").option("--project <id>", "Project ID").action(async (name, opts, cmd) => {
|
|
1587
|
+
const globalOpts = cmd.optsWithGlobals();
|
|
1588
|
+
const config = loadConfig();
|
|
1589
|
+
const apiUrl = resolveApiUrl(globalOpts.apiUrl, config);
|
|
1590
|
+
try {
|
|
1591
|
+
const token = await requireAuth(globalOpts.token);
|
|
1592
|
+
const projectId = resolveProjectId(opts.project, config);
|
|
1593
|
+
const client = new ApiClient({ apiUrl, token, projectId });
|
|
1594
|
+
const fn = await findFunctionByName(client, projectId, name);
|
|
1595
|
+
const spin = spinner("Triggering function...");
|
|
1596
|
+
await client.post(
|
|
1597
|
+
`/projects/${projectId}/functions/${fn.id}/trigger`
|
|
1598
|
+
);
|
|
1599
|
+
spin.succeed(`Function "${name}" triggered`);
|
|
1600
|
+
} catch (err) {
|
|
1601
|
+
error(err instanceof Error ? err.message : "Failed to trigger function");
|
|
1602
|
+
process.exitCode = 1;
|
|
1603
|
+
}
|
|
1604
|
+
});
|
|
1605
|
+
functions.command("logs <name>").description("View function execution logs").option("--limit <n>", "Number of executions to show", "10").option("--project <id>", "Project ID").action(async (name, opts, cmd) => {
|
|
1606
|
+
const globalOpts = cmd.optsWithGlobals();
|
|
1607
|
+
const config = loadConfig();
|
|
1608
|
+
const apiUrl = resolveApiUrl(globalOpts.apiUrl, config);
|
|
1609
|
+
try {
|
|
1610
|
+
const token = await requireAuth(globalOpts.token);
|
|
1611
|
+
const projectId = resolveProjectId(opts.project, config);
|
|
1612
|
+
const client = new ApiClient({ apiUrl, token, projectId });
|
|
1613
|
+
const fn = await findFunctionByName(client, projectId, name);
|
|
1614
|
+
const limit = parseInt(opts.limit, 10);
|
|
1615
|
+
const result = await client.get(
|
|
1616
|
+
`/projects/${projectId}/functions/${fn.id}/executions?limit=${limit}`
|
|
1617
|
+
);
|
|
1618
|
+
const executions = result.items || result.data?.items || [];
|
|
1619
|
+
if (isJsonMode()) {
|
|
1620
|
+
json(executions);
|
|
1621
|
+
return;
|
|
1622
|
+
}
|
|
1623
|
+
if (executions.length === 0) {
|
|
1624
|
+
info("No executions found");
|
|
1625
|
+
return;
|
|
1626
|
+
}
|
|
1627
|
+
for (const exec of executions) {
|
|
1628
|
+
const date = new Date(exec.startedAt).toLocaleString();
|
|
1629
|
+
const duration = exec.duration ? `${exec.duration}ms` : "-";
|
|
1630
|
+
info(
|
|
1631
|
+
`[${exec.status}] ${date} (${duration}) - ${exec.triggeredBy}${exec.error ? ` - ${exec.error}` : ""}`
|
|
1632
|
+
);
|
|
1633
|
+
if (exec.logs && exec.logs.length > 0) {
|
|
1634
|
+
for (const log of exec.logs) {
|
|
1635
|
+
const logTime = new Date(log.timestamp).toLocaleTimeString();
|
|
1636
|
+
info(` ${logTime} [${log.level}] ${log.message}`);
|
|
1637
|
+
}
|
|
1638
|
+
}
|
|
1639
|
+
}
|
|
1640
|
+
} catch (err) {
|
|
1641
|
+
error(err instanceof Error ? err.message : "Failed to get logs");
|
|
1642
|
+
process.exitCode = 1;
|
|
1643
|
+
}
|
|
1644
|
+
});
|
|
1645
|
+
const envCmd = functions.command("env").description("Manage function environment variables");
|
|
1646
|
+
envCmd.command("set <name> <keyValue>").description("Set an environment variable (KEY=VALUE)").option("--project <id>", "Project ID").action(async (name, keyValue, opts, cmd) => {
|
|
1647
|
+
const globalOpts = cmd.optsWithGlobals();
|
|
1648
|
+
const config = loadConfig();
|
|
1649
|
+
const apiUrl = resolveApiUrl(globalOpts.apiUrl, config);
|
|
1650
|
+
try {
|
|
1651
|
+
const token = await requireAuth(globalOpts.token);
|
|
1652
|
+
const projectId = resolveProjectId(opts.project, config);
|
|
1653
|
+
const client = new ApiClient({ apiUrl, token, projectId });
|
|
1654
|
+
const eqIndex = keyValue.indexOf("=");
|
|
1655
|
+
if (eqIndex === -1) {
|
|
1656
|
+
throw new Error("Format must be KEY=VALUE");
|
|
1657
|
+
}
|
|
1658
|
+
const key = keyValue.substring(0, eqIndex);
|
|
1659
|
+
const value = keyValue.substring(eqIndex + 1);
|
|
1660
|
+
const fn = await findFunctionByName(client, projectId, name);
|
|
1661
|
+
const currentVars = fn.environmentVariables || {};
|
|
1662
|
+
const updatedVars = { ...currentVars, [key]: value };
|
|
1663
|
+
await client.patch(
|
|
1664
|
+
`/projects/${projectId}/functions/${fn.id}`,
|
|
1665
|
+
{ environmentVariables: updatedVars }
|
|
1666
|
+
);
|
|
1667
|
+
info(`Set ${key} for function "${name}"`);
|
|
1668
|
+
} catch (err) {
|
|
1669
|
+
error(err instanceof Error ? err.message : "Failed to set env var");
|
|
1670
|
+
process.exitCode = 1;
|
|
1671
|
+
}
|
|
1672
|
+
});
|
|
1673
|
+
envCmd.command("list <name>").description("List environment variables for a function").option("--project <id>", "Project ID").action(async (name, opts, cmd) => {
|
|
1674
|
+
const globalOpts = cmd.optsWithGlobals();
|
|
1675
|
+
const config = loadConfig();
|
|
1676
|
+
const apiUrl = resolveApiUrl(globalOpts.apiUrl, config);
|
|
1677
|
+
try {
|
|
1678
|
+
const token = await requireAuth(globalOpts.token);
|
|
1679
|
+
const projectId = resolveProjectId(opts.project, config);
|
|
1680
|
+
const client = new ApiClient({ apiUrl, token, projectId });
|
|
1681
|
+
const fn = await findFunctionByName(client, projectId, name);
|
|
1682
|
+
const vars = fn.environmentVariables || {};
|
|
1683
|
+
const keys = Object.keys(vars);
|
|
1684
|
+
if (isJsonMode()) {
|
|
1685
|
+
json(vars);
|
|
1686
|
+
return;
|
|
1687
|
+
}
|
|
1688
|
+
if (keys.length === 0) {
|
|
1689
|
+
info("No environment variables set");
|
|
1690
|
+
return;
|
|
1691
|
+
}
|
|
1692
|
+
for (const key of keys) {
|
|
1693
|
+
info(`${key}=${vars[key]}`);
|
|
1694
|
+
}
|
|
1695
|
+
} catch (err) {
|
|
1696
|
+
error(
|
|
1697
|
+
err instanceof Error ? err.message : "Failed to list env vars"
|
|
1698
|
+
);
|
|
1699
|
+
process.exitCode = 1;
|
|
1700
|
+
}
|
|
1701
|
+
});
|
|
1702
|
+
return functions;
|
|
1703
|
+
}
|
|
1704
|
+
async function findFunctionByName(client, projectId, name) {
|
|
1705
|
+
const result = await client.get(
|
|
1706
|
+
`/projects/${projectId}/functions`
|
|
1707
|
+
);
|
|
1708
|
+
const fnList = Array.isArray(result) ? result : result.data || [];
|
|
1709
|
+
const fn = fnList.find((f) => f.name === name);
|
|
1710
|
+
if (!fn) {
|
|
1711
|
+
throw new Error(`Function "${name}" not found`);
|
|
1712
|
+
}
|
|
1713
|
+
return fn;
|
|
1714
|
+
}
|
|
1715
|
+
function collectEnvVars(value, previous) {
|
|
1716
|
+
const eqIndex = value.indexOf("=");
|
|
1717
|
+
if (eqIndex === -1) return previous;
|
|
1718
|
+
const key = value.substring(0, eqIndex);
|
|
1719
|
+
const val = value.substring(eqIndex + 1);
|
|
1720
|
+
return { ...previous, [key]: val };
|
|
1721
|
+
}
|
|
1722
|
+
|
|
1270
1723
|
// libs/cli/src/index.ts
|
|
1271
|
-
var program = new
|
|
1724
|
+
var program = new import_commander14.Command();
|
|
1272
1725
|
program.name("spacelr").description("CLI tool for the Spacelr platform").version("0.1.0").option("--api-url <url>", "Override API URL").option("--token <token>", "Explicit auth token (CI mode)").option("--json", "Output as JSON").option("--verbose", "Verbose logging").hook("preAction", (thisCommand) => {
|
|
1273
1726
|
const opts = thisCommand.optsWithGlobals();
|
|
1274
1727
|
setOutputMode({
|
|
@@ -1284,5 +1737,6 @@ program.addCommand(createUseCommand());
|
|
|
1284
1737
|
program.addCommand(createInitCommand());
|
|
1285
1738
|
program.addCommand(createDeployCommand());
|
|
1286
1739
|
program.addCommand(createDbCommand());
|
|
1740
|
+
program.addCommand(createFunctionsCommand());
|
|
1287
1741
|
program.parse();
|
|
1288
1742
|
//# sourceMappingURL=index.js.map
|