@kikimoradev/kcloud 0.12.1 → 0.14.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 +17 -12
- package/dist/main.js +1323 -463
- package/package.json +1 -1
package/dist/main.js
CHANGED
|
@@ -431,9 +431,10 @@ const createTerminal = ({ stdout, stderr }) => ({
|
|
|
431
431
|
//#region src/cli/cli.ts
|
|
432
432
|
const program = "kcloud";
|
|
433
433
|
const debugVariable = "KCLOUD_DEBUG";
|
|
434
|
+
const defaultOrigin = "https://cloud.kikimora.dev";
|
|
434
435
|
const globalOptions = {
|
|
435
|
-
profile: { type: "string" },
|
|
436
436
|
url: { type: "string" },
|
|
437
|
+
org: { type: "string" },
|
|
437
438
|
json: {
|
|
438
439
|
type: "boolean",
|
|
439
440
|
default: false
|
|
@@ -449,23 +450,28 @@ const globalOptions = {
|
|
|
449
450
|
}
|
|
450
451
|
};
|
|
451
452
|
const globalOptionsUsage = [
|
|
452
|
-
["--
|
|
453
|
-
["--
|
|
453
|
+
["--url <origin>", `the instance to act on (default: ${defaultOrigin})`],
|
|
454
|
+
["--org <organization>", "the organization to act in, by slug, name or id"],
|
|
454
455
|
["--json", "print machine-readable output"],
|
|
455
456
|
["--yes", "ask nothing; every input comes from a flag, a file or stdin"],
|
|
456
457
|
["-h, --help", "show help"]
|
|
457
458
|
];
|
|
458
459
|
const globalValuesSchema = z.looseObject({
|
|
459
|
-
profile: z.string().optional(),
|
|
460
460
|
url: z.string().optional(),
|
|
461
|
+
org: z.string().optional(),
|
|
461
462
|
json: z.boolean(),
|
|
462
463
|
yes: z.boolean()
|
|
463
464
|
});
|
|
465
|
+
const organizationOf = (given) => {
|
|
466
|
+
if (given === void 0) return null;
|
|
467
|
+
if (given.trim() === "") throw new UsageError("--org needs an organization.", "Name it by its slug, name or id.");
|
|
468
|
+
return given.trim();
|
|
469
|
+
};
|
|
464
470
|
const settingsOf = (values) => {
|
|
465
471
|
const global = globalValuesSchema.parse(values);
|
|
466
472
|
return {
|
|
467
|
-
profile: global.profile ?? null,
|
|
468
473
|
origin: global.url ?? null,
|
|
474
|
+
organization: organizationOf(global.org),
|
|
469
475
|
json: global.json,
|
|
470
476
|
yes: global.yes
|
|
471
477
|
};
|
|
@@ -819,7 +825,8 @@ const workerSettingsSchema = z.strictObject({
|
|
|
819
825
|
//#region ../../packages/contracts/src/common.ts
|
|
820
826
|
const idSchema = z.uuid().toLowerCase();
|
|
821
827
|
const localeSchema = z.enum(["en", "pl"]);
|
|
822
|
-
const
|
|
828
|
+
const organizationRoleSchema = z.enum(["admin", "member"]);
|
|
829
|
+
const projectRoleSchema = z.enum(["admin", "member"]);
|
|
823
830
|
const cursorSchema = z.string().max(512);
|
|
824
831
|
const cursorQuerySchema = z.object({
|
|
825
832
|
cursor: cursorSchema.optional(),
|
|
@@ -925,13 +932,16 @@ const errorCodes = Object.keys({
|
|
|
925
932
|
"user.disabled": "forbidden",
|
|
926
933
|
"user.cannot_modify_self": "rule_violation",
|
|
927
934
|
"user.last_admin": "rule_violation",
|
|
935
|
+
"organization.not_found": "not_found",
|
|
936
|
+
"organization.invalid_slug": "rule_violation",
|
|
937
|
+
"organization.slug_taken": "conflict",
|
|
928
938
|
"project.not_found": "not_found",
|
|
929
939
|
"project.invalid_slug": "rule_violation",
|
|
930
940
|
"project.slug_taken": "conflict",
|
|
931
|
-
"project.member_exists": "conflict",
|
|
932
941
|
"project.docker_not_allowed": "rule_violation",
|
|
933
942
|
"project.member_not_found": "not_found",
|
|
934
943
|
"project.member_must_be_active": "rule_violation",
|
|
944
|
+
"project.not_empty": "rule_violation",
|
|
935
945
|
"credential.not_found": "not_found",
|
|
936
946
|
"credential.label_taken": "conflict",
|
|
937
947
|
"credential.in_use": "conflict",
|
|
@@ -1354,7 +1364,7 @@ const costPointSchema = z.object({
|
|
|
1354
1364
|
costUsd: z.number().min(0),
|
|
1355
1365
|
runs: z.int().nonnegative()
|
|
1356
1366
|
}).meta({ id: "CostPoint" });
|
|
1357
|
-
z.object({
|
|
1367
|
+
const costSeriesSchema = z.object({
|
|
1358
1368
|
items: z.array(costPointSchema),
|
|
1359
1369
|
totalCostUsd: z.number().nonnegative(),
|
|
1360
1370
|
totalRuns: z.int().nonnegative(),
|
|
@@ -1364,6 +1374,12 @@ const costQuerySchema = z.object({
|
|
|
1364
1374
|
from: z.iso.date(),
|
|
1365
1375
|
to: z.iso.date()
|
|
1366
1376
|
});
|
|
1377
|
+
const projectCostSchema = z.object({
|
|
1378
|
+
projectId: idSchema,
|
|
1379
|
+
costUsd: z.number().min(0),
|
|
1380
|
+
runs: z.int().nonnegative()
|
|
1381
|
+
}).meta({ id: "ProjectCost" });
|
|
1382
|
+
costSeriesSchema.extend({ projects: z.array(projectCostSchema) }).meta({ id: "OrganizationCosts" });
|
|
1367
1383
|
pageSchema(z.object({
|
|
1368
1384
|
taskId: z.string(),
|
|
1369
1385
|
title: z.string(),
|
|
@@ -1381,18 +1397,34 @@ costQuerySchema.extend({ cursor: cursorSchema.optional() });
|
|
|
1381
1397
|
const emailSchema = z.email().max(254);
|
|
1382
1398
|
const passwordSchema = z.string().min(12).max(128);
|
|
1383
1399
|
const userStatusSchema = z.enum(["active", "disabled"]);
|
|
1384
|
-
const
|
|
1400
|
+
const organizationNameSchema = z.string().trim().min(2).max(80);
|
|
1401
|
+
const organizationSlugSchema = z.string().regex(slugPattern);
|
|
1402
|
+
const organizationSchema = z.object({
|
|
1385
1403
|
id: idSchema,
|
|
1404
|
+
name: z.string(),
|
|
1405
|
+
slug: organizationSlugSchema,
|
|
1406
|
+
createdAt: z.iso.datetime()
|
|
1407
|
+
}).meta({ id: "Organization" });
|
|
1408
|
+
z.object({ items: z.array(organizationSchema) }).meta({ id: "OrganizationList" });
|
|
1409
|
+
z.object({ organizationId: idSchema });
|
|
1410
|
+
z.object({
|
|
1386
1411
|
organizationId: idSchema,
|
|
1412
|
+
userId: idSchema
|
|
1413
|
+
});
|
|
1414
|
+
const meSchema = z.object({
|
|
1415
|
+
id: idSchema,
|
|
1387
1416
|
email: emailSchema,
|
|
1388
|
-
|
|
1417
|
+
organization: organizationSchema,
|
|
1418
|
+
organizationRole: organizationRoleSchema,
|
|
1419
|
+
platformAdmin: z.boolean(),
|
|
1389
1420
|
locale: localeSchema,
|
|
1390
1421
|
mustChangePassword: z.boolean()
|
|
1391
1422
|
}).meta({ id: "Me" });
|
|
1392
1423
|
const userSchema = z.object({
|
|
1393
1424
|
id: idSchema,
|
|
1394
1425
|
email: emailSchema,
|
|
1395
|
-
|
|
1426
|
+
organizationRole: organizationRoleSchema,
|
|
1427
|
+
platformAdmin: z.boolean(),
|
|
1396
1428
|
status: userStatusSchema,
|
|
1397
1429
|
locale: localeSchema,
|
|
1398
1430
|
mustChangePassword: z.boolean(),
|
|
@@ -1408,20 +1440,33 @@ z.object({
|
|
|
1408
1440
|
newPassword: passwordSchema
|
|
1409
1441
|
}).meta({ id: "ChangePasswordBody" });
|
|
1410
1442
|
z.object({ locale: localeSchema }).meta({ id: "ChangeLocaleBody" });
|
|
1411
|
-
z.object({
|
|
1443
|
+
const createUserBodySchema = z.object({
|
|
1412
1444
|
email: emailSchema,
|
|
1413
|
-
|
|
1445
|
+
organizationRole: organizationRoleSchema,
|
|
1414
1446
|
locale: localeSchema,
|
|
1415
1447
|
initialPassword: passwordSchema.optional()
|
|
1416
1448
|
}).meta({ id: "CreateUserBody" });
|
|
1417
|
-
z.object({
|
|
1449
|
+
const createdUserSchema = z.object({
|
|
1418
1450
|
user: userSchema,
|
|
1419
1451
|
temporaryPassword: z.string().nullable()
|
|
1420
1452
|
}).meta({ id: "CreatedUser" });
|
|
1421
|
-
z.object({
|
|
1453
|
+
z.object({ organizationRole: organizationRoleSchema }).meta({ id: "UpdateUserBody" });
|
|
1454
|
+
z.object({
|
|
1455
|
+
name: organizationNameSchema,
|
|
1456
|
+
slug: organizationSlugSchema.optional(),
|
|
1457
|
+
admin: z.object({
|
|
1458
|
+
email: emailSchema,
|
|
1459
|
+
locale: localeSchema,
|
|
1460
|
+
initialPassword: passwordSchema.optional()
|
|
1461
|
+
})
|
|
1462
|
+
}).meta({ id: "CreateOrganizationBody" });
|
|
1422
1463
|
z.object({ temporaryPassword: z.string() }).meta({ id: "TemporaryPassword" });
|
|
1423
1464
|
z.object({ items: z.array(userSchema) }).meta({ id: "UserList" });
|
|
1424
1465
|
z.object({ id: idSchema });
|
|
1466
|
+
z.object({
|
|
1467
|
+
organization: organizationSchema,
|
|
1468
|
+
admin: createdUserSchema
|
|
1469
|
+
}).meta({ id: "CreatedOrganization" });
|
|
1425
1470
|
const accessTokenNameSchema = z.string().trim().min(1).max(80);
|
|
1426
1471
|
const accessTokenSchema = z.object({
|
|
1427
1472
|
id: idSchema,
|
|
@@ -1481,6 +1526,8 @@ z.discriminatedUnion("status", [cliTokenWaitingSchema, cliTokenReadySchema]).met
|
|
|
1481
1526
|
z.object({ version: z.string() }).meta({ id: "PlatformVersion" });
|
|
1482
1527
|
const auditActionSchema = z.enum([
|
|
1483
1528
|
"platform.bootstrapped",
|
|
1529
|
+
"organization.created",
|
|
1530
|
+
"organization.renamed",
|
|
1484
1531
|
"user.created",
|
|
1485
1532
|
"user.role_changed",
|
|
1486
1533
|
"user.disabled",
|
|
@@ -1495,7 +1542,9 @@ const auditActionSchema = z.enum([
|
|
|
1495
1542
|
"project.created",
|
|
1496
1543
|
"project.updated",
|
|
1497
1544
|
"project.member_added",
|
|
1545
|
+
"project.member_role_changed",
|
|
1498
1546
|
"project.member_removed",
|
|
1547
|
+
"project.deleted",
|
|
1499
1548
|
"project.docker_changed",
|
|
1500
1549
|
"credential.created",
|
|
1501
1550
|
"credential.rotated",
|
|
@@ -1549,6 +1598,7 @@ const auditTargetTypeSchema = z.enum([
|
|
|
1549
1598
|
]);
|
|
1550
1599
|
const auditEntrySchema = z.object({
|
|
1551
1600
|
id: idSchema,
|
|
1601
|
+
organizationId: idSchema.nullable(),
|
|
1552
1602
|
occurredAt: z.iso.datetime(),
|
|
1553
1603
|
actor: z.object({
|
|
1554
1604
|
kind: z.enum(["user", "system"]),
|
|
@@ -1565,6 +1615,7 @@ const auditEntrySchema = z.object({
|
|
|
1565
1615
|
ip: z.string().nullable()
|
|
1566
1616
|
}).meta({ id: "AuditEntry" });
|
|
1567
1617
|
cursorQuerySchema.extend({
|
|
1618
|
+
organizationId: idSchema.optional(),
|
|
1568
1619
|
action: auditActionSchema.optional(),
|
|
1569
1620
|
actorUserId: idSchema.optional(),
|
|
1570
1621
|
from: z.iso.datetime().optional(),
|
|
@@ -1577,6 +1628,7 @@ const slugSchema = z.string().regex(slugPattern);
|
|
|
1577
1628
|
const colorSchema = z.string().regex(hexColorPattern);
|
|
1578
1629
|
const projectSchema = z.object({
|
|
1579
1630
|
id: idSchema,
|
|
1631
|
+
organizationId: idSchema,
|
|
1580
1632
|
name: z.string(),
|
|
1581
1633
|
slug: slugSchema,
|
|
1582
1634
|
description: z.string(),
|
|
@@ -1589,26 +1641,27 @@ const projectSchema = z.object({
|
|
|
1589
1641
|
const memberSchema = z.object({
|
|
1590
1642
|
userId: idSchema,
|
|
1591
1643
|
email: z.email(),
|
|
1592
|
-
role:
|
|
1644
|
+
role: projectRoleSchema,
|
|
1593
1645
|
status: userStatusSchema,
|
|
1594
1646
|
addedAt: z.iso.datetime()
|
|
1595
1647
|
}).meta({ id: "Member" });
|
|
1596
1648
|
projectSchema.extend({ members: z.array(memberSchema) }).meta({ id: "ProjectDetail" });
|
|
1597
1649
|
z.object({ items: z.array(projectSchema) }).meta({ id: "ProjectList" });
|
|
1598
1650
|
z.object({ items: z.array(memberSchema) }).meta({ id: "MemberList" });
|
|
1599
|
-
z.object({
|
|
1651
|
+
const createProjectBodySchema = z.object({
|
|
1600
1652
|
name: z.string().trim().min(2).max(80),
|
|
1601
1653
|
slug: slugSchema.optional(),
|
|
1602
1654
|
description: z.string().max(500),
|
|
1603
1655
|
color: colorSchema
|
|
1604
1656
|
}).meta({ id: "CreateProjectBody" });
|
|
1605
|
-
z.object({
|
|
1657
|
+
const updateProjectBodySchema = z.object({
|
|
1606
1658
|
name: z.string().trim().min(2).max(80),
|
|
1607
1659
|
slug: slugSchema,
|
|
1608
1660
|
description: z.string().max(500),
|
|
1609
1661
|
color: colorSchema
|
|
1610
1662
|
}).meta({ id: "UpdateProjectBody" });
|
|
1611
1663
|
z.object({ allowed: z.boolean() }).meta({ id: "SetProjectDockerBody" });
|
|
1664
|
+
z.object({ role: projectRoleSchema }).meta({ id: "SetProjectMemberBody" });
|
|
1612
1665
|
z.object({ id: idSchema });
|
|
1613
1666
|
z.object({
|
|
1614
1667
|
id: idSchema,
|
|
@@ -2842,10 +2895,90 @@ const createAdapters = (context) => ({
|
|
|
2842
2895
|
clientName: () => `kcloud on ${hostname()}`
|
|
2843
2896
|
});
|
|
2844
2897
|
//#endregion
|
|
2845
|
-
//#region src/
|
|
2898
|
+
//#region src/resolve.ts
|
|
2899
|
+
const organizationHints = { "organization.not_found": "kcloud orgs list shows the organizations you can see." };
|
|
2900
|
+
const isId = (reference) => idSchema.safeParse(reference).success;
|
|
2901
|
+
const once = (load) => {
|
|
2902
|
+
let loaded = null;
|
|
2903
|
+
return () => loaded ??= load();
|
|
2904
|
+
};
|
|
2905
|
+
const slugsOf = (items) => items.map(({ slug }) => slug).join(", ");
|
|
2906
|
+
const organizationIn = (organizations, reference) => {
|
|
2907
|
+
const found = organizations.filter((organization) => organization.id === reference.toLowerCase() || organization.slug === reference || organization.name === reference);
|
|
2908
|
+
const [first] = found;
|
|
2909
|
+
if (first === void 0) throw new CliError(`There is no organization ${reference} you can see.`, organizations.length === 0 ? "kcloud orgs list shows the organizations you can see." : `kcloud orgs list shows ${slugsOf(organizations)}.`);
|
|
2910
|
+
if (found.length > 1) throw new CliError(`${reference} names ${found.length} organizations.`, "Name it by its slug or by its id instead.");
|
|
2911
|
+
return first;
|
|
2912
|
+
};
|
|
2913
|
+
const scopeOf = (api, reference) => {
|
|
2914
|
+
const me = once(async () => dataOf("say who the token belongs to", await api.GET("/auth/me")));
|
|
2915
|
+
const organizations = once(async () => dataOf("list the organizations", await api.GET("/organizations")).items);
|
|
2916
|
+
return {
|
|
2917
|
+
api,
|
|
2918
|
+
me,
|
|
2919
|
+
organizations,
|
|
2920
|
+
organization: once(async () => reference === null ? (await me()).organization : organizationIn(await organizations(), reference))
|
|
2921
|
+
};
|
|
2922
|
+
};
|
|
2923
|
+
const projectsOf = async (scope) => {
|
|
2924
|
+
const { id } = await scope.organization();
|
|
2925
|
+
return dataOf("list the projects", await scope.api.GET("/organizations/{organizationId}/projects", { params: { path: { organizationId: id } } }), organizationHints).items;
|
|
2926
|
+
};
|
|
2927
|
+
const matches = (reference, project) => project.slug === reference || project.name === reference;
|
|
2928
|
+
const resolveProjectId = async (scope, reference) => {
|
|
2929
|
+
if (isId(reference)) return reference;
|
|
2930
|
+
const projects = await projectsOf(scope);
|
|
2931
|
+
const found = projects.filter((project) => matches(reference, project));
|
|
2932
|
+
const [first] = found;
|
|
2933
|
+
if (first === void 0) {
|
|
2934
|
+
const { slug } = await scope.organization();
|
|
2935
|
+
throw new CliError(`There is no project ${reference} you can see in ${slug}.`, projects.length === 0 ? "kcloud projects list shows the projects you are a member of." : `kcloud projects list shows ${slugsOf(projects)}.`);
|
|
2936
|
+
}
|
|
2937
|
+
if (found.length > 1) throw new CliError(`${reference} names ${found.length} projects.`, "Name it by its slug or by its id instead.");
|
|
2938
|
+
return first.id;
|
|
2939
|
+
};
|
|
2940
|
+
const agentsOfProject = async (api, projectId) => dataOf("list the agents of the project", await api.GET("/projects/{id}/agents", { params: { path: { id: projectId } } })).items;
|
|
2941
|
+
const resolveAgentId = async (scope, reference, project) => {
|
|
2942
|
+
if (isId(reference)) return reference;
|
|
2943
|
+
const [left, right] = reference.split("/");
|
|
2944
|
+
const projectReference = right === void 0 ? project : left;
|
|
2945
|
+
const agentReference = right ?? left ?? "";
|
|
2946
|
+
const searched = projectReference === void 0 ? await projectsOf(scope) : [{
|
|
2947
|
+
id: await resolveProjectId(scope, projectReference),
|
|
2948
|
+
slug: projectReference
|
|
2949
|
+
}];
|
|
2950
|
+
const found = [];
|
|
2951
|
+
for (const candidate of searched) for (const agent of await agentsOfProject(scope.api, candidate.id)) if (agent.slug === agentReference || agent.name === agentReference) found.push({
|
|
2952
|
+
agent,
|
|
2953
|
+
project: candidate
|
|
2954
|
+
});
|
|
2955
|
+
const [first] = found;
|
|
2956
|
+
if (first === void 0) throw new CliError(`There is no agent ${reference} you can see.`, "Name it as <project>/<agent>, or pass --project; kcloud agents list shows them.");
|
|
2957
|
+
if (found.length > 1) throw new CliError(`${reference} names an agent in ${found.map(({ project }) => project.slug).join(", ")}.`, "Name it as <project>/<agent> instead.");
|
|
2958
|
+
return first.agent.id;
|
|
2959
|
+
};
|
|
2960
|
+
const sameEmail = (left, right) => left.toLowerCase() === right.toLowerCase();
|
|
2961
|
+
const organizationMembersOf = async (api, organizationId) => dataOf("list the members of the organization", await api.GET("/organizations/{organizationId}/members", { params: { path: { organizationId } } }), organizationHints).items;
|
|
2962
|
+
const resolveOrganizationMember = async (api, organizationId, reference) => {
|
|
2963
|
+
const found = (await organizationMembersOf(api, organizationId)).find((member) => member.id === reference.toLowerCase() || sameEmail(member.email, reference));
|
|
2964
|
+
if (found === void 0) throw new CliError(`The organization has no member ${reference}.`, "kcloud orgs members list shows them.");
|
|
2965
|
+
return found;
|
|
2966
|
+
};
|
|
2967
|
+
//#endregion
|
|
2968
|
+
//#region src/instance/config.ts
|
|
2846
2969
|
const configFileName = "cli.json";
|
|
2847
2970
|
const stateDirectoryName = ".kikimora-cloud";
|
|
2848
|
-
const
|
|
2971
|
+
const configFileOf = (homeDirectory) => join(homeDirectory, stateDirectoryName, configFileName);
|
|
2972
|
+
const instanceSchema = z.strictObject({
|
|
2973
|
+
token: z.string().min(1),
|
|
2974
|
+
tokenId: z.string().min(1).nullable(),
|
|
2975
|
+
email: z.string().min(1).nullable(),
|
|
2976
|
+
organization: z.string().min(1).nullable()
|
|
2977
|
+
});
|
|
2978
|
+
const instanceStoreSchema = z.strictObject({
|
|
2979
|
+
version: z.literal(2),
|
|
2980
|
+
instances: z.record(z.url(), instanceSchema)
|
|
2981
|
+
});
|
|
2849
2982
|
const profileSchema = z.strictObject({
|
|
2850
2983
|
origin: z.url(),
|
|
2851
2984
|
token: z.string().min(1),
|
|
@@ -2857,97 +2990,88 @@ const profileStoreSchema = z.strictObject({
|
|
|
2857
2990
|
current: z.string().min(1).nullable(),
|
|
2858
2991
|
profiles: z.record(z.string().min(1), profileSchema)
|
|
2859
2992
|
});
|
|
2860
|
-
const
|
|
2861
|
-
|
|
2862
|
-
|
|
2863
|
-
|
|
2993
|
+
const storedSchema = z.discriminatedUnion("version", [instanceStoreSchema, profileStoreSchema]);
|
|
2994
|
+
const emptyInstanceStore = {
|
|
2995
|
+
version: 2,
|
|
2996
|
+
instances: {}
|
|
2997
|
+
};
|
|
2998
|
+
const instancesOfProfiles = ({ current, profiles }) => {
|
|
2999
|
+
const currentLast = Object.entries(profiles).toSorted(([left], [right]) => Number(left === current) - Number(right === current));
|
|
3000
|
+
return {
|
|
3001
|
+
version: 2,
|
|
3002
|
+
instances: Object.fromEntries(currentLast.map(([, { origin, token, tokenId, email }]) => [new URL(origin).origin, {
|
|
3003
|
+
token,
|
|
3004
|
+
tokenId,
|
|
3005
|
+
email,
|
|
3006
|
+
organization: null
|
|
3007
|
+
}]))
|
|
3008
|
+
};
|
|
2864
3009
|
};
|
|
2865
|
-
const
|
|
2866
|
-
const path =
|
|
3010
|
+
const readInstanceStore = async (homeDirectory) => {
|
|
3011
|
+
const path = configFileOf(homeDirectory);
|
|
2867
3012
|
const content = await readPrivateFile(path);
|
|
2868
|
-
if (content === null) return
|
|
3013
|
+
if (content === null) return emptyInstanceStore;
|
|
2869
3014
|
let document;
|
|
2870
3015
|
try {
|
|
2871
3016
|
document = JSON.parse(content);
|
|
2872
3017
|
} catch {
|
|
2873
3018
|
throw new CliError(`${path} is not JSON.`, `Fix it or delete it, then log in again.`);
|
|
2874
3019
|
}
|
|
2875
|
-
const parsed =
|
|
3020
|
+
const parsed = storedSchema.safeParse(document);
|
|
2876
3021
|
if (!parsed.success) throw new CliError(`${path} is not what ${configFileName} should hold.`, z.prettifyError(parsed.error));
|
|
2877
|
-
return parsed.data;
|
|
3022
|
+
return parsed.data.version === 2 ? parsed.data : instancesOfProfiles(parsed.data);
|
|
2878
3023
|
};
|
|
2879
|
-
const
|
|
2880
|
-
await writePrivateFile(
|
|
3024
|
+
const writeInstanceStore = async (homeDirectory, store) => {
|
|
3025
|
+
await writePrivateFile(configFileOf(homeDirectory), `${JSON.stringify(store, null, 2)}\n`);
|
|
2881
3026
|
};
|
|
2882
|
-
const
|
|
2883
|
-
version:
|
|
2884
|
-
|
|
2885
|
-
|
|
2886
|
-
|
|
2887
|
-
[name]: profile
|
|
3027
|
+
const withInstance = (store, origin, instance) => ({
|
|
3028
|
+
version: 2,
|
|
3029
|
+
instances: {
|
|
3030
|
+
...store.instances,
|
|
3031
|
+
[origin]: instance
|
|
2888
3032
|
}
|
|
2889
3033
|
});
|
|
2890
|
-
const
|
|
2891
|
-
|
|
2892
|
-
|
|
2893
|
-
|
|
2894
|
-
version: 1,
|
|
2895
|
-
current: store.current === name ? remaining[0] ?? null : store.current,
|
|
2896
|
-
profiles
|
|
2897
|
-
};
|
|
2898
|
-
};
|
|
2899
|
-
const profileNameOf = (origin) => {
|
|
2900
|
-
const { hostname } = new URL(origin);
|
|
2901
|
-
return hostname;
|
|
2902
|
-
};
|
|
3034
|
+
const withoutInstance = (store, origin) => ({
|
|
3035
|
+
version: 2,
|
|
3036
|
+
instances: Object.fromEntries(Object.entries(store.instances).filter(([candidate]) => candidate !== origin))
|
|
3037
|
+
});
|
|
2903
3038
|
//#endregion
|
|
2904
|
-
//#region src/
|
|
3039
|
+
//#region src/instance/session.ts
|
|
2905
3040
|
const urlVariable = "KIKIMORA_CLOUD_URL";
|
|
2906
3041
|
const tokenVariable = "KIKIMORA_CLOUD_TOKEN";
|
|
2907
|
-
const
|
|
2908
|
-
const
|
|
2909
|
-
const
|
|
2910
|
-
|
|
2911
|
-
const known = Object.keys(store.profiles);
|
|
2912
|
-
throw new CliError(`There is no profile ${name} here.`, known.length === 0 ? "Run kcloud login --url <origin> to make one." : `kcloud profiles list shows ${known.join(", ")}.`);
|
|
2913
|
-
}
|
|
2914
|
-
return [name, profile];
|
|
3042
|
+
const organizationVariable = "KIKIMORA_CLOUD_ORG";
|
|
3043
|
+
const givenIn = (environment, name) => {
|
|
3044
|
+
const value = environment[name]?.trim();
|
|
3045
|
+
return value === void 0 || value === "" ? null : value;
|
|
2915
3046
|
};
|
|
2916
|
-
const
|
|
2917
|
-
|
|
2918
|
-
|
|
3047
|
+
const originFor = (settings, context) => originOf(settings.origin ?? givenIn(context.environment, "KIKIMORA_CLOUD_URL") ?? "https://cloud.kikimora.dev");
|
|
3048
|
+
const notSignedIn = (origin, store) => {
|
|
3049
|
+
const known = Object.keys(store.instances);
|
|
3050
|
+
return new CliError(`Not signed in to ${origin}.`, known.length === 0 ? "Run kcloud login (add --url for another instance)." : `Run kcloud login, or add --url for ${known.join(" or ")}.`);
|
|
2919
3051
|
};
|
|
2920
3052
|
const openSession = async ({ settings, context, api = createApi }) => {
|
|
2921
|
-
const
|
|
2922
|
-
const
|
|
2923
|
-
|
|
2924
|
-
|
|
2925
|
-
|
|
2926
|
-
|
|
2927
|
-
|
|
2928
|
-
origin: originOf(origin),
|
|
2929
|
-
token: fromEnvironment
|
|
2930
|
-
}),
|
|
2931
|
-
origin: originOf(origin),
|
|
2932
|
-
profileName: null,
|
|
2933
|
-
store
|
|
2934
|
-
};
|
|
2935
|
-
}
|
|
2936
|
-
const [name, profile] = settings.profile === null ? currentProfileOf(store) : namedProfileOf(store, settings.profile);
|
|
2937
|
-
const origin = originOf(settings.origin ?? profile.origin);
|
|
3053
|
+
const origin = originFor(settings, context);
|
|
3054
|
+
const store = await readInstanceStore(context.homeDirectory);
|
|
3055
|
+
const stored = store.instances[origin] ?? null;
|
|
3056
|
+
const fromEnvironment = givenIn(context.environment, tokenVariable);
|
|
3057
|
+
const token = fromEnvironment ?? stored?.token ?? null;
|
|
3058
|
+
if (token === null) throw notSignedIn(origin, store);
|
|
3059
|
+
const organization = settings.organization ?? givenIn(context.environment, "KIKIMORA_CLOUD_ORG") ?? stored?.organization ?? null;
|
|
2938
3060
|
return {
|
|
2939
|
-
api
|
|
3061
|
+
...scopeOf(api({
|
|
2940
3062
|
origin,
|
|
2941
|
-
token
|
|
2942
|
-
}),
|
|
3063
|
+
token
|
|
3064
|
+
}), organization),
|
|
2943
3065
|
origin,
|
|
2944
|
-
|
|
2945
|
-
|
|
3066
|
+
token,
|
|
3067
|
+
tokenFromEnvironment: fromEnvironment !== null,
|
|
3068
|
+
store,
|
|
3069
|
+
stored
|
|
2946
3070
|
};
|
|
2947
3071
|
};
|
|
2948
3072
|
//#endregion
|
|
2949
3073
|
//#region src/commands/auth/account.ts
|
|
2950
|
-
const passwordLength = `between ${passwordSchema.minLength ?? 0} and ${passwordSchema.maxLength ?? 0} characters`;
|
|
3074
|
+
const passwordLength$1 = `between ${passwordSchema.minLength ?? 0} and ${passwordSchema.maxLength ?? 0} characters`;
|
|
2951
3075
|
const createAuthCommand = (adaptersOf) => defineGroup({
|
|
2952
3076
|
name: "auth",
|
|
2953
3077
|
summary: "Your own account on this instance.",
|
|
@@ -2975,8 +3099,8 @@ const createAuthCommand = (adaptersOf) => defineGroup({
|
|
|
2975
3099
|
validate: (value) => value === "" ? "cannot be empty" : void 0
|
|
2976
3100
|
});
|
|
2977
3101
|
const newPassword = await prompt.password({
|
|
2978
|
-
message: `Your new password, ${passwordLength}`,
|
|
2979
|
-
validate: (value) => passwordSchema.safeParse(value).success ? void 0 : passwordLength
|
|
3102
|
+
message: `Your new password, ${passwordLength$1}`,
|
|
3103
|
+
validate: (value) => passwordSchema.safeParse(value).success ? void 0 : passwordLength$1
|
|
2980
3104
|
});
|
|
2981
3105
|
doneWith("change the password", await session.api.PATCH("/auth/me/password", { body: {
|
|
2982
3106
|
currentPassword,
|
|
@@ -3019,16 +3143,7 @@ const createAuthCommand = (adaptersOf) => defineGroup({
|
|
|
3019
3143
|
});
|
|
3020
3144
|
//#endregion
|
|
3021
3145
|
//#region src/commands/auth/login.ts
|
|
3022
|
-
const usage = "login [--url <origin>] [--with-token]
|
|
3023
|
-
const originFor = async (settings, context) => {
|
|
3024
|
-
const given = settings.origin ?? context.environment["KIKIMORA_CLOUD_URL"];
|
|
3025
|
-
if (given !== void 0 && given !== "") return originOf(given);
|
|
3026
|
-
const store = await readProfileStore(context.homeDirectory);
|
|
3027
|
-
const named = settings.profile ?? store.current;
|
|
3028
|
-
const known = named === null ? void 0 : store.profiles[named];
|
|
3029
|
-
if (known !== void 0) return originOf(known.origin);
|
|
3030
|
-
throw new UsageError("login needs the address of an instance.", `Pass --url <origin> or set ${urlVariable}.`);
|
|
3031
|
-
};
|
|
3146
|
+
const usage = "login [--url <origin>] [--with-token]";
|
|
3032
3147
|
const isTerminal$2 = (stream) => stream.isTTY === true;
|
|
3033
3148
|
const pipedText$2 = async (context) => {
|
|
3034
3149
|
if (isTerminal$2(context.io.stdin)) return "";
|
|
@@ -3037,8 +3152,8 @@ const pipedText$2 = async (context) => {
|
|
|
3037
3152
|
return Buffer.concat(chunks).toString("utf8").trim();
|
|
3038
3153
|
};
|
|
3039
3154
|
const readToken = async (context, settings) => {
|
|
3040
|
-
const fromEnvironment = context.environment
|
|
3041
|
-
if (fromEnvironment !==
|
|
3155
|
+
const fromEnvironment = givenIn(context.environment, tokenVariable);
|
|
3156
|
+
if (fromEnvironment !== null) return fromEnvironment;
|
|
3042
3157
|
const piped = await pipedText$2(context);
|
|
3043
3158
|
if (piped !== "") return piped;
|
|
3044
3159
|
if (settings.yes) throw new UsageError("login --with-token found no token to use.", `Pipe it on stdin or set ${tokenVariable}.`);
|
|
@@ -3046,9 +3161,13 @@ const readToken = async (context, settings) => {
|
|
|
3046
3161
|
};
|
|
3047
3162
|
const createLoginCommand = (adaptersOf) => defineCommand({
|
|
3048
3163
|
name: "login",
|
|
3049
|
-
summary: "Sign in to an instance and save its token
|
|
3164
|
+
summary: "Sign in to an instance and save its token for its address.",
|
|
3050
3165
|
usage,
|
|
3051
|
-
optionsUsage: [
|
|
3166
|
+
optionsUsage: [
|
|
3167
|
+
["--with-token", "take a token from stdin or the environment instead of a browser"],
|
|
3168
|
+
["", `the address comes from --url or ${urlVariable}, or is ${defaultOrigin}`],
|
|
3169
|
+
["", `the token of --with-token comes from stdin or ${tokenVariable}`]
|
|
3170
|
+
],
|
|
3052
3171
|
options: { "with-token": {
|
|
3053
3172
|
type: "boolean",
|
|
3054
3173
|
default: false
|
|
@@ -3059,7 +3178,7 @@ const createLoginCommand = (adaptersOf) => defineCommand({
|
|
|
3059
3178
|
},
|
|
3060
3179
|
run: async ({ values, settings, context }) => {
|
|
3061
3180
|
const adapters = adaptersOf(context);
|
|
3062
|
-
const origin =
|
|
3181
|
+
const origin = originFor(settings, context);
|
|
3063
3182
|
const prompt = adapters.prompter(settings);
|
|
3064
3183
|
const anonymous = adapters.api({
|
|
3065
3184
|
origin,
|
|
@@ -3079,16 +3198,16 @@ const createLoginCommand = (adaptersOf) => defineCommand({
|
|
|
3079
3198
|
const me = dataOf("say who the token belongs to", await identified.GET("/auth/me"));
|
|
3080
3199
|
await save({
|
|
3081
3200
|
context,
|
|
3082
|
-
settings,
|
|
3083
3201
|
origin,
|
|
3084
3202
|
secret,
|
|
3085
3203
|
me
|
|
3086
3204
|
});
|
|
3087
3205
|
const version = await instanceVersion(identified);
|
|
3088
|
-
prompt.outro(`Signed in to ${origin} as ${me.email}${version === null ? "" : ` on ${version}`}.`);
|
|
3206
|
+
prompt.outro(`Signed in to ${origin} as ${me.email} of ${me.organization.name}${version === null ? "" : ` on ${version}`}.`);
|
|
3089
3207
|
if (settings.json) context.terminal.json({
|
|
3090
3208
|
origin,
|
|
3091
3209
|
email: me.email,
|
|
3210
|
+
organization: me.organization.slug,
|
|
3092
3211
|
version
|
|
3093
3212
|
});
|
|
3094
3213
|
return exitCodes.ok;
|
|
@@ -3124,14 +3243,14 @@ const authorizeInBrowser = async ({ adapters, api, origin, prompt }) => {
|
|
|
3124
3243
|
}
|
|
3125
3244
|
}
|
|
3126
3245
|
};
|
|
3127
|
-
const save = async ({ context,
|
|
3128
|
-
const store = await
|
|
3129
|
-
const
|
|
3130
|
-
await
|
|
3131
|
-
origin,
|
|
3246
|
+
const save = async ({ context, origin, secret, me }) => {
|
|
3247
|
+
const store = await readInstanceStore(context.homeDirectory);
|
|
3248
|
+
const previous = store.instances[origin];
|
|
3249
|
+
await writeInstanceStore(context.homeDirectory, withInstance(store, origin, {
|
|
3132
3250
|
token: secret,
|
|
3133
3251
|
tokenId: null,
|
|
3134
|
-
email: me.email
|
|
3252
|
+
email: me.email,
|
|
3253
|
+
organization: previous?.email === me.email ? previous.organization : null
|
|
3135
3254
|
}));
|
|
3136
3255
|
};
|
|
3137
3256
|
const instanceVersion = async (api) => {
|
|
@@ -3140,11 +3259,15 @@ const instanceVersion = async (api) => {
|
|
|
3140
3259
|
};
|
|
3141
3260
|
//#endregion
|
|
3142
3261
|
//#region src/commands/auth/logout.ts
|
|
3262
|
+
const outcomeOf = (session, kept) => {
|
|
3263
|
+
if (session.tokenFromEnvironment) return kept ? `${tokenVariable} signs in to ${session.origin}, so nothing is stored here to forget.` : `Revoked the token of ${tokenVariable} on ${session.origin}.`;
|
|
3264
|
+
return kept ? `Forgot ${session.origin} here. Its token is still usable.` : `Revoked the token of ${session.origin} and forgot it here.`;
|
|
3265
|
+
};
|
|
3143
3266
|
const createLogoutCommand = (adaptersOf) => defineCommand({
|
|
3144
3267
|
name: "logout",
|
|
3145
|
-
summary: "Revoke the token of
|
|
3146
|
-
usage: "logout [--keep-token]",
|
|
3147
|
-
optionsUsage: [["--keep-token", "forget the
|
|
3268
|
+
summary: "Revoke the token of an instance and forget it here.",
|
|
3269
|
+
usage: "logout [--url <origin>] [--keep-token]",
|
|
3270
|
+
optionsUsage: [["--keep-token", "forget the instance here and leave its token usable elsewhere"]],
|
|
3148
3271
|
options: { "keep-token": {
|
|
3149
3272
|
type: "boolean",
|
|
3150
3273
|
default: false
|
|
@@ -3160,12 +3283,10 @@ const createLogoutCommand = (adaptersOf) => defineCommand({
|
|
|
3160
3283
|
context,
|
|
3161
3284
|
api: adapters.api
|
|
3162
3285
|
});
|
|
3163
|
-
|
|
3164
|
-
if (session.
|
|
3165
|
-
|
|
3166
|
-
|
|
3167
|
-
}
|
|
3168
|
-
context.terminal.line(values["keep-token"] ? `Forgot ${session.origin} here; its token is still usable.` : `Revoked the token of ${session.origin} and forgot it here.`);
|
|
3286
|
+
const kept = values["keep-token"];
|
|
3287
|
+
if (!kept) doneWith("revoke the token", await session.api.POST("/auth/logout"));
|
|
3288
|
+
if (!session.tokenFromEnvironment) await writeInstanceStore(context.homeDirectory, withoutInstance(session.store, session.origin));
|
|
3289
|
+
context.terminal.line(outcomeOf(session, kept));
|
|
3169
3290
|
return exitCodes.ok;
|
|
3170
3291
|
}
|
|
3171
3292
|
});
|
|
@@ -3228,9 +3349,10 @@ const createVersionCommand = (adaptersOf, cliVersion) => defineCommand({
|
|
|
3228
3349
|
});
|
|
3229
3350
|
//#endregion
|
|
3230
3351
|
//#region src/commands/auth/whoami.ts
|
|
3352
|
+
const organizationNameOf = ({ name, slug }) => `${name} (${slug})`;
|
|
3231
3353
|
const createWhoamiCommand = (adaptersOf) => defineCommand({
|
|
3232
3354
|
name: "whoami",
|
|
3233
|
-
summary: "Say which instance and
|
|
3355
|
+
summary: "Say which instance, account and organization kcloud acts with.",
|
|
3234
3356
|
usage: "whoami",
|
|
3235
3357
|
options: {},
|
|
3236
3358
|
positionals: {
|
|
@@ -3244,20 +3366,23 @@ const createWhoamiCommand = (adaptersOf) => defineCommand({
|
|
|
3244
3366
|
context,
|
|
3245
3367
|
api: adapters.api
|
|
3246
3368
|
});
|
|
3247
|
-
const me =
|
|
3369
|
+
const me = await session.me();
|
|
3370
|
+
const actsIn = await session.organization();
|
|
3248
3371
|
if (settings.json) {
|
|
3249
3372
|
context.terminal.json({
|
|
3250
3373
|
origin: session.origin,
|
|
3251
|
-
|
|
3252
|
-
|
|
3374
|
+
...me,
|
|
3375
|
+
actsIn
|
|
3253
3376
|
});
|
|
3254
3377
|
return exitCodes.ok;
|
|
3255
3378
|
}
|
|
3256
3379
|
context.terminal.line(renderTable([{ header: "FIELD" }, { header: "VALUE" }], [
|
|
3257
3380
|
["instance", session.origin],
|
|
3258
|
-
["profile", session.profileName ?? "the environment"],
|
|
3259
3381
|
["email", me.email],
|
|
3260
|
-
["
|
|
3382
|
+
["organization", organizationNameOf(me.organization)],
|
|
3383
|
+
["role", me.organizationRole],
|
|
3384
|
+
["platform admin", me.platformAdmin ? "yes" : "no"],
|
|
3385
|
+
["acts in", organizationNameOf(actsIn)],
|
|
3261
3386
|
["locale", me.locale]
|
|
3262
3387
|
]));
|
|
3263
3388
|
return exitCodes.ok;
|
|
@@ -3274,6 +3399,9 @@ const runOf = async (adaptersOf, { settings, context }) => {
|
|
|
3274
3399
|
});
|
|
3275
3400
|
return {
|
|
3276
3401
|
api: session.api,
|
|
3402
|
+
me: session.me,
|
|
3403
|
+
organizations: session.organizations,
|
|
3404
|
+
organization: session.organization,
|
|
3277
3405
|
origin: session.origin,
|
|
3278
3406
|
adapters,
|
|
3279
3407
|
prompt: adapters.prompter(settings),
|
|
@@ -3282,40 +3410,6 @@ const runOf = async (adaptersOf, { settings, context }) => {
|
|
|
3282
3410
|
};
|
|
3283
3411
|
};
|
|
3284
3412
|
//#endregion
|
|
3285
|
-
//#region src/resolve.ts
|
|
3286
|
-
const isId = (reference) => idSchema.safeParse(reference).success;
|
|
3287
|
-
const matches = (reference, project) => project.slug === reference || project.name === reference;
|
|
3288
|
-
const projectsOf = async (api) => dataOf("list the projects", await api.GET("/projects")).items;
|
|
3289
|
-
const resolveProjectId = async (api, reference) => {
|
|
3290
|
-
if (isId(reference)) return reference;
|
|
3291
|
-
const projects = await projectsOf(api);
|
|
3292
|
-
const found = projects.filter((project) => matches(reference, project));
|
|
3293
|
-
const [first] = found;
|
|
3294
|
-
if (first === void 0) throw new CliError(`There is no project ${reference} you can see.`, projects.length === 0 ? "kcloud projects list shows the projects you are a member of." : `kcloud projects list shows ${projects.map(({ slug }) => slug).join(", ")}.`);
|
|
3295
|
-
if (found.length > 1) throw new CliError(`${reference} names ${found.length} projects.`, "Name it by its slug or by its id instead.");
|
|
3296
|
-
return first.id;
|
|
3297
|
-
};
|
|
3298
|
-
const agentsOfProject = async (api, projectId) => dataOf("list the agents of the project", await api.GET("/projects/{id}/agents", { params: { path: { id: projectId } } })).items;
|
|
3299
|
-
const resolveAgentId = async (api, reference, project) => {
|
|
3300
|
-
if (isId(reference)) return reference;
|
|
3301
|
-
const [left, right] = reference.split("/");
|
|
3302
|
-
const projectReference = right === void 0 ? project : left;
|
|
3303
|
-
const agentReference = right ?? left ?? "";
|
|
3304
|
-
const searched = projectReference === void 0 ? await projectsOf(api) : [{
|
|
3305
|
-
id: await resolveProjectId(api, projectReference),
|
|
3306
|
-
slug: projectReference
|
|
3307
|
-
}];
|
|
3308
|
-
const found = [];
|
|
3309
|
-
for (const candidate of searched) for (const agent of await agentsOfProject(api, candidate.id)) if (agent.slug === agentReference || agent.name === agentReference) found.push({
|
|
3310
|
-
agent,
|
|
3311
|
-
project: candidate
|
|
3312
|
-
});
|
|
3313
|
-
const [first] = found;
|
|
3314
|
-
if (first === void 0) throw new CliError(`There is no agent ${reference} you can see.`, "Name it as <project>/<agent>, or pass --project; kcloud agents list shows them.");
|
|
3315
|
-
if (found.length > 1) throw new CliError(`${reference} names an agent in ${found.map(({ project }) => project.slug).join(", ")}.`, "Name it as <project>/<agent> instead.");
|
|
3316
|
-
return first.agent.id;
|
|
3317
|
-
};
|
|
3318
|
-
//#endregion
|
|
3319
3413
|
//#region src/commands/agents/shared.ts
|
|
3320
3414
|
const agentOption = { project: { type: "string" } };
|
|
3321
3415
|
const agentUsage = [["--project <name>", "the project of the agent, when the agent is named alone"]];
|
|
@@ -3328,7 +3422,7 @@ const agentHints = {
|
|
|
3328
3422
|
};
|
|
3329
3423
|
const agentFrom = async (run, reference, project) => {
|
|
3330
3424
|
if (reference === void 0 || reference === "") throw new UsageError("This command needs an agent.", "Name it as <project>/<agent>, or pass --project.");
|
|
3331
|
-
return resolveAgentId(run
|
|
3425
|
+
return resolveAgentId(run, reference, project);
|
|
3332
3426
|
};
|
|
3333
3427
|
//#endregion
|
|
3334
3428
|
//#region src/commands/agents/check.ts
|
|
@@ -3420,17 +3514,17 @@ const agentPlanOf = (agent) => ({
|
|
|
3420
3514
|
});
|
|
3421
3515
|
//#endregion
|
|
3422
3516
|
//#region src/commands/agents/config.ts
|
|
3423
|
-
const agentsOf = async (
|
|
3424
|
-
const projects = project === void 0 ? (await projectsOf(
|
|
3517
|
+
const agentsOf = async (scope, project) => {
|
|
3518
|
+
const projects = project === void 0 ? (await projectsOf(scope)).map(({ id, slug }) => ({
|
|
3425
3519
|
id,
|
|
3426
3520
|
slug
|
|
3427
3521
|
})) : [{
|
|
3428
|
-
id: await resolveProjectId(
|
|
3522
|
+
id: await resolveProjectId(scope, project),
|
|
3429
3523
|
slug: project
|
|
3430
3524
|
}];
|
|
3431
3525
|
const listed = [];
|
|
3432
3526
|
for (const { id, slug } of projects) {
|
|
3433
|
-
const { items } = dataOf("list the agents of the project", await api.GET("/projects/{id}/agents", { params: { path: { id } } }), agentHints);
|
|
3527
|
+
const { items } = dataOf("list the agents of the project", await scope.api.GET("/projects/{id}/agents", { params: { path: { id } } }), agentHints);
|
|
3434
3528
|
for (const agent of items) listed.push({
|
|
3435
3529
|
project: slug,
|
|
3436
3530
|
...agent
|
|
@@ -3463,7 +3557,7 @@ const configCommands = (adaptersOf) => [
|
|
|
3463
3557
|
},
|
|
3464
3558
|
run: async (invocation) => {
|
|
3465
3559
|
const run = await runOf(adaptersOf, invocation);
|
|
3466
|
-
const listed = await agentsOf(run
|
|
3560
|
+
const listed = await agentsOf(run, invocation.values.project);
|
|
3467
3561
|
return printed(run, listed, (items) => items.length === 0 ? "There is no agent here yet; kcloud agents create makes one." : renderTable([
|
|
3468
3562
|
{ header: "PROJECT" },
|
|
3469
3563
|
{ header: "AGENT" },
|
|
@@ -3789,7 +3883,7 @@ const createCommand = (adaptersOf) => defineCommand({
|
|
|
3789
3883
|
const { values } = invocation;
|
|
3790
3884
|
const run = await runOf(adaptersOf, invocation);
|
|
3791
3885
|
if (values.project === void 0) throw new UsageError("agents create needs --project <name>.", "kcloud projects list shows the projects you are a member of.");
|
|
3792
|
-
const projectId = await resolveProjectId(run
|
|
3886
|
+
const projectId = await resolveProjectId(run, values.project);
|
|
3793
3887
|
const body = values.file === void 0 ? await askedPlan(run, projectId, values) : await fileOf(run, values.file);
|
|
3794
3888
|
const agent = dataOf("create the agent", await run.api.POST("/projects/{id}/agents", {
|
|
3795
3889
|
params: { path: { id: projectId } },
|
|
@@ -3897,10 +3991,10 @@ const pageOptions = {
|
|
|
3897
3991
|
};
|
|
3898
3992
|
const allUsage = ["--all", "follow the cursor to the end instead of one page"];
|
|
3899
3993
|
const pageUsage = [["--limit <n>", `how many rows (default: 50)`], allUsage];
|
|
3900
|
-
const dayOf = (date) => date.toISOString().slice(0, 10);
|
|
3994
|
+
const dayOf$1 = (date) => date.toISOString().slice(0, 10);
|
|
3901
3995
|
const costWindowOf = (days, now) => ({
|
|
3902
|
-
from: dayOf(/* @__PURE__ */ new Date(now.getTime() - (days - 1) * 24 * 60 * 6e4)),
|
|
3903
|
-
to: dayOf(now)
|
|
3996
|
+
from: dayOf$1(/* @__PURE__ */ new Date(now.getTime() - (days - 1) * 24 * 60 * 6e4)),
|
|
3997
|
+
to: dayOf$1(now)
|
|
3904
3998
|
});
|
|
3905
3999
|
const windowFrom = (days, now) => {
|
|
3906
4000
|
const span = days === void 0 ? 7 : Number(days);
|
|
@@ -4334,17 +4428,6 @@ const streamFrames = async (request) => {
|
|
|
4334
4428
|
}
|
|
4335
4429
|
};
|
|
4336
4430
|
//#endregion
|
|
4337
|
-
//#region src/profile/token.ts
|
|
4338
|
-
const tokenOf = async ({ settings, context }) => {
|
|
4339
|
-
const fromEnvironment = context.environment[tokenVariable];
|
|
4340
|
-
if (fromEnvironment !== void 0 && fromEnvironment !== "") return fromEnvironment;
|
|
4341
|
-
const store = await readProfileStore(context.homeDirectory);
|
|
4342
|
-
const name = settings.profile ?? store.current;
|
|
4343
|
-
const profile = name === null ? void 0 : store.profiles[name];
|
|
4344
|
-
if (profile === void 0) throw new CliError("No instance is signed in here.", "Run kcloud login --url <origin> first.");
|
|
4345
|
-
return profile.token;
|
|
4346
|
-
};
|
|
4347
|
-
//#endregion
|
|
4348
4431
|
//#region src/commands/agents/streams.ts
|
|
4349
4432
|
const streamOptions = {
|
|
4350
4433
|
...agentOption,
|
|
@@ -4397,15 +4480,12 @@ const streamCommands = (adaptersOf) => [{
|
|
|
4397
4480
|
api: adapters.api
|
|
4398
4481
|
});
|
|
4399
4482
|
const reference = positionals[0] ?? "";
|
|
4400
|
-
const agentId = await resolveAgentId(session
|
|
4483
|
+
const agentId = await resolveAgentId(session, reference, values.project);
|
|
4401
4484
|
const limit = values.limit;
|
|
4402
4485
|
if (limit !== void 0 && !/^[0-9]+$/u.test(limit)) throw new UsageError(`--limit ${limit} is not a whole number.`);
|
|
4403
4486
|
await streamFrames({
|
|
4404
4487
|
origin: session.origin,
|
|
4405
|
-
token:
|
|
4406
|
-
settings,
|
|
4407
|
-
context
|
|
4408
|
-
}),
|
|
4488
|
+
token: session.token,
|
|
4409
4489
|
path: path.replace("{id}", encodeURIComponent(agentId)),
|
|
4410
4490
|
query: {
|
|
4411
4491
|
...values.since === void 0 ? {} : { since: values.since },
|
|
@@ -4601,7 +4681,7 @@ const pipedText = async (stream) => {
|
|
|
4601
4681
|
for await (const chunk of stream) chunks.push(Buffer.from(chunk));
|
|
4602
4682
|
return Buffer.concat(chunks).toString("utf8");
|
|
4603
4683
|
};
|
|
4604
|
-
const secretOf = async ({ run, file, fromStdin = false, question, variable = secretVariable }) => {
|
|
4684
|
+
const secretOf = async ({ run, file, fromStdin = false, question, variable = secretVariable, stdinHint = "Pipe the secret in, or drop --secret-stdin to be asked for it." }) => {
|
|
4605
4685
|
if (file !== void 0) {
|
|
4606
4686
|
const path = resolve(run.context.workingDirectory, file);
|
|
4607
4687
|
try {
|
|
@@ -4611,7 +4691,7 @@ const secretOf = async ({ run, file, fromStdin = false, question, variable = sec
|
|
|
4611
4691
|
}
|
|
4612
4692
|
}
|
|
4613
4693
|
if (fromStdin) {
|
|
4614
|
-
if (isTerminal(run.context.io.stdin)) throw new UsageError("Nothing is piped on stdin.",
|
|
4694
|
+
if (isTerminal(run.context.io.stdin)) throw new UsageError("Nothing is piped on stdin.", stdinHint);
|
|
4615
4695
|
return (await pipedText(run.context.io.stdin)).trim();
|
|
4616
4696
|
}
|
|
4617
4697
|
const fromEnvironment = run.context.environment[variable];
|
|
@@ -4646,7 +4726,7 @@ const credentialHints = {
|
|
|
4646
4726
|
};
|
|
4647
4727
|
const projectFrom$2 = async (run, given) => {
|
|
4648
4728
|
if (given === void 0) throw new UsageError("This command needs a project.", "Pass --project <name>; kcloud projects list shows them.");
|
|
4649
|
-
return resolveProjectId(run
|
|
4729
|
+
return resolveProjectId(run, given);
|
|
4650
4730
|
};
|
|
4651
4731
|
const credentialsOf = async (api, projectId) => dataOf("list the credentials of the project", await api.GET("/projects/{id}/credentials", { params: { path: { id: projectId } } }), credentialHints).items;
|
|
4652
4732
|
const resolveCredentialId = async (api, projectId, reference) => {
|
|
@@ -5020,17 +5100,18 @@ const accountTools = [
|
|
|
5020
5100
|
defineTool({
|
|
5021
5101
|
name: "whoami",
|
|
5022
5102
|
title: "Who am I",
|
|
5023
|
-
description: "Say which instance and
|
|
5103
|
+
description: "Say which instance, account and organization this server acts with, and whether the version of kcloud matches the version of the instance.",
|
|
5024
5104
|
operations: ["getMe", "getPlatformVersion"],
|
|
5025
5105
|
safety: "read",
|
|
5026
5106
|
input: z.object({}),
|
|
5027
5107
|
run: async (_, run) => {
|
|
5028
|
-
const
|
|
5108
|
+
const user = await run.me();
|
|
5109
|
+
const actsIn = await run.organization();
|
|
5029
5110
|
const platform = dataOf("say which version it runs", await run.api.GET("/version"));
|
|
5030
5111
|
return {
|
|
5031
5112
|
origin: run.origin,
|
|
5032
|
-
|
|
5033
|
-
|
|
5113
|
+
user,
|
|
5114
|
+
actsIn,
|
|
5034
5115
|
kcloud: run.kcloudVersion,
|
|
5035
5116
|
instance: platform.version,
|
|
5036
5117
|
matched: platform.version === run.kcloudVersion
|
|
@@ -5084,8 +5165,8 @@ const windowOfPage = ({ limit, all }, fallback = 50) => ({
|
|
|
5084
5165
|
limit: limit ?? fallback,
|
|
5085
5166
|
all
|
|
5086
5167
|
});
|
|
5087
|
-
const projectIdOf = (run, project) => resolveProjectId(run
|
|
5088
|
-
const agentIdOf = (run, { agent, project }) => resolveAgentId(run
|
|
5168
|
+
const projectIdOf = (run, project) => resolveProjectId(run, project);
|
|
5169
|
+
const agentIdOf = (run, { agent, project }) => resolveAgentId(run, agent, project);
|
|
5089
5170
|
//#endregion
|
|
5090
5171
|
//#region src/mcp/tools/agents.ts
|
|
5091
5172
|
const actionOn = async (run, agentId, action, force) => dataOf(`${action} the agent`, await run.api.POST("/agents/{id}/actions/{action}", {
|
|
@@ -5103,7 +5184,7 @@ const agentTools = [
|
|
|
5103
5184
|
operations: ["listAgents"],
|
|
5104
5185
|
safety: "read",
|
|
5105
5186
|
input: z.object({ project: projectField.optional().describe("One project instead of all of them.") }),
|
|
5106
|
-
run: async ({ project }, run) => agentsOf(run
|
|
5187
|
+
run: async ({ project }, run) => agentsOf(run, project)
|
|
5107
5188
|
}),
|
|
5108
5189
|
defineTool({
|
|
5109
5190
|
name: "agent_show",
|
|
@@ -5391,10 +5472,7 @@ const createNotificationsCommand = (adaptersOf) => defineGroup({
|
|
|
5391
5472
|
});
|
|
5392
5473
|
await streamFrames({
|
|
5393
5474
|
origin: session.origin,
|
|
5394
|
-
token:
|
|
5395
|
-
settings,
|
|
5396
|
-
context
|
|
5397
|
-
}),
|
|
5475
|
+
token: session.token,
|
|
5398
5476
|
path: "/notifications/stream",
|
|
5399
5477
|
follow: true,
|
|
5400
5478
|
action: "follow the notifications",
|
|
@@ -5649,150 +5727,685 @@ const observabilityTools = [
|
|
|
5649
5727
|
})
|
|
5650
5728
|
];
|
|
5651
5729
|
//#endregion
|
|
5652
|
-
//#region src/commands/
|
|
5653
|
-
const
|
|
5654
|
-
const
|
|
5655
|
-
const
|
|
5656
|
-
|
|
5657
|
-
|
|
5658
|
-
|
|
5659
|
-
|
|
5660
|
-
"tool.in_use": "Take it off its agents first.",
|
|
5661
|
-
"tool.incomplete": "Every secret of the tool needs a value.",
|
|
5662
|
-
"concurrency.conflict": "Someone changed the tool meanwhile; read it again and retry."
|
|
5663
|
-
};
|
|
5664
|
-
const projectFrom$1 = async (run, given) => {
|
|
5665
|
-
if (given === void 0) throw new UsageError("This command needs a project.", "Pass --project <name>; kcloud projects list shows them.");
|
|
5666
|
-
return resolveProjectId(run.api, given);
|
|
5730
|
+
//#region src/commands/roles.ts
|
|
5731
|
+
const roleOption = { role: { type: "string" } };
|
|
5732
|
+
const roleUsage = ["--role <role>", "admin or member"];
|
|
5733
|
+
const roleFrom = (given, command) => {
|
|
5734
|
+
if (given === void 0) throw new UsageError(`${command} needs --role admin or --role member.`);
|
|
5735
|
+
const parsed = organizationRoleSchema.safeParse(given);
|
|
5736
|
+
if (!parsed.success) throw new UsageError(`--role ${given} is not a role.`, "The roles are admin and member.");
|
|
5737
|
+
return parsed.data;
|
|
5667
5738
|
};
|
|
5668
|
-
const
|
|
5669
|
-
|
|
5670
|
-
|
|
5671
|
-
|
|
5672
|
-
|
|
5673
|
-
|
|
5674
|
-
|
|
5675
|
-
|
|
5739
|
+
const asRole = (role) => role === "admin" ? "an admin" : "a member";
|
|
5740
|
+
//#endregion
|
|
5741
|
+
//#region src/commands/orgs/members.ts
|
|
5742
|
+
const memberHints = {
|
|
5743
|
+
...organizationHints,
|
|
5744
|
+
"user.not_found": "kcloud orgs members list shows them.",
|
|
5745
|
+
"user.email_taken": "An account with that e-mail exists already.",
|
|
5746
|
+
"user.cannot_modify_self": "Another admin of the organization has to make that change.",
|
|
5747
|
+
"user.last_admin": "Make another member an admin first.",
|
|
5748
|
+
"auth.forbidden": "Changing the members of an organization takes an admin of it."
|
|
5676
5749
|
};
|
|
5677
|
-
const
|
|
5678
|
-
|
|
5679
|
-
|
|
5680
|
-
|
|
5750
|
+
const passwordLength = `between ${passwordSchema.minLength ?? 0} and ${passwordSchema.maxLength ?? 0} characters`;
|
|
5751
|
+
const memberPath = (organizationId, userId) => ({ params: { path: {
|
|
5752
|
+
organizationId,
|
|
5753
|
+
userId
|
|
5754
|
+
} } });
|
|
5755
|
+
const memberRoleChanged = async (api, organizationId, userId, role) => dataOf("change the role of the member", await api.PATCH("/organizations/{organizationId}/members/{userId}", {
|
|
5756
|
+
...memberPath(organizationId, userId),
|
|
5757
|
+
body: { organizationRole: role }
|
|
5758
|
+
}), memberHints);
|
|
5759
|
+
const memberDisabled = async (api, organizationId, userId) => dataOf("disable the member", await api.POST("/organizations/{organizationId}/members/{userId}/disable", memberPath(organizationId, userId)), memberHints);
|
|
5760
|
+
const memberEnabled = async (api, organizationId, userId) => dataOf("enable the member", await api.POST("/organizations/{organizationId}/members/{userId}/enable", memberPath(organizationId, userId)), memberHints);
|
|
5761
|
+
const passwordReset = async (api, organizationId, userId) => dataOf("reset the password of the member", await api.POST("/organizations/{organizationId}/members/{userId}/reset-password", memberPath(organizationId, userId)), memberHints);
|
|
5762
|
+
const chosenMember$1 = async (run, reference) => {
|
|
5763
|
+
const organization = await run.organization();
|
|
5764
|
+
return {
|
|
5765
|
+
organization,
|
|
5766
|
+
member: await resolveOrganizationMember(run.api, organization.id, reference)
|
|
5681
5767
|
};
|
|
5682
|
-
validated(updateToolBodySchema, change, "a change to a tool");
|
|
5683
|
-
return dataOf("change the tool", await api.PATCH("/projects/{id}/tools/{toolId}", {
|
|
5684
|
-
params: { path: {
|
|
5685
|
-
id: projectId,
|
|
5686
|
-
toolId: tool.id
|
|
5687
|
-
} },
|
|
5688
|
-
body: change
|
|
5689
|
-
}), toolHints);
|
|
5690
5768
|
};
|
|
5691
|
-
const
|
|
5692
|
-
|
|
5693
|
-
|
|
5694
|
-
|
|
5769
|
+
const localeFrom = (given) => {
|
|
5770
|
+
if (given === void 0) return "en";
|
|
5771
|
+
const parsed = localeSchema.safeParse(given);
|
|
5772
|
+
if (!parsed.success) throw new UsageError(`--locale ${given} is not a language of the panel.`, "The languages are en and pl.");
|
|
5773
|
+
return parsed.data;
|
|
5774
|
+
};
|
|
5775
|
+
const initialPasswordOf = async (run, source) => {
|
|
5776
|
+
const file = source["initial-password-file"];
|
|
5777
|
+
const fromStdin = source["initial-password-stdin"];
|
|
5778
|
+
if (file === void 0 && !fromStdin) return null;
|
|
5779
|
+
const password = await secretOf({
|
|
5780
|
+
run,
|
|
5781
|
+
file,
|
|
5782
|
+
fromStdin,
|
|
5783
|
+
question: "The initial password",
|
|
5784
|
+
stdinHint: "Pipe the password in, or drop --initial-password-stdin."
|
|
5785
|
+
});
|
|
5786
|
+
if (!passwordSchema.safeParse(password).success) throw new UsageError(`The initial password is not ${passwordLength}.`);
|
|
5787
|
+
return password;
|
|
5788
|
+
};
|
|
5789
|
+
const printedPassword = (run, password, note) => {
|
|
5790
|
+
run.context.terminal.line(password);
|
|
5791
|
+
run.context.terminal.error(note);
|
|
5792
|
+
return exitCodes.ok;
|
|
5793
|
+
};
|
|
5794
|
+
const memberUsage = [["", "the member is named by e-mail or id"]];
|
|
5795
|
+
const organizationMembersCommand = (adaptersOf) => defineGroup({
|
|
5796
|
+
name: "members",
|
|
5797
|
+
summary: "The people of the organization kcloud acts in.",
|
|
5798
|
+
usage: "orgs members <command>",
|
|
5695
5799
|
subcommands: [
|
|
5696
5800
|
defineCommand({
|
|
5697
5801
|
name: "list",
|
|
5698
|
-
summary: "List the
|
|
5699
|
-
usage: "
|
|
5700
|
-
|
|
5701
|
-
options: projectOption$1,
|
|
5802
|
+
summary: "List the members of the organization.",
|
|
5803
|
+
usage: "orgs members list",
|
|
5804
|
+
options: {},
|
|
5702
5805
|
positionals: {
|
|
5703
5806
|
min: 0,
|
|
5704
5807
|
max: 0
|
|
5705
5808
|
},
|
|
5706
5809
|
run: async (invocation) => {
|
|
5707
5810
|
const run = await runOf(adaptersOf, invocation);
|
|
5708
|
-
const
|
|
5709
|
-
const items = await
|
|
5710
|
-
return printed(run, items, (
|
|
5711
|
-
{ header: "
|
|
5712
|
-
{ header: "
|
|
5713
|
-
{ header: "
|
|
5714
|
-
{ header: "
|
|
5715
|
-
{
|
|
5716
|
-
header: "AGENTS",
|
|
5717
|
-
align: "right"
|
|
5718
|
-
},
|
|
5811
|
+
const { id } = await run.organization();
|
|
5812
|
+
const items = await organizationMembersOf(run.api, id);
|
|
5813
|
+
return printed(run, items, (members) => renderTable([
|
|
5814
|
+
{ header: "EMAIL" },
|
|
5815
|
+
{ header: "ROLE" },
|
|
5816
|
+
{ header: "STATUS" },
|
|
5817
|
+
{ header: "LAST SIGN-IN" },
|
|
5719
5818
|
{ header: "ID" }
|
|
5720
|
-
],
|
|
5721
|
-
|
|
5722
|
-
|
|
5723
|
-
|
|
5724
|
-
|
|
5725
|
-
|
|
5726
|
-
tool.id
|
|
5819
|
+
], members.map((member) => [
|
|
5820
|
+
member.email,
|
|
5821
|
+
member.platformAdmin ? `${member.organizationRole}, platform admin` : member.organizationRole,
|
|
5822
|
+
member.status,
|
|
5823
|
+
member.lastLoginAt ?? "never",
|
|
5824
|
+
member.id
|
|
5727
5825
|
])));
|
|
5728
5826
|
}
|
|
5729
5827
|
}),
|
|
5730
5828
|
defineCommand({
|
|
5731
|
-
name: "
|
|
5732
|
-
summary: "
|
|
5733
|
-
usage: "
|
|
5734
|
-
optionsUsage:
|
|
5735
|
-
|
|
5736
|
-
|
|
5737
|
-
|
|
5738
|
-
|
|
5739
|
-
|
|
5740
|
-
|
|
5741
|
-
|
|
5742
|
-
|
|
5743
|
-
const tool = await resolveTool(run.api, projectId, invocation.positionals[0] ?? "");
|
|
5744
|
-
return printed(run, tool, (shown) => [
|
|
5745
|
-
renderTable([{ header: "FIELD" }, { header: "VALUE" }], [
|
|
5746
|
-
["name", shown.name],
|
|
5747
|
-
["slug", shown.slug],
|
|
5748
|
-
["kind", shown.kind],
|
|
5749
|
-
["version", String(shown.version)],
|
|
5750
|
-
["complete", shown.complete ? "yes" : "no"],
|
|
5751
|
-
["agents", String(shown.agentsUsing)],
|
|
5752
|
-
["id", shown.id]
|
|
5753
|
-
]),
|
|
5754
|
-
"",
|
|
5755
|
-
JSON.stringify(shown.config, null, 2)
|
|
5756
|
-
].join("\n"));
|
|
5757
|
-
}
|
|
5758
|
-
}),
|
|
5759
|
-
defineCommand({
|
|
5760
|
-
name: "create",
|
|
5761
|
-
summary: "Create a tool from a JSON description of it.",
|
|
5762
|
-
usage: "tools create --project <name> --file <path>",
|
|
5763
|
-
optionsUsage: [...projectUsage$1, ...fileUsage("the tool")],
|
|
5829
|
+
name: "add",
|
|
5830
|
+
summary: "Add a person to the organization, printing a temporary password once.",
|
|
5831
|
+
usage: "orgs members add --email <email> --role <role> [--locale <locale>] [--initial-password-file <path> | --initial-password-stdin]",
|
|
5832
|
+
optionsUsage: [
|
|
5833
|
+
["--email <email>", "the e-mail the person signs in with"],
|
|
5834
|
+
roleUsage,
|
|
5835
|
+
["--locale <locale>", "the language of the panel for them, en or pl (default: en)"],
|
|
5836
|
+
["--initial-password-file <path>", "read the first password from a file"],
|
|
5837
|
+
["--initial-password-stdin", "read the first password from stdin"],
|
|
5838
|
+
["", "a password is never taken from a flag"],
|
|
5839
|
+
["", "without one the instance makes a temporary password and kcloud prints it"]
|
|
5840
|
+
],
|
|
5764
5841
|
options: {
|
|
5765
|
-
|
|
5766
|
-
...
|
|
5842
|
+
email: { type: "string" },
|
|
5843
|
+
...roleOption,
|
|
5844
|
+
locale: { type: "string" },
|
|
5845
|
+
"initial-password-file": { type: "string" },
|
|
5846
|
+
"initial-password-stdin": {
|
|
5847
|
+
type: "boolean",
|
|
5848
|
+
default: false
|
|
5849
|
+
}
|
|
5767
5850
|
},
|
|
5768
5851
|
positionals: {
|
|
5769
5852
|
min: 0,
|
|
5770
5853
|
max: 0
|
|
5771
5854
|
},
|
|
5772
5855
|
run: async (invocation) => {
|
|
5856
|
+
const { values } = invocation;
|
|
5857
|
+
const email = values.email;
|
|
5858
|
+
if (email === void 0) throw new UsageError("orgs members add needs --email <email>.");
|
|
5859
|
+
const role = roleFrom(values.role, "orgs members add");
|
|
5860
|
+
const locale = localeFrom(values.locale);
|
|
5861
|
+
if (values["initial-password-file"] !== void 0 && values["initial-password-stdin"]) throw new UsageError("Pass --initial-password-file or --initial-password-stdin, not both.");
|
|
5773
5862
|
const run = await runOf(adaptersOf, invocation);
|
|
5774
|
-
const
|
|
5775
|
-
const
|
|
5776
|
-
|
|
5777
|
-
|
|
5778
|
-
|
|
5779
|
-
|
|
5780
|
-
|
|
5781
|
-
|
|
5863
|
+
const initialPassword = await initialPasswordOf(run, values);
|
|
5864
|
+
const body = {
|
|
5865
|
+
email,
|
|
5866
|
+
organizationRole: role,
|
|
5867
|
+
locale,
|
|
5868
|
+
...initialPassword === null ? {} : { initialPassword }
|
|
5869
|
+
};
|
|
5870
|
+
validated(createUserBodySchema, body, "a new member");
|
|
5871
|
+
const organization = await run.organization();
|
|
5872
|
+
const created = dataOf("add the member", await run.api.POST("/organizations/{organizationId}/members", {
|
|
5873
|
+
params: { path: { organizationId: organization.id } },
|
|
5782
5874
|
body
|
|
5783
|
-
}),
|
|
5784
|
-
|
|
5875
|
+
}), memberHints);
|
|
5876
|
+
if (run.settings.json) {
|
|
5877
|
+
run.context.terminal.json(created);
|
|
5878
|
+
return exitCodes.ok;
|
|
5879
|
+
}
|
|
5880
|
+
const added = `Added ${created.user.email} to ${organization.slug} as ${asRole(role)}.`;
|
|
5881
|
+
return created.temporaryPassword === null ? said(run, added) : printedPassword(run, created.temporaryPassword, `${added} The temporary password is shown only here.`);
|
|
5785
5882
|
}
|
|
5786
5883
|
}),
|
|
5787
5884
|
defineCommand({
|
|
5788
|
-
name: "
|
|
5789
|
-
summary: "
|
|
5790
|
-
usage: "
|
|
5791
|
-
optionsUsage: [
|
|
5792
|
-
options:
|
|
5793
|
-
|
|
5794
|
-
|
|
5795
|
-
|
|
5885
|
+
name: "set-role",
|
|
5886
|
+
summary: "Make a member an admin of the organization, or a member again.",
|
|
5887
|
+
usage: "orgs members set-role <member> --role <role>",
|
|
5888
|
+
optionsUsage: [roleUsage, ...memberUsage],
|
|
5889
|
+
options: roleOption,
|
|
5890
|
+
positionals: {
|
|
5891
|
+
min: 1,
|
|
5892
|
+
max: 1
|
|
5893
|
+
},
|
|
5894
|
+
run: async (invocation) => {
|
|
5895
|
+
const role = roleFrom(invocation.values.role, "orgs members set-role");
|
|
5896
|
+
const run = await runOf(adaptersOf, invocation);
|
|
5897
|
+
const { organization, member } = await chosenMember$1(run, invocation.positionals[0] ?? "");
|
|
5898
|
+
const user = await memberRoleChanged(run.api, organization.id, member.id, role);
|
|
5899
|
+
return printed(run, user, (shown) => `${shown.email} is ${asRole(shown.organizationRole)} of ${organization.slug} now.`);
|
|
5900
|
+
}
|
|
5901
|
+
}),
|
|
5902
|
+
defineCommand({
|
|
5903
|
+
name: "disable",
|
|
5904
|
+
summary: "Stop a member from signing in, keeping the account.",
|
|
5905
|
+
usage: "orgs members disable <member>",
|
|
5906
|
+
optionsUsage: memberUsage,
|
|
5907
|
+
options: {},
|
|
5908
|
+
positionals: {
|
|
5909
|
+
min: 1,
|
|
5910
|
+
max: 1
|
|
5911
|
+
},
|
|
5912
|
+
run: async (invocation) => {
|
|
5913
|
+
const run = await runOf(adaptersOf, invocation);
|
|
5914
|
+
const { organization, member } = await chosenMember$1(run, invocation.positionals[0] ?? "");
|
|
5915
|
+
await confirmed({
|
|
5916
|
+
prompt: run.prompt,
|
|
5917
|
+
settings: run.settings,
|
|
5918
|
+
question: `Disable ${member.email}? The account cannot sign in until it is enabled again.`
|
|
5919
|
+
});
|
|
5920
|
+
const user = await memberDisabled(run.api, organization.id, member.id);
|
|
5921
|
+
return printed(run, user, (shown) => `Disabled ${shown.email}.`);
|
|
5922
|
+
}
|
|
5923
|
+
}),
|
|
5924
|
+
defineCommand({
|
|
5925
|
+
name: "enable",
|
|
5926
|
+
summary: "Let a disabled member sign in again.",
|
|
5927
|
+
usage: "orgs members enable <member>",
|
|
5928
|
+
optionsUsage: memberUsage,
|
|
5929
|
+
options: {},
|
|
5930
|
+
positionals: {
|
|
5931
|
+
min: 1,
|
|
5932
|
+
max: 1
|
|
5933
|
+
},
|
|
5934
|
+
run: async (invocation) => {
|
|
5935
|
+
const run = await runOf(adaptersOf, invocation);
|
|
5936
|
+
const { organization, member } = await chosenMember$1(run, invocation.positionals[0] ?? "");
|
|
5937
|
+
const user = await memberEnabled(run.api, organization.id, member.id);
|
|
5938
|
+
return printed(run, user, (shown) => `Enabled ${shown.email}.`);
|
|
5939
|
+
}
|
|
5940
|
+
}),
|
|
5941
|
+
defineCommand({
|
|
5942
|
+
name: "reset-password",
|
|
5943
|
+
summary: "Give a member a new temporary password and print it once.",
|
|
5944
|
+
usage: "orgs members reset-password <member>",
|
|
5945
|
+
optionsUsage: memberUsage,
|
|
5946
|
+
options: {},
|
|
5947
|
+
positionals: {
|
|
5948
|
+
min: 1,
|
|
5949
|
+
max: 1
|
|
5950
|
+
},
|
|
5951
|
+
run: async (invocation) => {
|
|
5952
|
+
const run = await runOf(adaptersOf, invocation);
|
|
5953
|
+
const { organization, member } = await chosenMember$1(run, invocation.positionals[0] ?? "");
|
|
5954
|
+
await confirmed({
|
|
5955
|
+
prompt: run.prompt,
|
|
5956
|
+
settings: run.settings,
|
|
5957
|
+
question: `Reset the password of ${member.email}? The current one stops working.`
|
|
5958
|
+
});
|
|
5959
|
+
const reset = await passwordReset(run.api, organization.id, member.id);
|
|
5960
|
+
if (run.settings.json) {
|
|
5961
|
+
run.context.terminal.json(reset);
|
|
5962
|
+
return exitCodes.ok;
|
|
5963
|
+
}
|
|
5964
|
+
return printedPassword(run, reset.temporaryPassword, `The temporary password of ${member.email} is shown only here.`);
|
|
5965
|
+
}
|
|
5966
|
+
})
|
|
5967
|
+
]
|
|
5968
|
+
});
|
|
5969
|
+
//#endregion
|
|
5970
|
+
//#region src/commands/orgs/index.ts
|
|
5971
|
+
const organizationOrInUse = async (scope, reference) => reference === void 0 ? scope.organization() : organizationIn(await scope.organizations(), reference);
|
|
5972
|
+
const organizationsMarked = async (scope, warn) => {
|
|
5973
|
+
const organizations = await scope.organizations();
|
|
5974
|
+
const inUse = await scope.organization().then(({ id }) => id, (error) => {
|
|
5975
|
+
if (!(error instanceof CliError)) throw error;
|
|
5976
|
+
warn(error);
|
|
5977
|
+
return null;
|
|
5978
|
+
});
|
|
5979
|
+
return organizations.map((organization) => ({
|
|
5980
|
+
...organization,
|
|
5981
|
+
inUse: organization.id === inUse
|
|
5982
|
+
}));
|
|
5983
|
+
};
|
|
5984
|
+
const organizationCostsOf = async (api, organizationId, { from, to }) => dataOf("show what the organization spent", await api.GET("/organizations/{organizationId}/costs", { params: {
|
|
5985
|
+
path: { organizationId },
|
|
5986
|
+
query: {
|
|
5987
|
+
from,
|
|
5988
|
+
to
|
|
5989
|
+
}
|
|
5990
|
+
} }), organizationHints);
|
|
5991
|
+
const dayOf = (flag, given) => {
|
|
5992
|
+
if (!z.iso.date().safeParse(given).success) throw new UsageError(`--${flag} ${given} is not a day written as YYYY-MM-DD.`);
|
|
5993
|
+
return given;
|
|
5994
|
+
};
|
|
5995
|
+
const shiftedDay = (day, days) => new Date(Date.parse(`${day}T00:00:00.000Z`) + days * 24 * 60 * 6e4).toISOString().slice(0, 10);
|
|
5996
|
+
const costWindowFrom = (from, to, now) => {
|
|
5997
|
+
const last = to === void 0 ? now.toISOString().slice(0, 10) : dayOf("to", to);
|
|
5998
|
+
const first = from === void 0 ? shiftedDay(last, -6) : dayOf("from", from);
|
|
5999
|
+
if (first > last) throw new UsageError(`--from ${first} comes after --to ${last}.`);
|
|
6000
|
+
return {
|
|
6001
|
+
from: first,
|
|
6002
|
+
to: last
|
|
6003
|
+
};
|
|
6004
|
+
};
|
|
6005
|
+
const costsText = (costs, { from, to }, slugs) => [
|
|
6006
|
+
costs.items.length === 0 ? `The organization spent nothing between ${from} and ${to}.` : renderTable([
|
|
6007
|
+
{ header: "DAY" },
|
|
6008
|
+
{
|
|
6009
|
+
header: "COST",
|
|
6010
|
+
align: "right"
|
|
6011
|
+
},
|
|
6012
|
+
{
|
|
6013
|
+
header: "RUNS",
|
|
6014
|
+
align: "right"
|
|
6015
|
+
}
|
|
6016
|
+
], costs.items.map((point) => [
|
|
6017
|
+
point.day,
|
|
6018
|
+
point.costUsd.toFixed(2),
|
|
6019
|
+
String(point.runs)
|
|
6020
|
+
])),
|
|
6021
|
+
...costs.projects.length === 0 ? [] : ["", renderTable([
|
|
6022
|
+
{ header: "PROJECT" },
|
|
6023
|
+
{
|
|
6024
|
+
header: "COST",
|
|
6025
|
+
align: "right"
|
|
6026
|
+
},
|
|
6027
|
+
{
|
|
6028
|
+
header: "RUNS",
|
|
6029
|
+
align: "right"
|
|
6030
|
+
}
|
|
6031
|
+
], costs.projects.map((project) => [
|
|
6032
|
+
slugs.get(project.projectId) ?? project.projectId,
|
|
6033
|
+
project.costUsd.toFixed(2),
|
|
6034
|
+
String(project.runs)
|
|
6035
|
+
]))],
|
|
6036
|
+
"",
|
|
6037
|
+
`${costs.totalCostUsd.toFixed(2)} over ${costs.totalRuns} runs and ${costs.sessions} sessions.`
|
|
6038
|
+
].join("\n");
|
|
6039
|
+
const createOrgsCommand = (adaptersOf) => defineGroup({
|
|
6040
|
+
name: "orgs",
|
|
6041
|
+
summary: "The organizations you can see, and the one kcloud acts in.",
|
|
6042
|
+
usage: "orgs <command>",
|
|
6043
|
+
subcommands: [
|
|
6044
|
+
defineCommand({
|
|
6045
|
+
name: "list",
|
|
6046
|
+
summary: "List the organizations you can see, marking the one kcloud acts in.",
|
|
6047
|
+
usage: "orgs list",
|
|
6048
|
+
options: {},
|
|
6049
|
+
positionals: {
|
|
6050
|
+
min: 0,
|
|
6051
|
+
max: 0
|
|
6052
|
+
},
|
|
6053
|
+
run: async (invocation) => {
|
|
6054
|
+
const run = await runOf(adaptersOf, invocation);
|
|
6055
|
+
const items = await organizationsMarked(run, (problem) => {
|
|
6056
|
+
run.context.terminal.error(run.context.terminal.errorStyles.warning(`warning ${problem.message}`));
|
|
6057
|
+
});
|
|
6058
|
+
return printed(run, items, (listed) => listed.length === 0 ? "You can see no organization." : renderTable([
|
|
6059
|
+
{ header: "SLUG" },
|
|
6060
|
+
{ header: "NAME" },
|
|
6061
|
+
{ header: "ID" }
|
|
6062
|
+
], listed.map((organization) => [
|
|
6063
|
+
organization.inUse ? `${organization.slug} *` : organization.slug,
|
|
6064
|
+
organization.name,
|
|
6065
|
+
organization.id
|
|
6066
|
+
])));
|
|
6067
|
+
}
|
|
6068
|
+
}),
|
|
6069
|
+
defineCommand({
|
|
6070
|
+
name: "use",
|
|
6071
|
+
summary: "Act in an organization on this instance from now on.",
|
|
6072
|
+
usage: "orgs use <organization> | orgs use --clear",
|
|
6073
|
+
optionsUsage: [["--clear", "act in your own organization again"], ["", `--org and ${organizationVariable} still win over the one stored here`]],
|
|
6074
|
+
options: { clear: {
|
|
6075
|
+
type: "boolean",
|
|
6076
|
+
default: false
|
|
6077
|
+
} },
|
|
6078
|
+
positionals: {
|
|
6079
|
+
min: 0,
|
|
6080
|
+
max: 1
|
|
6081
|
+
},
|
|
6082
|
+
run: async ({ values, positionals, settings, context }) => {
|
|
6083
|
+
const [reference] = positionals;
|
|
6084
|
+
if (values.clear === (reference !== void 0)) throw new UsageError("orgs use needs an organization or --clear, and not both.", "kcloud orgs list shows the organizations you can see.");
|
|
6085
|
+
const adapters = adaptersOf(context);
|
|
6086
|
+
const session = await openSession({
|
|
6087
|
+
settings,
|
|
6088
|
+
context,
|
|
6089
|
+
api: adapters.api
|
|
6090
|
+
});
|
|
6091
|
+
if (session.stored === null) throw new CliError(`${tokenVariable} signs in to ${session.origin}, so nothing is stored here to keep the organization with.`, `Pass --org or set ${organizationVariable} instead.`);
|
|
6092
|
+
const organization = reference === void 0 ? null : organizationIn(await session.organizations(), reference);
|
|
6093
|
+
await writeInstanceStore(context.homeDirectory, withInstance(session.store, session.origin, {
|
|
6094
|
+
...session.stored,
|
|
6095
|
+
organization: organization?.slug ?? null
|
|
6096
|
+
}));
|
|
6097
|
+
if (givenIn(context.environment, "KIKIMORA_CLOUD_ORG") !== null) context.terminal.error(context.terminal.errorStyles.warning(`warning ${organizationVariable} still wins over it in this environment.`));
|
|
6098
|
+
return printed({
|
|
6099
|
+
settings,
|
|
6100
|
+
context
|
|
6101
|
+
}, {
|
|
6102
|
+
origin: session.origin,
|
|
6103
|
+
organization
|
|
6104
|
+
}, ({ origin }) => organization === null ? `kcloud acts in your own organization on ${origin} again.` : `kcloud acts in ${organizationNameOf(organization)} on ${origin} from now on.`);
|
|
6105
|
+
}
|
|
6106
|
+
}),
|
|
6107
|
+
defineCommand({
|
|
6108
|
+
name: "show",
|
|
6109
|
+
summary: "Show an organization, by default the one kcloud acts in.",
|
|
6110
|
+
usage: "orgs show [<organization>]",
|
|
6111
|
+
options: {},
|
|
6112
|
+
positionals: {
|
|
6113
|
+
min: 0,
|
|
6114
|
+
max: 1
|
|
6115
|
+
},
|
|
6116
|
+
run: async (invocation) => {
|
|
6117
|
+
const run = await runOf(adaptersOf, invocation);
|
|
6118
|
+
const { id } = await organizationOrInUse(run, invocation.positionals[0]);
|
|
6119
|
+
const organization = dataOf("describe the organization", await run.api.GET("/organizations/{organizationId}", { params: { path: { organizationId: id } } }), organizationHints);
|
|
6120
|
+
return printed(run, organization, (shown) => renderTable([{ header: "FIELD" }, { header: "VALUE" }], [
|
|
6121
|
+
["name", shown.name],
|
|
6122
|
+
["slug", shown.slug],
|
|
6123
|
+
["created", shown.createdAt],
|
|
6124
|
+
["id", shown.id]
|
|
6125
|
+
]));
|
|
6126
|
+
}
|
|
6127
|
+
}),
|
|
6128
|
+
defineCommand({
|
|
6129
|
+
name: "costs",
|
|
6130
|
+
summary: "Show what the organization spent, by day and by project.",
|
|
6131
|
+
usage: "orgs costs [--from <day>] [--to <day>]",
|
|
6132
|
+
optionsUsage: [["--from <day>", `the first day, as YYYY-MM-DD (default: 7 days up to --to)`], ["--to <day>", "the last day, as YYYY-MM-DD (default: today)"]],
|
|
6133
|
+
options: {
|
|
6134
|
+
from: { type: "string" },
|
|
6135
|
+
to: { type: "string" }
|
|
6136
|
+
},
|
|
6137
|
+
positionals: {
|
|
6138
|
+
min: 0,
|
|
6139
|
+
max: 0
|
|
6140
|
+
},
|
|
6141
|
+
run: async (invocation) => {
|
|
6142
|
+
const { values } = invocation;
|
|
6143
|
+
const run = await runOf(adaptersOf, invocation);
|
|
6144
|
+
const window = costWindowFrom(values.from, values.to, run.adapters.now());
|
|
6145
|
+
const organization = await run.organization();
|
|
6146
|
+
const costs = await organizationCostsOf(run.api, organization.id, window);
|
|
6147
|
+
if (run.settings.json) {
|
|
6148
|
+
run.context.terminal.json(costs);
|
|
6149
|
+
return exitCodes.ok;
|
|
6150
|
+
}
|
|
6151
|
+
const slugs = new Map(costs.projects.length === 0 ? [] : (await projectsOf(run)).map(({ id, slug }) => [id, slug]));
|
|
6152
|
+
run.context.terminal.line(costsText(costs, window, slugs));
|
|
6153
|
+
return exitCodes.ok;
|
|
6154
|
+
}
|
|
6155
|
+
}),
|
|
6156
|
+
organizationMembersCommand(adaptersOf)
|
|
6157
|
+
]
|
|
6158
|
+
});
|
|
6159
|
+
//#endregion
|
|
6160
|
+
//#region src/mcp/tools/organizations.ts
|
|
6161
|
+
const memberField = z.string().trim().min(1).describe("The member, by e-mail or id, from organization_members_list.");
|
|
6162
|
+
const dayField = z.iso.date();
|
|
6163
|
+
const chosenMember = async (run, member) => {
|
|
6164
|
+
const { id } = await run.organization();
|
|
6165
|
+
return {
|
|
6166
|
+
organizationId: id,
|
|
6167
|
+
userId: (await resolveOrganizationMember(run.api, id, member)).id
|
|
6168
|
+
};
|
|
6169
|
+
};
|
|
6170
|
+
const organizationTools = [
|
|
6171
|
+
defineTool({
|
|
6172
|
+
name: "organizations_list",
|
|
6173
|
+
title: "List organizations",
|
|
6174
|
+
description: "List the organizations you can see, marking the one this server acts in. A member sees one, a platform admin sees all.",
|
|
6175
|
+
operations: ["listOrganizations"],
|
|
6176
|
+
safety: "read",
|
|
6177
|
+
input: z.object({}),
|
|
6178
|
+
run: (_, run) => organizationsMarked(run, () => void 0)
|
|
6179
|
+
}),
|
|
6180
|
+
defineTool({
|
|
6181
|
+
name: "organization_show",
|
|
6182
|
+
title: "Show an organization",
|
|
6183
|
+
description: "Show an organization, by default the one this server acts in.",
|
|
6184
|
+
operations: ["getOrganization"],
|
|
6185
|
+
safety: "read",
|
|
6186
|
+
input: z.object({ organization: z.string().trim().min(1).optional().describe("The organization, by its slug, its name or its id.") }),
|
|
6187
|
+
run: async ({ organization }, run) => {
|
|
6188
|
+
const { id } = await organizationOrInUse(run, organization);
|
|
6189
|
+
return dataOf("describe the organization", await run.api.GET("/organizations/{organizationId}", { params: { path: { organizationId: id } } }), organizationHints);
|
|
6190
|
+
}
|
|
6191
|
+
}),
|
|
6192
|
+
defineTool({
|
|
6193
|
+
name: "organization_costs",
|
|
6194
|
+
title: "Show what the organization spent",
|
|
6195
|
+
description: "Show what the organization this server acts in spent, by day and by project, over the last seven days unless from and to say otherwise.",
|
|
6196
|
+
operations: ["getOrganizationCosts"],
|
|
6197
|
+
safety: "read",
|
|
6198
|
+
input: z.object({
|
|
6199
|
+
from: dayField.optional().describe("The first day, as YYYY-MM-DD."),
|
|
6200
|
+
to: dayField.optional().describe("The last day, as YYYY-MM-DD, today by default.")
|
|
6201
|
+
}),
|
|
6202
|
+
run: async ({ from, to }, run) => {
|
|
6203
|
+
const { id } = await run.organization();
|
|
6204
|
+
return organizationCostsOf(run.api, id, costWindowFrom(from, to, run.now()));
|
|
6205
|
+
}
|
|
6206
|
+
}),
|
|
6207
|
+
defineTool({
|
|
6208
|
+
name: "organization_members_list",
|
|
6209
|
+
title: "List the members of the organization",
|
|
6210
|
+
description: "List the people of the organization this server acts in, with their role and status.",
|
|
6211
|
+
operations: ["listOrganizationMembers"],
|
|
6212
|
+
safety: "read",
|
|
6213
|
+
input: z.object({}),
|
|
6214
|
+
run: async (_, run) => organizationMembersOf(run.api, (await run.organization()).id)
|
|
6215
|
+
}),
|
|
6216
|
+
defineTool({
|
|
6217
|
+
name: "organization_member_set_role",
|
|
6218
|
+
title: "Set the role of a member",
|
|
6219
|
+
description: "Make a member an admin of the organization, or a member again. It takes an admin of the organization.",
|
|
6220
|
+
operations: ["updateOrganizationMember"],
|
|
6221
|
+
safety: "write",
|
|
6222
|
+
idempotent: true,
|
|
6223
|
+
input: z.object({
|
|
6224
|
+
member: memberField,
|
|
6225
|
+
role: organizationRoleSchema
|
|
6226
|
+
}),
|
|
6227
|
+
run: async ({ member, role }, run) => {
|
|
6228
|
+
const { organizationId, userId } = await chosenMember(run, member);
|
|
6229
|
+
return memberRoleChanged(run.api, organizationId, userId, role);
|
|
6230
|
+
}
|
|
6231
|
+
}),
|
|
6232
|
+
defineTool({
|
|
6233
|
+
name: "organization_member_disable",
|
|
6234
|
+
title: "Disable a member",
|
|
6235
|
+
description: "Stop a member from signing in, keeping the account. It takes an admin of the organization.",
|
|
6236
|
+
operations: ["disableOrganizationMember"],
|
|
6237
|
+
safety: "destructive",
|
|
6238
|
+
input: z.object({ member: memberField }),
|
|
6239
|
+
run: async ({ member }, run) => {
|
|
6240
|
+
const { organizationId, userId } = await chosenMember(run, member);
|
|
6241
|
+
return memberDisabled(run.api, organizationId, userId);
|
|
6242
|
+
}
|
|
6243
|
+
}),
|
|
6244
|
+
defineTool({
|
|
6245
|
+
name: "organization_member_enable",
|
|
6246
|
+
title: "Enable a member",
|
|
6247
|
+
description: "Let a disabled member sign in again. It takes an admin of the organization.",
|
|
6248
|
+
operations: ["enableOrganizationMember"],
|
|
6249
|
+
safety: "write",
|
|
6250
|
+
input: z.object({ member: memberField }),
|
|
6251
|
+
run: async ({ member }, run) => {
|
|
6252
|
+
const { organizationId, userId } = await chosenMember(run, member);
|
|
6253
|
+
return memberEnabled(run.api, organizationId, userId);
|
|
6254
|
+
}
|
|
6255
|
+
})
|
|
6256
|
+
];
|
|
6257
|
+
//#endregion
|
|
6258
|
+
//#region src/commands/tools.ts
|
|
6259
|
+
const projectOption$1 = { project: { type: "string" } };
|
|
6260
|
+
const projectUsage$1 = [["--project <name>", "the project the tool belongs to, by slug, name or id"]];
|
|
6261
|
+
const toolHints = {
|
|
6262
|
+
"project.not_found": "kcloud projects list shows the projects you are a member of.",
|
|
6263
|
+
"tool.not_found": "kcloud tools list --project <name> shows them.",
|
|
6264
|
+
"tool.name_taken": "Another tool of the project has that name.",
|
|
6265
|
+
"tool.slug_taken": "Another tool of the project has that slug.",
|
|
6266
|
+
"tool.in_use": "Take it off its agents first.",
|
|
6267
|
+
"tool.incomplete": "Every secret of the tool needs a value.",
|
|
6268
|
+
"concurrency.conflict": "Someone changed the tool meanwhile; read it again and retry."
|
|
6269
|
+
};
|
|
6270
|
+
const projectFrom$1 = async (run, given) => {
|
|
6271
|
+
if (given === void 0) throw new UsageError("This command needs a project.", "Pass --project <name>; kcloud projects list shows them.");
|
|
6272
|
+
return resolveProjectId(run, given);
|
|
6273
|
+
};
|
|
6274
|
+
const toolsOf = async (api, projectId) => dataOf("list the tools of the project", await api.GET("/projects/{id}/tools", { params: { path: { id: projectId } } }), toolHints).items;
|
|
6275
|
+
const resolveTool = async (api, projectId, reference) => {
|
|
6276
|
+
const tools = await toolsOf(api, projectId);
|
|
6277
|
+
const found = tools.filter((tool) => tool.id === reference || tool.slug === reference || tool.name === reference);
|
|
6278
|
+
const [first] = found;
|
|
6279
|
+
if (first === void 0) throw new CliError(`The project has no tool ${reference}.`, tools.length === 0 ? "kcloud tools create --file <path> makes one." : `Its tools are ${tools.map(({ slug }) => slug).join(", ")}.`);
|
|
6280
|
+
if (found.length > 1) throw new CliError(`${reference} names ${found.length} tools of the project.`, "Name it by its slug or its id instead.");
|
|
6281
|
+
return first;
|
|
6282
|
+
};
|
|
6283
|
+
const changedTool = async (api, projectId, tool, document) => {
|
|
6284
|
+
const change = typeof document === "object" && document !== null && "version" in document ? document : {
|
|
6285
|
+
...document,
|
|
6286
|
+
version: tool.version
|
|
6287
|
+
};
|
|
6288
|
+
validated(updateToolBodySchema, change, "a change to a tool");
|
|
6289
|
+
return dataOf("change the tool", await api.PATCH("/projects/{id}/tools/{toolId}", {
|
|
6290
|
+
params: { path: {
|
|
6291
|
+
id: projectId,
|
|
6292
|
+
toolId: tool.id
|
|
6293
|
+
} },
|
|
6294
|
+
body: change
|
|
6295
|
+
}), toolHints);
|
|
6296
|
+
};
|
|
6297
|
+
const copiedTool = async (api, projectId, toolId, targetProjectId) => dataOf("copy the tool", await api.POST("/projects/{id}/tools/{toolId}/copy", {
|
|
6298
|
+
params: { path: {
|
|
6299
|
+
id: projectId,
|
|
6300
|
+
toolId
|
|
6301
|
+
} },
|
|
6302
|
+
body: { targetProjectId }
|
|
6303
|
+
}), toolHints);
|
|
6304
|
+
const createToolsCommand = (adaptersOf) => defineGroup({
|
|
6305
|
+
name: "tools",
|
|
6306
|
+
summary: "The HTTP APIs and MCP servers a project offers its agents.",
|
|
6307
|
+
usage: "tools <command>",
|
|
6308
|
+
subcommands: [
|
|
6309
|
+
defineCommand({
|
|
6310
|
+
name: "list",
|
|
6311
|
+
summary: "List the tools of a project.",
|
|
6312
|
+
usage: "tools list --project <name>",
|
|
6313
|
+
optionsUsage: projectUsage$1,
|
|
6314
|
+
options: projectOption$1,
|
|
6315
|
+
positionals: {
|
|
6316
|
+
min: 0,
|
|
6317
|
+
max: 0
|
|
6318
|
+
},
|
|
6319
|
+
run: async (invocation) => {
|
|
6320
|
+
const run = await runOf(adaptersOf, invocation);
|
|
6321
|
+
const projectId = await projectFrom$1(run, invocation.values.project);
|
|
6322
|
+
const items = await toolsOf(run.api, projectId);
|
|
6323
|
+
return printed(run, items, (tools) => tools.length === 0 ? "The project offers no tool yet." : renderTable([
|
|
6324
|
+
{ header: "SLUG" },
|
|
6325
|
+
{ header: "NAME" },
|
|
6326
|
+
{ header: "KIND" },
|
|
6327
|
+
{ header: "SECRETS" },
|
|
6328
|
+
{
|
|
6329
|
+
header: "AGENTS",
|
|
6330
|
+
align: "right"
|
|
6331
|
+
},
|
|
6332
|
+
{ header: "ID" }
|
|
6333
|
+
], tools.map((tool) => [
|
|
6334
|
+
tool.slug,
|
|
6335
|
+
tool.name,
|
|
6336
|
+
tool.kind,
|
|
6337
|
+
tool.complete ? "complete" : "incomplete",
|
|
6338
|
+
String(tool.agentsUsing),
|
|
6339
|
+
tool.id
|
|
6340
|
+
])));
|
|
6341
|
+
}
|
|
6342
|
+
}),
|
|
6343
|
+
defineCommand({
|
|
6344
|
+
name: "show",
|
|
6345
|
+
summary: "Show one tool with its configuration.",
|
|
6346
|
+
usage: "tools show <tool> --project <name>",
|
|
6347
|
+
optionsUsage: projectUsage$1,
|
|
6348
|
+
options: projectOption$1,
|
|
6349
|
+
positionals: {
|
|
6350
|
+
min: 1,
|
|
6351
|
+
max: 1
|
|
6352
|
+
},
|
|
6353
|
+
run: async (invocation) => {
|
|
6354
|
+
const run = await runOf(adaptersOf, invocation);
|
|
6355
|
+
const projectId = await projectFrom$1(run, invocation.values.project);
|
|
6356
|
+
const tool = await resolveTool(run.api, projectId, invocation.positionals[0] ?? "");
|
|
6357
|
+
return printed(run, tool, (shown) => [
|
|
6358
|
+
renderTable([{ header: "FIELD" }, { header: "VALUE" }], [
|
|
6359
|
+
["name", shown.name],
|
|
6360
|
+
["slug", shown.slug],
|
|
6361
|
+
["kind", shown.kind],
|
|
6362
|
+
["version", String(shown.version)],
|
|
6363
|
+
["complete", shown.complete ? "yes" : "no"],
|
|
6364
|
+
["agents", String(shown.agentsUsing)],
|
|
6365
|
+
["id", shown.id]
|
|
6366
|
+
]),
|
|
6367
|
+
"",
|
|
6368
|
+
JSON.stringify(shown.config, null, 2)
|
|
6369
|
+
].join("\n"));
|
|
6370
|
+
}
|
|
6371
|
+
}),
|
|
6372
|
+
defineCommand({
|
|
6373
|
+
name: "create",
|
|
6374
|
+
summary: "Create a tool from a JSON description of it.",
|
|
6375
|
+
usage: "tools create --project <name> --file <path>",
|
|
6376
|
+
optionsUsage: [...projectUsage$1, ...fileUsage("the tool")],
|
|
6377
|
+
options: {
|
|
6378
|
+
...projectOption$1,
|
|
6379
|
+
...fileOptions
|
|
6380
|
+
},
|
|
6381
|
+
positionals: {
|
|
6382
|
+
min: 0,
|
|
6383
|
+
max: 0
|
|
6384
|
+
},
|
|
6385
|
+
run: async (invocation) => {
|
|
6386
|
+
const run = await runOf(adaptersOf, invocation);
|
|
6387
|
+
const projectId = await projectFrom$1(run, invocation.values.project);
|
|
6388
|
+
const file = invocation.values.file;
|
|
6389
|
+
if (file === void 0) throw new UsageError("tools create needs --file <path>.", "kcloud tools show <tool> --json prints the shape of a tool.");
|
|
6390
|
+
const document = await documentOf(run, file);
|
|
6391
|
+
validated(createToolBodySchema, document, "a tool");
|
|
6392
|
+
const body = document;
|
|
6393
|
+
const tool = dataOf("create the tool", await run.api.POST("/projects/{id}/tools", {
|
|
6394
|
+
params: { path: { id: projectId } },
|
|
6395
|
+
body
|
|
6396
|
+
}), toolHints);
|
|
6397
|
+
return printed(run, tool, (created) => `Created ${created.name} (${created.slug}) as ${created.id}.`);
|
|
6398
|
+
}
|
|
6399
|
+
}),
|
|
6400
|
+
defineCommand({
|
|
6401
|
+
name: "update",
|
|
6402
|
+
summary: "Change a tool from a JSON description of the change.",
|
|
6403
|
+
usage: "tools update <tool> --project <name> --file <path>",
|
|
6404
|
+
optionsUsage: [...projectUsage$1, ...fileUsage("the change")],
|
|
6405
|
+
options: {
|
|
6406
|
+
...projectOption$1,
|
|
6407
|
+
...fileOptions
|
|
6408
|
+
},
|
|
5796
6409
|
positionals: {
|
|
5797
6410
|
min: 1,
|
|
5798
6411
|
max: 1
|
|
@@ -5833,6 +6446,31 @@ const createToolsCommand = (adaptersOf) => defineGroup({
|
|
|
5833
6446
|
} } }), toolHints);
|
|
5834
6447
|
return said(run, `Deleted the tool ${tool.name}.`);
|
|
5835
6448
|
}
|
|
6449
|
+
}),
|
|
6450
|
+
defineCommand({
|
|
6451
|
+
name: "copy",
|
|
6452
|
+
summary: "Copy a tool to another project, without its secrets.",
|
|
6453
|
+
usage: "tools copy <tool> --project <name> --to <project>",
|
|
6454
|
+
optionsUsage: [...projectUsage$1, ["--to <project>", "the project that receives the copy, by slug, name or id"]],
|
|
6455
|
+
options: {
|
|
6456
|
+
...projectOption$1,
|
|
6457
|
+
to: { type: "string" }
|
|
6458
|
+
},
|
|
6459
|
+
positionals: {
|
|
6460
|
+
min: 1,
|
|
6461
|
+
max: 1
|
|
6462
|
+
},
|
|
6463
|
+
run: async (invocation) => {
|
|
6464
|
+
const { values } = invocation;
|
|
6465
|
+
const target = values.to;
|
|
6466
|
+
if (target === void 0) throw new UsageError("tools copy needs --to <project>.", "kcloud projects list shows the projects.");
|
|
6467
|
+
const run = await runOf(adaptersOf, invocation);
|
|
6468
|
+
const projectId = await projectFrom$1(run, values.project);
|
|
6469
|
+
const tool = await resolveTool(run.api, projectId, invocation.positionals[0] ?? "");
|
|
6470
|
+
const targetProjectId = await resolveProjectId(run, target);
|
|
6471
|
+
const copied = await copiedTool(run.api, projectId, tool.id, targetProjectId);
|
|
6472
|
+
return printed(run, copied, (shown) => [`Copied ${tool.name} to ${target} as ${shown.id}.`, ...shown.complete ? [] : ["Its secrets stay behind. kcloud tools update --file sets them."]].join(" "));
|
|
6473
|
+
}
|
|
5836
6474
|
})
|
|
5837
6475
|
]
|
|
5838
6476
|
});
|
|
@@ -5906,6 +6544,23 @@ const projectToolTools = [
|
|
|
5906
6544
|
});
|
|
5907
6545
|
}
|
|
5908
6546
|
}),
|
|
6547
|
+
defineTool({
|
|
6548
|
+
name: "tool_copy",
|
|
6549
|
+
title: "Copy a tool",
|
|
6550
|
+
description: "Copy a tool to another project of the organization. Its secrets stay behind, so a copy that needs them is incomplete until someone sets them.",
|
|
6551
|
+
operations: ["copyTool"],
|
|
6552
|
+
safety: "write",
|
|
6553
|
+
input: z.object({
|
|
6554
|
+
project: projectField,
|
|
6555
|
+
tool: toolField,
|
|
6556
|
+
to: projectField.describe("The project that receives the copy, by slug, name or id.")
|
|
6557
|
+
}),
|
|
6558
|
+
run: async ({ project, tool, to }, run) => {
|
|
6559
|
+
const projectId = await projectIdOf(run, project);
|
|
6560
|
+
const found = await resolveTool(run.api, projectId, tool);
|
|
6561
|
+
return copiedTool(run.api, projectId, found.id, await projectIdOf(run, to));
|
|
6562
|
+
}
|
|
6563
|
+
}),
|
|
5909
6564
|
defineTool({
|
|
5910
6565
|
name: "tool_delete",
|
|
5911
6566
|
title: "Delete a tool",
|
|
@@ -5928,16 +6583,178 @@ const projectToolTools = [
|
|
|
5928
6583
|
})
|
|
5929
6584
|
];
|
|
5930
6585
|
//#endregion
|
|
5931
|
-
//#region src/commands/projects.ts
|
|
5932
|
-
const
|
|
6586
|
+
//#region src/commands/projects/shared.ts
|
|
6587
|
+
const defaultProjectColor = "#7c86ff";
|
|
6588
|
+
const projectHint = {
|
|
6589
|
+
"project.not_found": "kcloud projects list shows the projects you are a member of.",
|
|
6590
|
+
"project.slug_taken": "Another project of the organization has that slug.",
|
|
6591
|
+
"project.invalid_slug": "A slug holds lowercase letters, digits and hyphens.",
|
|
6592
|
+
"project.not_empty": "Delete its agents, credentials, repositories and tools first.",
|
|
6593
|
+
"project.member_not_found": "kcloud projects members list <project> shows them.",
|
|
6594
|
+
"project.member_must_be_active": "kcloud orgs members enable <member> enables the account first."
|
|
6595
|
+
};
|
|
6596
|
+
const projectOf = async (api, id) => dataOf("describe the project", await api.GET("/projects/{id}", { params: { path: { id } } }), projectHint);
|
|
6597
|
+
//#endregion
|
|
6598
|
+
//#region src/commands/projects/members.ts
|
|
6599
|
+
const projectMembersOf = async (api, projectId) => dataOf("list the members of the project", await api.GET("/projects/{id}/members", { params: { path: { id: projectId } } }), projectHint).items;
|
|
6600
|
+
const resolveProjectMember = async (api, projectId, reference) => {
|
|
6601
|
+
const found = (await projectMembersOf(api, projectId)).find((member) => member.userId === reference.toLowerCase() || sameEmail(member.email, reference));
|
|
6602
|
+
if (found === void 0) throw new CliError(`The project has no member ${reference}.`, "kcloud projects members list <project> shows them.");
|
|
6603
|
+
return found;
|
|
6604
|
+
};
|
|
6605
|
+
const projectUserIdOf = async (api, projectId, reference) => {
|
|
6606
|
+
if (isId(reference)) return reference.toLowerCase();
|
|
6607
|
+
const { organizationId } = await projectOf(api, projectId);
|
|
6608
|
+
return (await resolveOrganizationMember(api, organizationId, reference)).id;
|
|
6609
|
+
};
|
|
6610
|
+
const setProjectMember = async (api, projectId, userId, role) => {
|
|
6611
|
+
doneWith("set the member of the project", await api.PUT("/projects/{id}/members/{userId}", {
|
|
6612
|
+
params: { path: {
|
|
6613
|
+
id: projectId,
|
|
6614
|
+
userId
|
|
6615
|
+
} },
|
|
6616
|
+
body: { role }
|
|
6617
|
+
}), projectHint);
|
|
6618
|
+
};
|
|
6619
|
+
const removeProjectMember = async (api, projectId, userId) => {
|
|
6620
|
+
doneWith("remove the member of the project", await api.DELETE("/projects/{id}/members/{userId}", { params: { path: {
|
|
6621
|
+
id: projectId,
|
|
6622
|
+
userId
|
|
6623
|
+
} } }), projectHint);
|
|
6624
|
+
};
|
|
6625
|
+
const projectMembersCommand = (adaptersOf) => defineGroup({
|
|
6626
|
+
name: "members",
|
|
6627
|
+
summary: "The members of a project and their project role.",
|
|
6628
|
+
usage: "projects members <command>",
|
|
6629
|
+
subcommands: [
|
|
6630
|
+
defineCommand({
|
|
6631
|
+
name: "list",
|
|
6632
|
+
summary: "List the members of one project.",
|
|
6633
|
+
usage: "projects members list <project>",
|
|
6634
|
+
options: {},
|
|
6635
|
+
positionals: {
|
|
6636
|
+
min: 1,
|
|
6637
|
+
max: 1
|
|
6638
|
+
},
|
|
6639
|
+
run: async (invocation) => {
|
|
6640
|
+
const run = await runOf(adaptersOf, invocation);
|
|
6641
|
+
const id = await resolveProjectId(run, invocation.positionals[0] ?? "");
|
|
6642
|
+
const items = await projectMembersOf(run.api, id);
|
|
6643
|
+
return printed(run, items, (members) => members.length === 0 ? "The project has no member." : renderTable([
|
|
6644
|
+
{ header: "EMAIL" },
|
|
6645
|
+
{ header: "ROLE" },
|
|
6646
|
+
{ header: "STATUS" },
|
|
6647
|
+
{ header: "ADDED" },
|
|
6648
|
+
{ header: "ID" }
|
|
6649
|
+
], members.map((member) => [
|
|
6650
|
+
member.email,
|
|
6651
|
+
member.role,
|
|
6652
|
+
member.status,
|
|
6653
|
+
member.addedAt,
|
|
6654
|
+
member.userId
|
|
6655
|
+
])));
|
|
6656
|
+
}
|
|
6657
|
+
}),
|
|
6658
|
+
defineCommand({
|
|
6659
|
+
name: "set",
|
|
6660
|
+
summary: "Add a member of the organization to a project, or change their role.",
|
|
6661
|
+
usage: "projects members set <project> <user> --role <role>",
|
|
6662
|
+
optionsUsage: [roleUsage, ["", "the user is named by e-mail or id, and belongs to the organization"]],
|
|
6663
|
+
options: roleOption,
|
|
6664
|
+
positionals: {
|
|
6665
|
+
min: 2,
|
|
6666
|
+
max: 2
|
|
6667
|
+
},
|
|
6668
|
+
run: async (invocation) => {
|
|
6669
|
+
const [project = "", user = ""] = invocation.positionals;
|
|
6670
|
+
const role = roleFrom(invocation.values.role, "projects members set");
|
|
6671
|
+
const run = await runOf(adaptersOf, invocation);
|
|
6672
|
+
const id = await resolveProjectId(run, project);
|
|
6673
|
+
const userId = await projectUserIdOf(run.api, id, user);
|
|
6674
|
+
await setProjectMember(run.api, id, userId, role);
|
|
6675
|
+
return said(run, `${user} is ${asRole(role)} of the project ${project} now.`);
|
|
6676
|
+
}
|
|
6677
|
+
}),
|
|
6678
|
+
defineCommand({
|
|
6679
|
+
name: "remove",
|
|
6680
|
+
summary: "Take a member off a project.",
|
|
6681
|
+
usage: "projects members remove <project> <user>",
|
|
6682
|
+
optionsUsage: [["", "the user is named by e-mail or id"]],
|
|
6683
|
+
options: {},
|
|
6684
|
+
positionals: {
|
|
6685
|
+
min: 2,
|
|
6686
|
+
max: 2
|
|
6687
|
+
},
|
|
6688
|
+
run: async (invocation) => {
|
|
6689
|
+
const [project = "", user = ""] = invocation.positionals;
|
|
6690
|
+
const run = await runOf(adaptersOf, invocation);
|
|
6691
|
+
const id = await resolveProjectId(run, project);
|
|
6692
|
+
const member = await resolveProjectMember(run.api, id, user);
|
|
6693
|
+
await confirmed({
|
|
6694
|
+
prompt: run.prompt,
|
|
6695
|
+
settings: run.settings,
|
|
6696
|
+
question: `Remove ${member.email} from the project ${project}?`
|
|
6697
|
+
});
|
|
6698
|
+
await removeProjectMember(run.api, id, member.userId);
|
|
6699
|
+
return said(run, `Removed ${member.email} from the project ${project}.`);
|
|
6700
|
+
}
|
|
6701
|
+
})
|
|
6702
|
+
]
|
|
6703
|
+
});
|
|
6704
|
+
//#endregion
|
|
6705
|
+
//#region src/commands/projects/index.ts
|
|
6706
|
+
const fieldOptions = {
|
|
6707
|
+
slug: { type: "string" },
|
|
6708
|
+
description: { type: "string" },
|
|
6709
|
+
color: { type: "string" }
|
|
6710
|
+
};
|
|
6711
|
+
const fieldUsage = [
|
|
6712
|
+
["--slug <slug>", "lowercase letters, digits and hyphens (default: made from the name)"],
|
|
6713
|
+
["--description <text>", "what the project is for"],
|
|
6714
|
+
["--color <#rrggbb>", `the color of the project in the panel (default: ${defaultProjectColor})`]
|
|
6715
|
+
];
|
|
6716
|
+
const createdProject = async (scope, name, { slug, description, color }) => {
|
|
6717
|
+
const body = {
|
|
6718
|
+
name,
|
|
6719
|
+
...slug === void 0 ? {} : { slug },
|
|
6720
|
+
description: description ?? "",
|
|
6721
|
+
color: color ?? "#7c86ff"
|
|
6722
|
+
};
|
|
6723
|
+
validated(createProjectBodySchema, body, "a project");
|
|
6724
|
+
const organization = await scope.organization();
|
|
6725
|
+
return {
|
|
6726
|
+
project: dataOf("create the project", await scope.api.POST("/organizations/{organizationId}/projects", {
|
|
6727
|
+
params: { path: { organizationId: organization.id } },
|
|
6728
|
+
body
|
|
6729
|
+
}), projectHint),
|
|
6730
|
+
organization
|
|
6731
|
+
};
|
|
6732
|
+
};
|
|
6733
|
+
const changedProject = async (api, id, change) => {
|
|
6734
|
+
const current = await projectOf(api, id);
|
|
6735
|
+
const body = {
|
|
6736
|
+
name: change.name ?? current.name,
|
|
6737
|
+
slug: change.slug ?? current.slug,
|
|
6738
|
+
description: change.description ?? current.description,
|
|
6739
|
+
color: change.color ?? current.color
|
|
6740
|
+
};
|
|
6741
|
+
validated(updateProjectBodySchema, body, "a change to a project");
|
|
6742
|
+
return dataOf("change the project", await api.PATCH("/projects/{id}", {
|
|
6743
|
+
params: { path: { id } },
|
|
6744
|
+
body
|
|
6745
|
+
}), projectHint);
|
|
6746
|
+
};
|
|
6747
|
+
const deleteProject = async (api, id) => {
|
|
6748
|
+
doneWith("delete the project", await api.DELETE("/projects/{id}", { params: { path: { id } } }), projectHint);
|
|
6749
|
+
};
|
|
5933
6750
|
const createProjectsCommand = (adaptersOf) => defineGroup({
|
|
5934
6751
|
name: "projects",
|
|
5935
|
-
summary: "The projects
|
|
6752
|
+
summary: "The projects of the organization you can see.",
|
|
5936
6753
|
usage: "projects <command>",
|
|
5937
6754
|
subcommands: [
|
|
5938
6755
|
defineCommand({
|
|
5939
6756
|
name: "list",
|
|
5940
|
-
summary: "List the projects you can see.",
|
|
6757
|
+
summary: "List the projects of the organization you can see.",
|
|
5941
6758
|
usage: "projects list",
|
|
5942
6759
|
options: {},
|
|
5943
6760
|
positionals: {
|
|
@@ -5946,8 +6763,9 @@ const createProjectsCommand = (adaptersOf) => defineGroup({
|
|
|
5946
6763
|
},
|
|
5947
6764
|
run: async (invocation) => {
|
|
5948
6765
|
const run = await runOf(adaptersOf, invocation);
|
|
5949
|
-
const
|
|
5950
|
-
|
|
6766
|
+
const items = await projectsOf(run);
|
|
6767
|
+
const { slug } = await run.organization();
|
|
6768
|
+
return printed(run, items, (projects) => projects.length === 0 ? `You see no project in ${slug} yet.` : renderTable([
|
|
5951
6769
|
{ header: "SLUG" },
|
|
5952
6770
|
{ header: "NAME" },
|
|
5953
6771
|
{
|
|
@@ -5976,12 +6794,13 @@ const createProjectsCommand = (adaptersOf) => defineGroup({
|
|
|
5976
6794
|
},
|
|
5977
6795
|
run: async (invocation) => {
|
|
5978
6796
|
const run = await runOf(adaptersOf, invocation);
|
|
5979
|
-
const id = await resolveProjectId(run
|
|
6797
|
+
const id = await resolveProjectId(run, invocation.positionals[0] ?? "");
|
|
5980
6798
|
const project = dataOf("describe the project", await run.api.GET("/projects/{id}", { params: { path: { id } } }), projectHint);
|
|
5981
6799
|
return printed(run, project, (shown) => renderTable([{ header: "FIELD" }, { header: "VALUE" }], [
|
|
5982
6800
|
["name", shown.name],
|
|
5983
6801
|
["slug", shown.slug],
|
|
5984
6802
|
["description", shown.description === "" ? "-" : shown.description],
|
|
6803
|
+
["color", shown.color],
|
|
5985
6804
|
["docker", shown.dockerAllowed ? "allowed" : "not allowed"],
|
|
5986
6805
|
["members", String(shown.memberCount)],
|
|
5987
6806
|
["created", shown.createdAt],
|
|
@@ -5990,9 +6809,52 @@ const createProjectsCommand = (adaptersOf) => defineGroup({
|
|
|
5990
6809
|
}
|
|
5991
6810
|
}),
|
|
5992
6811
|
defineCommand({
|
|
5993
|
-
name: "
|
|
5994
|
-
summary: "
|
|
5995
|
-
usage: "projects
|
|
6812
|
+
name: "create",
|
|
6813
|
+
summary: "Create a project in the organization.",
|
|
6814
|
+
usage: "projects create <name> [--slug <slug>] [--description <text>] [--color <#rrggbb>]",
|
|
6815
|
+
optionsUsage: fieldUsage,
|
|
6816
|
+
options: fieldOptions,
|
|
6817
|
+
positionals: {
|
|
6818
|
+
min: 1,
|
|
6819
|
+
max: 1
|
|
6820
|
+
},
|
|
6821
|
+
run: async (invocation) => {
|
|
6822
|
+
const run = await runOf(adaptersOf, invocation);
|
|
6823
|
+
const { project, organization } = await createdProject(run, invocation.positionals[0] ?? "", invocation.values);
|
|
6824
|
+
return printed(run, project, (created) => `Created ${created.name} (${created.slug}) in ${organization.slug} as ${created.id}.`);
|
|
6825
|
+
}
|
|
6826
|
+
}),
|
|
6827
|
+
defineCommand({
|
|
6828
|
+
name: "update",
|
|
6829
|
+
summary: "Change the name, slug, description or color of a project.",
|
|
6830
|
+
usage: "projects update <project> [--name <name>] [--slug <slug>] [--description <text>] [--color <#rrggbb>]",
|
|
6831
|
+
optionsUsage: [["--name <name>", "the new name"], ...fieldUsage],
|
|
6832
|
+
options: {
|
|
6833
|
+
name: { type: "string" },
|
|
6834
|
+
...fieldOptions
|
|
6835
|
+
},
|
|
6836
|
+
positionals: {
|
|
6837
|
+
min: 1,
|
|
6838
|
+
max: 1
|
|
6839
|
+
},
|
|
6840
|
+
run: async (invocation) => {
|
|
6841
|
+
const { values } = invocation;
|
|
6842
|
+
if ([
|
|
6843
|
+
values.name,
|
|
6844
|
+
values.slug,
|
|
6845
|
+
values.description,
|
|
6846
|
+
values.color
|
|
6847
|
+
].every((value) => value === void 0)) throw new UsageError("projects update needs --name, --slug, --description or --color.");
|
|
6848
|
+
const run = await runOf(adaptersOf, invocation);
|
|
6849
|
+
const id = await resolveProjectId(run, invocation.positionals[0] ?? "");
|
|
6850
|
+
const project = await changedProject(run.api, id, values);
|
|
6851
|
+
return printed(run, project, (shown) => `Changed ${shown.name} (${shown.slug}).`);
|
|
6852
|
+
}
|
|
6853
|
+
}),
|
|
6854
|
+
defineCommand({
|
|
6855
|
+
name: "delete",
|
|
6856
|
+
summary: "Delete a project that holds no agent, credential, repository or tool.",
|
|
6857
|
+
usage: "projects delete <project>",
|
|
5996
6858
|
options: {},
|
|
5997
6859
|
positionals: {
|
|
5998
6860
|
min: 1,
|
|
@@ -6000,50 +6862,136 @@ const createProjectsCommand = (adaptersOf) => defineGroup({
|
|
|
6000
6862
|
},
|
|
6001
6863
|
run: async (invocation) => {
|
|
6002
6864
|
const run = await runOf(adaptersOf, invocation);
|
|
6003
|
-
const id = await resolveProjectId(run
|
|
6004
|
-
const
|
|
6005
|
-
|
|
6006
|
-
|
|
6007
|
-
|
|
6008
|
-
|
|
6009
|
-
|
|
6010
|
-
|
|
6011
|
-
|
|
6012
|
-
member.role,
|
|
6013
|
-
member.status,
|
|
6014
|
-
member.addedAt
|
|
6015
|
-
])));
|
|
6865
|
+
const id = await resolveProjectId(run, invocation.positionals[0] ?? "");
|
|
6866
|
+
const project = await projectOf(run.api, id);
|
|
6867
|
+
await confirmed({
|
|
6868
|
+
prompt: run.prompt,
|
|
6869
|
+
settings: run.settings,
|
|
6870
|
+
question: `Delete the project ${project.name} (${project.slug})?`
|
|
6871
|
+
});
|
|
6872
|
+
await deleteProject(run.api, id);
|
|
6873
|
+
return said(run, `Deleted the project ${project.name}.`);
|
|
6016
6874
|
}
|
|
6017
|
-
})
|
|
6875
|
+
}),
|
|
6876
|
+
projectMembersCommand(adaptersOf)
|
|
6018
6877
|
]
|
|
6019
6878
|
});
|
|
6020
6879
|
//#endregion
|
|
6021
6880
|
//#region src/mcp/tools/projects.ts
|
|
6022
|
-
const
|
|
6023
|
-
|
|
6024
|
-
|
|
6025
|
-
description:
|
|
6026
|
-
|
|
6027
|
-
|
|
6028
|
-
|
|
6029
|
-
|
|
6030
|
-
|
|
6031
|
-
|
|
6032
|
-
|
|
6033
|
-
|
|
6034
|
-
|
|
6035
|
-
|
|
6036
|
-
|
|
6037
|
-
|
|
6038
|
-
|
|
6039
|
-
|
|
6040
|
-
|
|
6041
|
-
|
|
6042
|
-
|
|
6043
|
-
|
|
6044
|
-
}
|
|
6045
|
-
|
|
6046
|
-
|
|
6881
|
+
const userField = z.string().trim().min(1).describe("The user, by e-mail or id, a member of the organization of the project.");
|
|
6882
|
+
const fields = {
|
|
6883
|
+
slug: slugSchema.optional().describe("Lowercase letters, digits and hyphens."),
|
|
6884
|
+
description: z.string().max(500).optional(),
|
|
6885
|
+
color: colorSchema.optional().describe("The color in the panel, as #rrggbb.")
|
|
6886
|
+
};
|
|
6887
|
+
const projectTools = [
|
|
6888
|
+
defineTool({
|
|
6889
|
+
name: "projects_list",
|
|
6890
|
+
title: "List projects",
|
|
6891
|
+
description: "List the projects you can see in the organization this server acts in.",
|
|
6892
|
+
operations: ["listProjects"],
|
|
6893
|
+
safety: "read",
|
|
6894
|
+
input: z.object({}),
|
|
6895
|
+
run: async (_, run) => projectsOf(run)
|
|
6896
|
+
}),
|
|
6897
|
+
defineTool({
|
|
6898
|
+
name: "project_show",
|
|
6899
|
+
title: "Show a project",
|
|
6900
|
+
description: "Show one project together with its members.",
|
|
6901
|
+
operations: ["getProject", "listProjectMembers"],
|
|
6902
|
+
safety: "read",
|
|
6903
|
+
input: z.object({ project: projectField }),
|
|
6904
|
+
run: async ({ project }, run) => {
|
|
6905
|
+
const id = await projectIdOf(run, project);
|
|
6906
|
+
return {
|
|
6907
|
+
...dataOf("describe the project", await run.api.GET("/projects/{id}", { params: { path: { id } } }), projectHint),
|
|
6908
|
+
members: await projectMembersOf(run.api, id)
|
|
6909
|
+
};
|
|
6910
|
+
}
|
|
6911
|
+
}),
|
|
6912
|
+
defineTool({
|
|
6913
|
+
name: "project_create",
|
|
6914
|
+
title: "Create a project",
|
|
6915
|
+
description: "Create a project in the organization this server acts in.",
|
|
6916
|
+
operations: ["createProject"],
|
|
6917
|
+
safety: "write",
|
|
6918
|
+
input: z.object({
|
|
6919
|
+
name: z.string().trim().min(2).max(80),
|
|
6920
|
+
...fields
|
|
6921
|
+
}),
|
|
6922
|
+
run: async ({ name, ...rest }, run) => (await createdProject(run, name, rest)).project
|
|
6923
|
+
}),
|
|
6924
|
+
defineTool({
|
|
6925
|
+
name: "project_update",
|
|
6926
|
+
title: "Change a project",
|
|
6927
|
+
description: "Change the name, slug, description or color of a project. A field left out keeps its value.",
|
|
6928
|
+
operations: ["updateProject"],
|
|
6929
|
+
safety: "write",
|
|
6930
|
+
idempotent: true,
|
|
6931
|
+
input: z.object({
|
|
6932
|
+
project: projectField,
|
|
6933
|
+
name: z.string().trim().min(2).max(80).optional(),
|
|
6934
|
+
...fields
|
|
6935
|
+
}),
|
|
6936
|
+
run: async ({ project, ...change }, run) => changedProject(run.api, await projectIdOf(run, project), change)
|
|
6937
|
+
}),
|
|
6938
|
+
defineTool({
|
|
6939
|
+
name: "project_delete",
|
|
6940
|
+
title: "Delete a project",
|
|
6941
|
+
description: "Delete a project. The instance refuses while it holds agents, credentials, repositories or tools.",
|
|
6942
|
+
operations: ["deleteProject"],
|
|
6943
|
+
safety: "destructive",
|
|
6944
|
+
input: z.object({ project: projectField }),
|
|
6945
|
+
run: async ({ project }, run) => {
|
|
6946
|
+
const id = await projectIdOf(run, project);
|
|
6947
|
+
await deleteProject(run.api, id);
|
|
6948
|
+
return { deleted: id };
|
|
6949
|
+
}
|
|
6950
|
+
}),
|
|
6951
|
+
defineTool({
|
|
6952
|
+
name: "project_member_set",
|
|
6953
|
+
title: "Set a member of a project",
|
|
6954
|
+
description: "Add a member of the organization to a project with a project role, or change the role of a member.",
|
|
6955
|
+
operations: ["setProjectMember"],
|
|
6956
|
+
safety: "write",
|
|
6957
|
+
idempotent: true,
|
|
6958
|
+
input: z.object({
|
|
6959
|
+
project: projectField,
|
|
6960
|
+
user: userField,
|
|
6961
|
+
role: projectRoleSchema
|
|
6962
|
+
}),
|
|
6963
|
+
run: async ({ project, user, role }, run) => {
|
|
6964
|
+
const id = await projectIdOf(run, project);
|
|
6965
|
+
const userId = await projectUserIdOf(run.api, id, user);
|
|
6966
|
+
await setProjectMember(run.api, id, userId, role);
|
|
6967
|
+
return {
|
|
6968
|
+
projectId: id,
|
|
6969
|
+
userId,
|
|
6970
|
+
role
|
|
6971
|
+
};
|
|
6972
|
+
}
|
|
6973
|
+
}),
|
|
6974
|
+
defineTool({
|
|
6975
|
+
name: "project_member_remove",
|
|
6976
|
+
title: "Remove a member of a project",
|
|
6977
|
+
description: "Take a member off a project. The account stays in the organization.",
|
|
6978
|
+
operations: ["removeProjectMember"],
|
|
6979
|
+
safety: "destructive",
|
|
6980
|
+
input: z.object({
|
|
6981
|
+
project: projectField,
|
|
6982
|
+
user: userField
|
|
6983
|
+
}),
|
|
6984
|
+
run: async ({ project, user }, run) => {
|
|
6985
|
+
const id = await projectIdOf(run, project);
|
|
6986
|
+
const member = await resolveProjectMember(run.api, id, user);
|
|
6987
|
+
await removeProjectMember(run.api, id, member.userId);
|
|
6988
|
+
return {
|
|
6989
|
+
projectId: id,
|
|
6990
|
+
removed: member.userId
|
|
6991
|
+
};
|
|
6992
|
+
}
|
|
6993
|
+
})
|
|
6994
|
+
];
|
|
6047
6995
|
//#endregion
|
|
6048
6996
|
//#region src/mcp/tools/releases.ts
|
|
6049
6997
|
const releaseTools = [defineTool({
|
|
@@ -6071,7 +7019,7 @@ const repositoryHints = {
|
|
|
6071
7019
|
};
|
|
6072
7020
|
const projectFrom = async (run, given) => {
|
|
6073
7021
|
if (given === void 0) throw new UsageError("This command needs a project.", "Pass --project <name>; kcloud projects list shows them.");
|
|
6074
|
-
return resolveProjectId(run
|
|
7022
|
+
return resolveProjectId(run, given);
|
|
6075
7023
|
};
|
|
6076
7024
|
const repositoriesOf = async (api, projectId) => dataOf("list the repositories of the project", await api.GET("/projects/{id}/repositories", { params: { path: { id: projectId } } }), repositoryHints).items;
|
|
6077
7025
|
const resolveRepositoryId = async (api, projectId, reference) => {
|
|
@@ -6447,6 +7395,7 @@ const taskTools = [
|
|
|
6447
7395
|
//#region src/mcp/tools/index.ts
|
|
6448
7396
|
const kcloudTools = [
|
|
6449
7397
|
...accountTools,
|
|
7398
|
+
...organizationTools,
|
|
6450
7399
|
...projectTools,
|
|
6451
7400
|
...credentialTools,
|
|
6452
7401
|
...repositoryTools,
|
|
@@ -6457,13 +7406,15 @@ const kcloudTools = [
|
|
|
6457
7406
|
...notificationTools,
|
|
6458
7407
|
...releaseTools
|
|
6459
7408
|
];
|
|
7409
|
+
const passwordPrinted = (what) => `a secret never passes through the context of a model, and kcloud prints ${what}`;
|
|
7410
|
+
passwordPrinted("the temporary password of the new member"), passwordPrinted("the new temporary password");
|
|
6460
7411
|
//#endregion
|
|
6461
7412
|
//#region src/mcp/server.ts
|
|
6462
7413
|
const instructions = [
|
|
6463
|
-
"These tools act on one Kikimora Cloud instance as the account kcloud is signed in with
|
|
7414
|
+
"These tools act on one Kikimora Cloud instance, as the account kcloud is signed in with, in one organization of it. whoami names all three.",
|
|
6464
7415
|
"Name a project by its slug, its name or its id. Name an agent as <project>/<agent>, as its slug or name together with project, or by its id.",
|
|
6465
7416
|
"A refusal quotes the error code of the instance. Its hint may name a kcloud command, such as kcloud agents list; the tool for the same resource and verb, agents_list, does that work here.",
|
|
6466
|
-
"Secrets never pass through these tools. For them the person at the terminal runs kcloud credentials add, kcloud credentials rotate, kcloud tokens create, kcloud auth password, or sets the secrets of a tool in the panel."
|
|
7417
|
+
"Secrets never pass through these tools. For them the person at the terminal runs kcloud credentials add, kcloud credentials rotate, kcloud tokens create, kcloud auth password, kcloud orgs members add, kcloud orgs members reset-password, or sets the secrets of a tool in the panel."
|
|
6467
7418
|
].join("\n");
|
|
6468
7419
|
const createKcloudServer = ({ version, open }) => {
|
|
6469
7420
|
const server = new McpServer({
|
|
@@ -6484,8 +7435,8 @@ const createKcloudServer = ({ version, open }) => {
|
|
|
6484
7435
|
const createMcpCommand = (adaptersOf, cliVersion) => defineCommand({
|
|
6485
7436
|
name: "mcp",
|
|
6486
7437
|
summary: "Serve the work of kcloud to an MCP client over stdio.",
|
|
6487
|
-
usage: "mcp [--
|
|
6488
|
-
optionsUsage: [["", "an MCP client starts it, as in: claude mcp add kcloud -- kcloud mcp"], ["", "each tool call
|
|
7438
|
+
usage: "mcp [--url <origin>] [--org <organization>]",
|
|
7439
|
+
optionsUsage: [["", "an MCP client starts it, as in: claude mcp add kcloud -- kcloud mcp"], ["", "each tool call reads the token and the organization of that moment"]],
|
|
6489
7440
|
options: {},
|
|
6490
7441
|
positionals: {
|
|
6491
7442
|
min: 0,
|
|
@@ -6504,13 +7455,12 @@ const createMcpCommand = (adaptersOf, cliVersion) => defineCommand({
|
|
|
6504
7455
|
});
|
|
6505
7456
|
return {
|
|
6506
7457
|
api: session.api,
|
|
7458
|
+
me: session.me,
|
|
7459
|
+
organizations: session.organizations,
|
|
7460
|
+
organization: session.organization,
|
|
6507
7461
|
origin: session.origin,
|
|
6508
|
-
profileName: session.profileName,
|
|
6509
7462
|
kcloudVersion: cliVersion,
|
|
6510
|
-
token: () =>
|
|
6511
|
-
settings,
|
|
6512
|
-
context
|
|
6513
|
-
}),
|
|
7463
|
+
token: () => Promise.resolve(session.token),
|
|
6514
7464
|
wait: adapters.wait,
|
|
6515
7465
|
now: adapters.now
|
|
6516
7466
|
};
|
|
@@ -6527,96 +7477,6 @@ const createMcpCommand = (adaptersOf, cliVersion) => defineCommand({
|
|
|
6527
7477
|
}
|
|
6528
7478
|
});
|
|
6529
7479
|
//#endregion
|
|
6530
|
-
//#region src/commands/profiles.ts
|
|
6531
|
-
const rowsOf = (store) => Object.entries(store.profiles).map(([name, profile]) => [
|
|
6532
|
-
name === store.current ? `${name} *` : name,
|
|
6533
|
-
profile.origin,
|
|
6534
|
-
profile.email ?? "unknown"
|
|
6535
|
-
]);
|
|
6536
|
-
const namedProfile = (store, name) => {
|
|
6537
|
-
if (store.profiles[name] === void 0) {
|
|
6538
|
-
const known = Object.keys(store.profiles);
|
|
6539
|
-
throw new CliError(`There is no profile ${name} here.`, known.length === 0 ? "Run kcloud login --url <origin> to make one." : `kcloud profiles list shows ${known.join(", ")}.`);
|
|
6540
|
-
}
|
|
6541
|
-
};
|
|
6542
|
-
const createProfilesCommand = () => defineGroup({
|
|
6543
|
-
name: "profiles",
|
|
6544
|
-
summary: "The instances this machine is signed in to.",
|
|
6545
|
-
usage: "profiles <command>",
|
|
6546
|
-
subcommands: [
|
|
6547
|
-
defineCommand({
|
|
6548
|
-
name: "list",
|
|
6549
|
-
summary: "List the saved profiles, marking the current one.",
|
|
6550
|
-
usage: "profiles list",
|
|
6551
|
-
options: {},
|
|
6552
|
-
positionals: {
|
|
6553
|
-
min: 0,
|
|
6554
|
-
max: 0
|
|
6555
|
-
},
|
|
6556
|
-
run: async ({ settings, context }) => {
|
|
6557
|
-
const store = await readProfileStore(context.homeDirectory);
|
|
6558
|
-
if (settings.json) {
|
|
6559
|
-
context.terminal.json({
|
|
6560
|
-
current: store.current,
|
|
6561
|
-
profiles: Object.entries(store.profiles).map(([name, profile]) => ({
|
|
6562
|
-
name,
|
|
6563
|
-
origin: profile.origin,
|
|
6564
|
-
email: profile.email
|
|
6565
|
-
}))
|
|
6566
|
-
});
|
|
6567
|
-
return exitCodes.ok;
|
|
6568
|
-
}
|
|
6569
|
-
const rows = rowsOf(store);
|
|
6570
|
-
context.terminal.line(rows.length === 0 ? "No instance is signed in here; kcloud login --url <origin> signs in." : renderTable([
|
|
6571
|
-
{ header: "PROFILE" },
|
|
6572
|
-
{ header: "INSTANCE" },
|
|
6573
|
-
{ header: "ACCOUNT" }
|
|
6574
|
-
], rows));
|
|
6575
|
-
return exitCodes.ok;
|
|
6576
|
-
}
|
|
6577
|
-
}),
|
|
6578
|
-
defineCommand({
|
|
6579
|
-
name: "use",
|
|
6580
|
-
summary: "Make one of the saved profiles the current one.",
|
|
6581
|
-
usage: "profiles use <name>",
|
|
6582
|
-
options: {},
|
|
6583
|
-
positionals: {
|
|
6584
|
-
min: 1,
|
|
6585
|
-
max: 1
|
|
6586
|
-
},
|
|
6587
|
-
run: async ({ positionals, context }) => {
|
|
6588
|
-
const [name = ""] = positionals;
|
|
6589
|
-
const store = await readProfileStore(context.homeDirectory);
|
|
6590
|
-
namedProfile(store, name);
|
|
6591
|
-
await writeProfileStore(context.homeDirectory, {
|
|
6592
|
-
...store,
|
|
6593
|
-
current: name
|
|
6594
|
-
});
|
|
6595
|
-
context.terminal.line(`${name} is the current profile.`);
|
|
6596
|
-
return exitCodes.ok;
|
|
6597
|
-
}
|
|
6598
|
-
}),
|
|
6599
|
-
defineCommand({
|
|
6600
|
-
name: "remove",
|
|
6601
|
-
summary: "Forget a saved profile without revoking its token.",
|
|
6602
|
-
usage: "profiles remove <name>",
|
|
6603
|
-
options: {},
|
|
6604
|
-
positionals: {
|
|
6605
|
-
min: 1,
|
|
6606
|
-
max: 1
|
|
6607
|
-
},
|
|
6608
|
-
run: async ({ positionals, context }) => {
|
|
6609
|
-
const [name = ""] = positionals;
|
|
6610
|
-
const store = await readProfileStore(context.homeDirectory);
|
|
6611
|
-
namedProfile(store, name);
|
|
6612
|
-
await writeProfileStore(context.homeDirectory, withoutProfile(store, name));
|
|
6613
|
-
context.terminal.line(`Forgot ${name}; its token stays usable until it is revoked.`);
|
|
6614
|
-
return exitCodes.ok;
|
|
6615
|
-
}
|
|
6616
|
-
})
|
|
6617
|
-
]
|
|
6618
|
-
});
|
|
6619
|
-
//#endregion
|
|
6620
7480
|
//#region src/commands/releases.ts
|
|
6621
7481
|
const createReleasesCommand = (adaptersOf) => defineGroup({
|
|
6622
7482
|
name: "releases",
|
|
@@ -6656,7 +7516,7 @@ const kcloudCommands = () => [
|
|
|
6656
7516
|
createLogoutCommand(createAdapters),
|
|
6657
7517
|
createWhoamiCommand(createAdapters),
|
|
6658
7518
|
createVersionCommand(createAdapters, cliVersion),
|
|
6659
|
-
|
|
7519
|
+
createOrgsCommand(createAdapters),
|
|
6660
7520
|
createTokensCommand(createAdapters),
|
|
6661
7521
|
createAuthCommand(createAdapters),
|
|
6662
7522
|
createProjectsCommand(createAdapters),
|