simplemdg-dev-cli 2.5.0 → 2.6.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.
- package/README.md +33 -0
- package/USER_GUIDE.md +57 -0
- package/dist/commands/cache.command.d.ts +2 -0
- package/dist/commands/cache.command.js +129 -0
- package/dist/commands/cache.command.js.map +1 -0
- package/dist/commands/cf.command.js +201 -122
- package/dist/commands/cf.command.js.map +1 -1
- package/dist/commands/gitlab.command.js +33 -23
- package/dist/commands/gitlab.command.js.map +1 -1
- package/dist/core/cache/smart-cache-events.d.ts +3 -0
- package/dist/core/cache/smart-cache-events.js +20 -0
- package/dist/core/cache/smart-cache-events.js.map +1 -0
- package/dist/core/cache/smart-cache-manager.d.ts +20 -0
- package/dist/core/cache/smart-cache-manager.js +148 -0
- package/dist/core/cache/smart-cache-manager.js.map +1 -0
- package/dist/core/cache/smart-cache-store.d.ts +8 -0
- package/dist/core/cache/smart-cache-store.js +74 -0
- package/dist/core/cache/smart-cache-store.js.map +1 -0
- package/dist/core/cache/smart-cache.d.ts +18 -0
- package/dist/core/cache/smart-cache.js +117 -0
- package/dist/core/cache/smart-cache.js.map +1 -0
- package/dist/core/cache/smart-cache.types.d.ts +62 -0
- package/dist/core/cache/smart-cache.types.js +17 -0
- package/dist/core/cache/smart-cache.types.js.map +1 -0
- package/dist/core/cf/cf-target-cache.d.ts +7 -0
- package/dist/core/cf/cf-target-cache.js +58 -0
- package/dist/core/cf/cf-target-cache.js.map +1 -0
- package/dist/core/cf/cf-target.types.d.ts +11 -0
- package/dist/core/cf/cf-target.types.js +11 -0
- package/dist/core/cf/cf-target.types.js.map +1 -0
- package/dist/core/db/db-studio-client.d.ts +1 -1
- package/dist/core/db/db-studio-client.js +173 -44
- package/dist/core/db/db-studio-client.js.map +1 -1
- package/dist/core/db/db-studio-server.js +125 -0
- package/dist/core/db/db-studio-server.js.map +1 -1
- package/dist/core/db/db-studio-styles.d.ts +1 -1
- package/dist/core/db/db-studio-styles.js +36 -0
- package/dist/core/db/db-studio-styles.js.map +1 -1
- package/dist/core/db/db-types.d.ts +54 -0
- package/dist/core/db/studio/sql-formatter.d.ts +25 -0
- package/dist/core/db/studio/sql-formatter.js +139 -0
- package/dist/core/db/studio/sql-formatter.js.map +1 -0
- package/dist/core/db/studio/studio-settings.d.ts +4 -0
- package/dist/core/db/studio/studio-settings.js +39 -0
- package/dist/core/db/studio/studio-settings.js.map +1 -0
- package/dist/core/db/studio/workspace-cache.d.ts +3 -0
- package/dist/core/db/studio/workspace-cache.js +51 -0
- package/dist/core/db/studio/workspace-cache.js.map +1 -0
- package/dist/index.js +3 -1
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/commands/cache.command.ts +159 -0
- package/src/commands/cf.command.ts +232 -129
- package/src/commands/gitlab.command.ts +37 -21
- package/src/core/cache/smart-cache-events.ts +20 -0
- package/src/core/cache/smart-cache-manager.ts +169 -0
- package/src/core/cache/smart-cache-store.ts +83 -0
- package/src/core/cache/smart-cache.ts +97 -0
- package/src/core/cache/smart-cache.types.ts +79 -0
- package/src/core/cf/cf-target-cache.ts +61 -0
- package/src/core/cf/cf-target.types.ts +17 -0
- package/src/core/db/db-studio-client.ts +173 -44
- package/src/core/db/db-studio-server.ts +109 -1
- package/src/core/db/db-studio-styles.ts +36 -0
- package/src/core/db/db-types.ts +61 -0
- package/src/core/db/studio/sql-formatter.ts +139 -0
- package/src/core/db/studio/studio-settings.ts +36 -0
- package/src/core/db/studio/workspace-cache.ts +51 -0
- package/src/index.ts +3 -1
|
@@ -34,6 +34,17 @@ import { runCommand, runCommandInherit } from "../core/process";
|
|
|
34
34
|
import { resolveRepositoryPath } from "../core/repository";
|
|
35
35
|
import { searchableSelectChoice, selectFromHistoryOrInput } from "../core/prompts";
|
|
36
36
|
import { ensureExternalTool } from "../core/tooling";
|
|
37
|
+
import { smartRead, buildCfAppsKey, formatRelativeTime, DEFAULT_CACHE_TTL } from "../core/cache/smart-cache";
|
|
38
|
+
import {
|
|
39
|
+
addFavoriteTarget,
|
|
40
|
+
addRecentTarget,
|
|
41
|
+
isFavoriteTarget,
|
|
42
|
+
listFavoriteTargets,
|
|
43
|
+
listRecentTargets,
|
|
44
|
+
removeFavoriteTarget,
|
|
45
|
+
} from "../core/cf/cf-target-cache";
|
|
46
|
+
import { cfTargetKey, cfTargetLabel } from "../core/cf/cf-target.types";
|
|
47
|
+
import type { TCfTarget } from "../core/cf/cf-target.types";
|
|
37
48
|
import type { TCloudFoundryApp, TCloudFoundryLoginProfile, TCloudFoundryOrgEntry, TCloudFoundryTarget } from "../core/types";
|
|
38
49
|
|
|
39
50
|
type TCloudFoundryLoginOptions = {
|
|
@@ -1268,175 +1279,250 @@ async function getCloudFoundryOrganizationsAcrossRegions(options: { api?: string
|
|
|
1268
1279
|
return fallbackEntries;
|
|
1269
1280
|
}
|
|
1270
1281
|
|
|
1271
|
-
|
|
1272
|
-
|
|
1282
|
+
function orgEntryToTarget(entry: TCloudFoundryOrgEntry): TCfTarget {
|
|
1283
|
+
return { region: entry.region, apiEndpoint: entry.apiEndpoint, org: entry.org, space: "", lastRefreshedAt: entry.updatedAt };
|
|
1284
|
+
}
|
|
1273
1285
|
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
? "switch"
|
|
1278
|
-
: await searchableSelectChoice({
|
|
1279
|
-
message: "What do you want to do with CF org?",
|
|
1280
|
-
choices: [
|
|
1281
|
-
{ title: "List orgs across regions", value: "list" },
|
|
1282
|
-
{ title: "Switch org across regions", value: "switch" },
|
|
1283
|
-
],
|
|
1284
|
-
validateCustomValue: (value) => {
|
|
1285
|
-
const normalizedValue = value.trim().toLowerCase();
|
|
1286
|
-
return normalizedValue === "list" || normalizedValue === "switch"
|
|
1287
|
-
? true
|
|
1288
|
-
: "Type list or switch, or select one option.";
|
|
1289
|
-
},
|
|
1290
|
-
});
|
|
1286
|
+
function dedupeTargets(targets: TCfTarget[]): TCfTarget[] {
|
|
1287
|
+
const seen = new Set<string>();
|
|
1288
|
+
const result: TCfTarget[] = [];
|
|
1291
1289
|
|
|
1292
|
-
const
|
|
1293
|
-
|
|
1294
|
-
|
|
1290
|
+
for (const target of targets) {
|
|
1291
|
+
const key = cfTargetKey(target);
|
|
1292
|
+
if (seen.has(key)) continue;
|
|
1293
|
+
seen.add(key);
|
|
1294
|
+
result.push(target);
|
|
1295
|
+
}
|
|
1296
|
+
|
|
1297
|
+
return result;
|
|
1298
|
+
}
|
|
1299
|
+
|
|
1300
|
+
async function switchToCfTarget(target: TCfTarget, options: { space?: string }): Promise<void> {
|
|
1301
|
+
const apiEndpoint = target.apiEndpoint;
|
|
1302
|
+
|
|
1303
|
+
if (!apiEndpoint) {
|
|
1304
|
+
throw new Error("Cannot determine CF API endpoint for the selected target.");
|
|
1305
|
+
}
|
|
1306
|
+
|
|
1307
|
+
const region = target.region || inferCloudFoundryRegionFromApiEndpoint(apiEndpoint);
|
|
1308
|
+
const authenticatedProfile = await ensureCloudFoundryAuthenticatedForApiEndpoint({
|
|
1309
|
+
apiEndpoint,
|
|
1310
|
+
preferredOrg: target.org,
|
|
1311
|
+
preferredSpace: options.space ?? target.space,
|
|
1312
|
+
reason: "switch-target",
|
|
1295
1313
|
});
|
|
1296
|
-
const organizationRegionCount = new Set(organizationEntries.map((entry) => entry.region)).size;
|
|
1297
|
-
const latestTarget = await readCloudFoundryTarget();
|
|
1298
1314
|
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1315
|
+
const orgExitCode = await targetCloudFoundryOrg(target.org);
|
|
1316
|
+
|
|
1317
|
+
if (orgExitCode !== 0) {
|
|
1318
|
+
console.log(chalk.yellow("Cannot switch to this org after automatic authentication."));
|
|
1319
|
+
console.log(chalk.gray("Run smdg cf login, save the password, then try again."));
|
|
1320
|
+
process.exitCode = orgExitCode;
|
|
1321
|
+
return;
|
|
1322
|
+
}
|
|
1323
|
+
|
|
1324
|
+
let space = options.space?.trim() || target.space?.trim() || "";
|
|
1302
1325
|
|
|
1303
|
-
|
|
1304
|
-
|
|
1326
|
+
if (!space) {
|
|
1327
|
+
const spaces = await listCloudFoundrySpaces().catch(() => [] as string[]);
|
|
1328
|
+
const currentAfter = await readCloudFoundryTarget();
|
|
1329
|
+
const preferred = currentAfter.space || (spaces.includes("app") ? "app" : spaces[0]);
|
|
1330
|
+
space = spaces.length
|
|
1331
|
+
? await searchableSelectChoice({
|
|
1332
|
+
message: "Select CF space",
|
|
1333
|
+
choices: [
|
|
1334
|
+
...spaces.filter((s) => s === preferred).map((s) => ({ title: `${s} ${chalk.gray("suggested")}`, value: s })),
|
|
1335
|
+
...spaces.filter((s) => s !== preferred).map((s) => ({ title: s, value: s })),
|
|
1336
|
+
],
|
|
1337
|
+
validateCustomValue: validateRequired,
|
|
1338
|
+
customValueTitle: (value) => `Use typed CF space: ${value}`,
|
|
1339
|
+
})
|
|
1340
|
+
: await selectFromHistoryOrInput({ message: "Enter CF space", values: [], initialValue: "app" });
|
|
1341
|
+
}
|
|
1342
|
+
|
|
1343
|
+
if (space) {
|
|
1344
|
+
const spaceExitCode = await targetCloudFoundrySpace(space);
|
|
1345
|
+
if (spaceExitCode !== 0) {
|
|
1346
|
+
process.exitCode = spaceExitCode;
|
|
1305
1347
|
return;
|
|
1306
1348
|
}
|
|
1349
|
+
}
|
|
1307
1350
|
|
|
1308
|
-
|
|
1309
|
-
|
|
1351
|
+
await addRecentTarget({ region, apiEndpoint, org: target.org, space });
|
|
1352
|
+
|
|
1353
|
+
if (authenticatedProfile?.password) {
|
|
1354
|
+
await rememberCloudFoundryLoginProfile({ ...authenticatedProfile, apiEndpoint, org: target.org, space, updatedAt: new Date().toISOString() });
|
|
1355
|
+
}
|
|
1356
|
+
|
|
1357
|
+
console.log(chalk.green("CF target switched."));
|
|
1358
|
+
printTarget(await readCloudFoundryTarget());
|
|
1310
1359
|
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1360
|
+
if (!(await isFavoriteTarget({ region, apiEndpoint, org: target.org, space }))) {
|
|
1361
|
+
const favorite = await prompts({ type: "confirm", name: "fav", message: "Mark this target as favorite?", initial: false });
|
|
1362
|
+
if (favorite.fav) {
|
|
1363
|
+
await addFavoriteTarget({ region, apiEndpoint, org: target.org, space });
|
|
1364
|
+
console.log(chalk.gray("Added to favorites."));
|
|
1314
1365
|
}
|
|
1366
|
+
}
|
|
1367
|
+
}
|
|
1315
1368
|
|
|
1369
|
+
async function manageFavoriteTargets(favorites: TCfTarget[]): Promise<void> {
|
|
1370
|
+
if (!favorites.length) {
|
|
1371
|
+
console.log(chalk.gray("No favorites yet. Switch to a target and choose to favorite it."));
|
|
1316
1372
|
return;
|
|
1317
1373
|
}
|
|
1318
1374
|
|
|
1319
|
-
|
|
1375
|
+
const selected = await searchableSelectChoice({
|
|
1376
|
+
message: "Favorites — select one to remove",
|
|
1377
|
+
choices: [
|
|
1378
|
+
...favorites.map((target, index) => ({ title: `★ ${cfTargetLabel(target)}`, value: String(index) })),
|
|
1379
|
+
{ title: "Cancel", value: "__cancel__" },
|
|
1380
|
+
],
|
|
1381
|
+
allowCustomValue: false,
|
|
1382
|
+
});
|
|
1383
|
+
|
|
1384
|
+
if (selected === "__cancel__") {
|
|
1385
|
+
return;
|
|
1386
|
+
}
|
|
1320
1387
|
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
}) ?? {
|
|
1325
|
-
apiEndpoint: options.api?.trim() || latestTarget.apiEndpoint || "",
|
|
1326
|
-
region: options.api?.trim()
|
|
1327
|
-
? inferCloudFoundryRegionFromApiEndpoint(options.api.trim())
|
|
1328
|
-
: inferCloudFoundryRegionFromApiEndpoint(latestTarget.apiEndpoint ?? "current"),
|
|
1329
|
-
org: options.org.trim(),
|
|
1330
|
-
updatedAt: new Date().toISOString(),
|
|
1331
|
-
};
|
|
1332
|
-
} else {
|
|
1333
|
-
if (!organizationEntries.length) {
|
|
1334
|
-
console.log(chalk.yellow("No orgs were found across regions."));
|
|
1335
|
-
console.log(chalk.gray("Run smdg cf login and save the password, then run smdg cf org --list --refresh."));
|
|
1336
|
-
return;
|
|
1337
|
-
}
|
|
1388
|
+
await removeFavoriteTarget(favorites[Number(selected)]);
|
|
1389
|
+
console.log(chalk.green("Removed from favorites."));
|
|
1390
|
+
}
|
|
1338
1391
|
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
title: formatCloudFoundryOrgEntry(entry, latestTarget),
|
|
1343
|
-
value: String(index),
|
|
1344
|
-
})),
|
|
1345
|
-
validateCustomValue: validateRequired,
|
|
1346
|
-
customValueTitle: (value) => `Use typed CF org in current region: ${value}`,
|
|
1347
|
-
});
|
|
1392
|
+
function printTargetSections(favorites: TCfTarget[], recent: TCfTarget[], orgEntries: TCloudFoundryOrgEntry[], current: TCloudFoundryTarget): void {
|
|
1393
|
+
console.log(chalk.bold("CF Target Switcher"));
|
|
1394
|
+
console.log("");
|
|
1348
1395
|
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
updatedAt: new Date().toISOString(),
|
|
1354
|
-
};
|
|
1396
|
+
if (favorites.length) {
|
|
1397
|
+
console.log(chalk.yellow("Favorites"));
|
|
1398
|
+
favorites.forEach((target) => console.log(` ${chalk.yellow("★")} ${cfTargetLabel(target)}`));
|
|
1399
|
+
console.log("");
|
|
1355
1400
|
}
|
|
1356
1401
|
|
|
1357
|
-
if (
|
|
1358
|
-
|
|
1402
|
+
if (recent.length) {
|
|
1403
|
+
console.log(chalk.cyan("Recent"));
|
|
1404
|
+
recent.forEach((target) => console.log(` ${chalk.gray("◷")} ${cfTargetLabel(target)}`));
|
|
1405
|
+
console.log("");
|
|
1359
1406
|
}
|
|
1360
1407
|
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
1408
|
+
console.log(chalk.gray(`All Targets (${orgEntries.length})`));
|
|
1409
|
+
for (const entry of orgEntries) {
|
|
1410
|
+
const marker = entry.apiEndpoint === current.apiEndpoint && entry.org === current.org ? chalk.green("*") : " ";
|
|
1411
|
+
console.log(`${marker} ${entry.region} / ${entry.org}`);
|
|
1412
|
+
}
|
|
1413
|
+
}
|
|
1367
1414
|
|
|
1368
|
-
|
|
1415
|
+
async function runOrgCommand(options: TCloudFoundryOrgOptions): Promise<void> {
|
|
1416
|
+
const latestTarget = await readCloudFoundryTarget();
|
|
1417
|
+
const favorites = await listFavoriteTargets();
|
|
1418
|
+
const recent = await listRecentTargets();
|
|
1369
1419
|
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
1420
|
+
// Direct switch via flags.
|
|
1421
|
+
if (options.org?.trim()) {
|
|
1422
|
+
await ensureExternalTool("cf");
|
|
1423
|
+
const apiEndpoint = options.api?.trim() || latestTarget.apiEndpoint || "";
|
|
1424
|
+
const region = inferCloudFoundryRegionFromApiEndpoint(apiEndpoint || "current");
|
|
1425
|
+
await switchToCfTarget({ region, apiEndpoint, org: options.org.trim(), space: options.space?.trim() || "" }, { space: options.space });
|
|
1374
1426
|
return;
|
|
1375
1427
|
}
|
|
1376
1428
|
|
|
1377
|
-
const
|
|
1378
|
-
|
|
1379
|
-
|
|
1429
|
+
const action = options.list
|
|
1430
|
+
? "list"
|
|
1431
|
+
: options.switch
|
|
1432
|
+
? "switch"
|
|
1433
|
+
: await searchableSelectChoice({
|
|
1434
|
+
message: "CF target switcher",
|
|
1435
|
+
choices: [
|
|
1436
|
+
{ title: "Switch to a target (favorites, recent, all)", value: "switch" },
|
|
1437
|
+
{ title: "Refresh all regions", value: "refresh" },
|
|
1438
|
+
{ title: "List targets", value: "list" },
|
|
1439
|
+
{ title: "Manage favorites", value: "favorites" },
|
|
1440
|
+
{ title: "Show current target", value: "current" },
|
|
1441
|
+
],
|
|
1442
|
+
allowCustomValue: false,
|
|
1443
|
+
});
|
|
1380
1444
|
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
.filter((spaceName) => spaceName === preferredSpace)
|
|
1386
|
-
.map((spaceName) => ({ title: `${spaceName} ${spaceName === currentTargetAfterOrgSwitch.space ? chalk.gray("current") : chalk.gray("suggested")}`, value: spaceName })),
|
|
1387
|
-
...spaces
|
|
1388
|
-
.filter((spaceName) => spaceName !== preferredSpace)
|
|
1389
|
-
.map((spaceName) => ({ title: spaceName, value: spaceName })),
|
|
1390
|
-
],
|
|
1391
|
-
validateCustomValue: validateRequired,
|
|
1392
|
-
customValueTitle: (value) => `Use typed CF space: ${value}`,
|
|
1393
|
-
});
|
|
1445
|
+
if (action === "current") {
|
|
1446
|
+
printTarget(latestTarget);
|
|
1447
|
+
return;
|
|
1448
|
+
}
|
|
1394
1449
|
|
|
1395
|
-
if (
|
|
1396
|
-
|
|
1450
|
+
if (action === "favorites") {
|
|
1451
|
+
await manageFavoriteTargets(favorites);
|
|
1452
|
+
return;
|
|
1453
|
+
}
|
|
1397
1454
|
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1455
|
+
let orgEntries: TCloudFoundryOrgEntry[];
|
|
1456
|
+
|
|
1457
|
+
if (action === "refresh" || options.refresh) {
|
|
1458
|
+
await ensureExternalTool("cf");
|
|
1459
|
+
console.log(chalk.gray("Refreshing CF orgs across regions..."));
|
|
1460
|
+
orgEntries = await getCloudFoundryOrganizationsAcrossRegions({ api: options.api, refresh: true });
|
|
1461
|
+
console.log(chalk.green(`Refresh completed · ${orgEntries.length} target(s) found.`));
|
|
1462
|
+
console.log("");
|
|
1463
|
+
} else {
|
|
1464
|
+
const cache = await readCache();
|
|
1465
|
+
orgEntries = cache.cloudFoundry.orgsAcrossRegions ?? [];
|
|
1402
1466
|
}
|
|
1403
1467
|
|
|
1404
|
-
if (
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
1468
|
+
if (action === "list") {
|
|
1469
|
+
printTargetSections(favorites, recent, orgEntries, latestTarget);
|
|
1470
|
+
return;
|
|
1471
|
+
}
|
|
1472
|
+
|
|
1473
|
+
const allTargets = orgEntries.map(orgEntryToTarget);
|
|
1474
|
+
const recentKeys = new Set(recent.map((target) => cfTargetKey(target)));
|
|
1475
|
+
const combined = dedupeTargets([...favorites, ...recent, ...allTargets]);
|
|
1476
|
+
|
|
1477
|
+
if (!combined.length) {
|
|
1478
|
+
console.log(chalk.yellow("No cached targets yet."));
|
|
1479
|
+
console.log(chalk.gray("Choose 'Refresh all regions', or run smdg cf login and save the password."));
|
|
1480
|
+
return;
|
|
1412
1481
|
}
|
|
1413
1482
|
|
|
1414
|
-
const
|
|
1415
|
-
|
|
1416
|
-
|
|
1483
|
+
const selectedIndex = await searchableSelectChoice({
|
|
1484
|
+
message: `Select CF target (${favorites.length} favorite · ${recent.length} recent · ${allTargets.length} all)`,
|
|
1485
|
+
choices: combined.map((target, index) => {
|
|
1486
|
+
const marker = target.isFavorite ? chalk.yellow("★ ") : recentKeys.has(cfTargetKey(target)) ? chalk.gray("◷ ") : " ";
|
|
1487
|
+
return { title: `${marker}${cfTargetLabel(target)}`, value: String(index) };
|
|
1488
|
+
}),
|
|
1489
|
+
validateCustomValue: validateRequired,
|
|
1490
|
+
customValueTitle: (value) => `Use typed org in current region: ${value}`,
|
|
1491
|
+
});
|
|
1492
|
+
|
|
1493
|
+
const chosen = combined[Number(selectedIndex)] ?? {
|
|
1494
|
+
region: inferCloudFoundryRegionFromApiEndpoint(latestTarget.apiEndpoint ?? "current"),
|
|
1495
|
+
apiEndpoint: latestTarget.apiEndpoint ?? "",
|
|
1496
|
+
org: selectedIndex,
|
|
1497
|
+
space: "",
|
|
1498
|
+
};
|
|
1499
|
+
|
|
1500
|
+
await ensureExternalTool("cf");
|
|
1501
|
+
await switchToCfTarget(chosen, { space: options.space });
|
|
1417
1502
|
}
|
|
1418
1503
|
|
|
1419
1504
|
async function runAppsCommand(options: TCloudFoundryAppsOptions): Promise<void> {
|
|
1420
|
-
const target = await
|
|
1421
|
-
const targetKey = buildCloudFoundryTargetKey(target);
|
|
1422
|
-
const cache = await readCache();
|
|
1423
|
-
const cachedEntry = cache.cloudFoundry.appListsByTarget[targetKey];
|
|
1424
|
-
|
|
1505
|
+
const target = await ensureCloudFoundrySessionFromCache();
|
|
1425
1506
|
printTarget(target);
|
|
1426
1507
|
console.log("");
|
|
1427
1508
|
|
|
1428
|
-
const
|
|
1429
|
-
const
|
|
1430
|
-
|
|
1431
|
-
|
|
1509
|
+
const region = target.apiEndpoint ? inferCloudFoundryRegionFromApiEndpoint(target.apiEndpoint) : "current";
|
|
1510
|
+
const cacheKey = buildCfAppsKey(region, target.org ?? "?", target.space ?? "?");
|
|
1511
|
+
const targetLabel = `${region} / ${target.org ?? "?"} / ${target.space ?? "?"}`;
|
|
1512
|
+
|
|
1513
|
+
const result = await smartRead<TCloudFoundryApp[]>({
|
|
1514
|
+
namespace: "cf-apps",
|
|
1515
|
+
key: cacheKey,
|
|
1516
|
+
ttlMs: DEFAULT_CACHE_TTL.cfApps,
|
|
1517
|
+
mode: options.refresh ? "network-only" : "stale-while-revalidate",
|
|
1518
|
+
fetcher: listCloudFoundryApps,
|
|
1432
1519
|
});
|
|
1433
1520
|
|
|
1434
|
-
if (
|
|
1435
|
-
console.log(chalk.gray(`Using cached
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
console.log(chalk.green(`Refreshed cf apps cache for ${targetKey}.`));
|
|
1521
|
+
if (result.fromCache) {
|
|
1522
|
+
console.log(chalk.gray(`Using cached apps for ${targetLabel} from ${formatRelativeTime(result.updatedAt)}.`));
|
|
1523
|
+
if (result.isRefreshing) {
|
|
1524
|
+
console.log(chalk.gray("Refreshing apps in background..."));
|
|
1525
|
+
}
|
|
1440
1526
|
console.log("");
|
|
1441
1527
|
}
|
|
1442
1528
|
|
|
@@ -1446,9 +1532,26 @@ async function runAppsCommand(options: TCloudFoundryAppsOptions): Promise<void>
|
|
|
1446
1532
|
return;
|
|
1447
1533
|
}
|
|
1448
1534
|
|
|
1449
|
-
for (const app of
|
|
1535
|
+
for (const app of result.data) {
|
|
1450
1536
|
console.log([app.name, app.requestedState, app.processes, app.routes].filter(Boolean).join(" | "));
|
|
1451
1537
|
}
|
|
1538
|
+
|
|
1539
|
+
// Mirror the smart-cache apps into the legacy per-target cache used by
|
|
1540
|
+
// resolveAppSelection so bind/debug/logs stay in sync.
|
|
1541
|
+
await rememberCloudFoundryApps(buildCloudFoundryTargetKey(target), result.data).catch(() => undefined);
|
|
1542
|
+
|
|
1543
|
+
if (result.refreshPromise) {
|
|
1544
|
+
try {
|
|
1545
|
+
const fresh = await result.refreshPromise;
|
|
1546
|
+
await rememberCloudFoundryApps(buildCloudFoundryTargetKey(target), fresh).catch(() => undefined);
|
|
1547
|
+
console.log("");
|
|
1548
|
+
console.log(chalk.gray("Background refresh completed. Cache updated."));
|
|
1549
|
+
} catch (error) {
|
|
1550
|
+
console.log("");
|
|
1551
|
+
console.log(chalk.yellow(`Background refresh failed: ${error instanceof Error ? error.message : String(error)}`));
|
|
1552
|
+
console.log(chalk.gray("Showing cached apps. Run smdg cf login if your session expired."));
|
|
1553
|
+
}
|
|
1554
|
+
}
|
|
1452
1555
|
}
|
|
1453
1556
|
|
|
1454
1557
|
async function runAppsCacheRefreshCommand(): Promise<void> {
|
|
@@ -7,6 +7,7 @@ import prompts from "prompts";
|
|
|
7
7
|
import chalk from "chalk";
|
|
8
8
|
import { Command } from "commander";
|
|
9
9
|
import { searchableSelectChoice } from "../core/prompts";
|
|
10
|
+
import { smartRead, buildGitLabGroupsKey, buildGitLabProjectsKey, formatRelativeTime, DEFAULT_CACHE_TTL } from "../core/cache/smart-cache";
|
|
10
11
|
|
|
11
12
|
const GITLAB_CACHE_DIR = path.join(os.homedir(), ".simplemdg");
|
|
12
13
|
const GITLAB_CACHE_FILE = path.join(GITLAB_CACHE_DIR, "gitlab.json");
|
|
@@ -186,15 +187,23 @@ async function runLoginFlow(): Promise<TGitLabAuth> {
|
|
|
186
187
|
return auth;
|
|
187
188
|
}
|
|
188
189
|
|
|
189
|
-
async function listRootGroups(auth: TGitLabAuth, refresh: boolean): Promise<TGitLabGroup[]> {
|
|
190
|
-
const
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
190
|
+
async function listRootGroups(auth: TGitLabAuth, refresh: boolean, notify = false): Promise<TGitLabGroup[]> {
|
|
191
|
+
const result = await smartRead<TGitLabGroup[]>({
|
|
192
|
+
namespace: "gitlab-groups",
|
|
193
|
+
key: buildGitLabGroupsKey(auth.baseUrl, auth.username),
|
|
194
|
+
ttlMs: DEFAULT_CACHE_TTL.gitlabGroups,
|
|
195
|
+
mode: refresh ? "network-only" : "stale-while-revalidate",
|
|
196
|
+
fetcher: () => gitlabFetchAll<TGitLabGroup>(auth, "/groups", { min_access_level: "10", top_level_only: "true", all_available: "false", order_by: "name", sort: "asc" }),
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
if (notify && result.fromCache) {
|
|
200
|
+
console.log(chalk.gray(`Using ${result.data.length} cached GitLab groups from ${formatRelativeTime(result.updatedAt)}.${result.isRefreshing ? " Refreshing in background..." : ""}`));
|
|
201
|
+
}
|
|
202
|
+
if (notify && result.refreshPromise) {
|
|
203
|
+
result.refreshPromise.then(() => console.log(chalk.gray("GitLab groups cache updated."))).catch(() => console.log(chalk.yellow("GitLab groups refresh failed; using cached list.")));
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
return result.data;
|
|
198
207
|
}
|
|
199
208
|
|
|
200
209
|
async function askGroup(auth: TGitLabAuth, refresh?: boolean): Promise<TGitLabGroup> {
|
|
@@ -210,17 +219,24 @@ async function askGroup(auth: TGitLabAuth, refresh?: boolean): Promise<TGitLabGr
|
|
|
210
219
|
return group;
|
|
211
220
|
}
|
|
212
221
|
|
|
213
|
-
async function listProjects(auth: TGitLabAuth, group: TGitLabGroup, refresh: boolean): Promise<TGitLabProject[]> {
|
|
214
|
-
const cache = await readGitLabCache();
|
|
215
|
-
const key = makeGroupCacheKey(auth.baseUrl, group.id);
|
|
216
|
-
const cached = cache.projectsByGroup[key];
|
|
217
|
-
if (!refresh && cached?.projects?.length) return cached.projects;
|
|
218
|
-
console.log(chalk.gray(`Scanning projects in ${group.full_path}...`));
|
|
222
|
+
async function listProjects(auth: TGitLabAuth, group: TGitLabGroup, refresh: boolean, notify = false): Promise<TGitLabProject[]> {
|
|
219
223
|
const encodedId = encodeURIComponent(String(group.id));
|
|
220
|
-
const
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
+
const result = await smartRead<TGitLabProject[]>({
|
|
225
|
+
namespace: "gitlab-projects",
|
|
226
|
+
key: buildGitLabProjectsKey(auth.baseUrl, group.id),
|
|
227
|
+
ttlMs: DEFAULT_CACHE_TTL.gitlabProjects,
|
|
228
|
+
mode: refresh ? "network-only" : "stale-while-revalidate",
|
|
229
|
+
fetcher: () => gitlabFetchAll<TGitLabProject>(auth, `/groups/${encodedId}/projects`, { include_subgroups: "true", archived: "false", order_by: "path", sort: "asc" }),
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
if (notify && result.fromCache) {
|
|
233
|
+
console.log(chalk.gray(`Using ${result.data.length} cached projects in ${group.full_path} from ${formatRelativeTime(result.updatedAt)}.${result.isRefreshing ? " Refreshing in background..." : ""}`));
|
|
234
|
+
}
|
|
235
|
+
if (notify && result.refreshPromise) {
|
|
236
|
+
result.refreshPromise.then(() => console.log(chalk.gray("GitLab projects cache updated."))).catch(() => console.log(chalk.yellow("GitLab projects refresh failed; using cached list.")));
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
return result.data;
|
|
224
240
|
}
|
|
225
241
|
|
|
226
242
|
function localProjectPath(destination: string, project: TGitLabProject): string {
|
|
@@ -359,13 +375,13 @@ export function registerGitLabCommands(program: Command): void {
|
|
|
359
375
|
gitlab.command("logout").description("Remove cached GitLab login").action(async () => { const cache = await readGitLabCache(); cache.instances = []; await writeGitLabCache(cache); console.log("GitLab login cache cleared."); });
|
|
360
376
|
gitlab.command("groups").description("List GitLab root groups").option("--refresh", "Refresh from API").action(async (options: { refresh?: boolean }) => {
|
|
361
377
|
const auth = await askAuth();
|
|
362
|
-
const groups = await listRootGroups(auth, !!options.refresh);
|
|
378
|
+
const groups = await listRootGroups(auth, !!options.refresh, true);
|
|
363
379
|
for (const group of groups) console.log(`${group.full_path} · #${group.id} · ${group.visibility ?? ""}`);
|
|
364
380
|
});
|
|
365
381
|
gitlab.command("projects").description("List projects in a GitLab root group").option("--refresh", "Refresh from API").action(async (options: { refresh?: boolean }) => {
|
|
366
382
|
const auth = await askAuth();
|
|
367
383
|
const group = await askGroup(auth, options.refresh);
|
|
368
|
-
const projects = await listProjects(auth, group, !!options.refresh);
|
|
384
|
+
const projects = await listProjects(auth, group, !!options.refresh, true);
|
|
369
385
|
for (const project of projects) console.log(`${project.path_with_namespace} · #${project.id}`);
|
|
370
386
|
});
|
|
371
387
|
gitlab.command("sync").alias("clone").description("Clone or update GitLab projects without ghorg").option("--refresh", "Refresh groups/projects from API").action(runSync);
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { EventEmitter } from "node:events";
|
|
2
|
+
import type { TCacheEvent } from "./smart-cache.types";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Process-local event bus for Smart Cache background refreshes. The DB Studio
|
|
6
|
+
* server subscribes to this and forwards events to the browser over SSE.
|
|
7
|
+
*/
|
|
8
|
+
const emitter = new EventEmitter();
|
|
9
|
+
emitter.setMaxListeners(50);
|
|
10
|
+
|
|
11
|
+
const EVENT_NAME = "cache-event";
|
|
12
|
+
|
|
13
|
+
export function emitCacheEvent(event: TCacheEvent): void {
|
|
14
|
+
emitter.emit(EVENT_NAME, event);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function onCacheEvent(listener: (event: TCacheEvent) => void): () => void {
|
|
18
|
+
emitter.on(EVENT_NAME, listener);
|
|
19
|
+
return () => emitter.off(EVENT_NAME, listener);
|
|
20
|
+
}
|