@khotan/cli 0.6.0 → 0.9.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 +34 -1
- package/dist/khotan.js +921 -42
- package/package.json +4 -4
package/dist/khotan.js
CHANGED
|
@@ -19,7 +19,7 @@ var __esm = (fn, res) => () => (fn && (res = fn(fn = 0)), res);
|
|
|
19
19
|
var __require = /* @__PURE__ */ createRequire(import.meta.url);
|
|
20
20
|
|
|
21
21
|
// ../khotan-core/src/version.ts
|
|
22
|
-
var CATALOG_SCHEMA_VERSION = 1, CATALOG_VERSION = "2026-06-
|
|
22
|
+
var CATALOG_SCHEMA_VERSION = 1, CATALOG_VERSION = "2026-06-28", KHOTAN_ADAPTER_NAME = "khotan", KHOTAN_ADAPTER_VERSION = "0.7.0", SUPPORTED_CATALOG_SCHEMA_VERSIONS;
|
|
23
23
|
var init_version = __esm(() => {
|
|
24
24
|
SUPPORTED_CATALOG_SCHEMA_VERSIONS = [
|
|
25
25
|
CATALOG_SCHEMA_VERSION
|
|
@@ -322,6 +322,71 @@ function platformDomain(config) {
|
|
|
322
322
|
};
|
|
323
323
|
return [...operations, resource];
|
|
324
324
|
}
|
|
325
|
+
function emailSubscriptionDomain(config) {
|
|
326
|
+
const { domain, noun, resourceParam, cap } = config;
|
|
327
|
+
const base = `/api/v1/${domain}/{${resourceParam}}/email-subscriptions`;
|
|
328
|
+
const idArg = idField(resourceParam, `The ${noun} id.`);
|
|
329
|
+
const subArg = idField("subscriptionId", "The email subscription id.");
|
|
330
|
+
const tool = (suffix) => `khotan_${domain}_email_subscriptions_${suffix}`;
|
|
331
|
+
return [
|
|
332
|
+
{
|
|
333
|
+
id: `${domain}.email-subscriptions.list`,
|
|
334
|
+
kind: "operation",
|
|
335
|
+
domain,
|
|
336
|
+
safety: "read",
|
|
337
|
+
auth: true,
|
|
338
|
+
title: `List ${noun} email subscriptions`,
|
|
339
|
+
description: `List the connected mailboxes this ${noun} watches.`,
|
|
340
|
+
input: [idArg],
|
|
341
|
+
http: { method: "GET", path: base, operationId: `list${cap}EmailSubscriptions` },
|
|
342
|
+
cli: { command: [domain, "email-subscriptions", "list"], summary: "List email subscriptions", defaultOutput: "table" },
|
|
343
|
+
mcp: { tool: { name: tool("list"), title: "List email subscriptions", description: `List the connected mailboxes this ${noun} watches.` } }
|
|
344
|
+
},
|
|
345
|
+
{
|
|
346
|
+
id: `${domain}.email-subscriptions.create`,
|
|
347
|
+
kind: "operation",
|
|
348
|
+
domain,
|
|
349
|
+
safety: "write",
|
|
350
|
+
auth: true,
|
|
351
|
+
title: `Subscribe ${article(noun)} ${noun} to a mailbox`,
|
|
352
|
+
description: `Watch a connected Gmail mailbox for this ${noun}; Google pushes notifications directly to the delivery URL.`,
|
|
353
|
+
input: [
|
|
354
|
+
idArg,
|
|
355
|
+
{ name: "emailIntegrationId", location: "body", type: "string", required: true, description: "The connected Gmail integration id." },
|
|
356
|
+
{ name: "deliveryUrl", location: "body", type: "string", required: true, description: "https URL on the target's primary hostname." }
|
|
357
|
+
],
|
|
358
|
+
http: { method: "POST", path: base, operationId: `create${cap}EmailSubscription`, bodyMode: "json" },
|
|
359
|
+
cli: { command: [domain, "email-subscriptions", "create"], summary: "Subscribe to a mailbox", defaultOutput: "detail" },
|
|
360
|
+
mcp: { tool: { name: tool("create"), title: "Subscribe to a mailbox", description: `Watch a connected Gmail mailbox for this ${noun}.` } }
|
|
361
|
+
},
|
|
362
|
+
{
|
|
363
|
+
id: `${domain}.email-subscriptions.get`,
|
|
364
|
+
kind: "operation",
|
|
365
|
+
domain,
|
|
366
|
+
safety: "read",
|
|
367
|
+
auth: true,
|
|
368
|
+
title: `Get ${article(noun)} ${noun} email subscription`,
|
|
369
|
+
description: "Get one email subscription by id.",
|
|
370
|
+
input: [idArg, subArg],
|
|
371
|
+
http: { method: "GET", path: `${base}/{subscriptionId}`, operationId: `get${cap}EmailSubscription` },
|
|
372
|
+
cli: { command: [domain, "email-subscriptions", "get"], summary: "Get an email subscription", defaultOutput: "detail" },
|
|
373
|
+
mcp: { tool: { name: tool("get"), title: "Get an email subscription", description: "Get one email subscription by id." } }
|
|
374
|
+
},
|
|
375
|
+
{
|
|
376
|
+
id: `${domain}.email-subscriptions.delete`,
|
|
377
|
+
kind: "operation",
|
|
378
|
+
domain,
|
|
379
|
+
safety: "destructive",
|
|
380
|
+
auth: true,
|
|
381
|
+
title: `Unsubscribe ${article(noun)} ${noun} from a mailbox`,
|
|
382
|
+
description: "Remove an email subscription (deletes the Pub/Sub push subscription).",
|
|
383
|
+
input: [idArg, subArg],
|
|
384
|
+
http: { method: "DELETE", path: `${base}/{subscriptionId}`, operationId: `delete${cap}EmailSubscription` },
|
|
385
|
+
cli: { command: [domain, "email-subscriptions", "delete"], summary: "Unsubscribe from a mailbox" },
|
|
386
|
+
mcp: { tool: { name: tool("delete"), title: "Unsubscribe from a mailbox", description: "Remove an email subscription. Requires confirmation." } }
|
|
387
|
+
}
|
|
388
|
+
];
|
|
389
|
+
}
|
|
325
390
|
function getCapability(id) {
|
|
326
391
|
return capabilitiesById.get(id);
|
|
327
392
|
}
|
|
@@ -343,7 +408,7 @@ var idField = (name, description) => ({
|
|
|
343
408
|
type: "string",
|
|
344
409
|
required: true,
|
|
345
410
|
description
|
|
346
|
-
}), article = (noun) => /^[aeiou]/i.test(noun) ? "an" : "a", identityCapabilities, databaseCapabilities, fileCapabilities, folderCapabilities, contextCapabilities, appsCapabilities, pipelinesCapabilities, capabilities, khotanCatalog, capabilitiesById;
|
|
411
|
+
}), article = (noun) => /^[aeiou]/i.test(noun) ? "an" : "a", identityCapabilities, databaseCapabilities, redisCapabilities, fileCapabilities, folderCapabilities, contextCapabilities, appsCapabilities, pipelinesCapabilities, integrationsCapabilities, emailSubscriptionCapabilities, capabilities, khotanCatalog, capabilitiesById;
|
|
347
412
|
var init_catalog = __esm(() => {
|
|
348
413
|
init_version();
|
|
349
414
|
identityCapabilities = [
|
|
@@ -582,6 +647,54 @@ var init_catalog = __esm(() => {
|
|
|
582
647
|
cli: { command: ["databases", "branches", "rotate-credentials"], summary: "Rotate branch credentials (secret)", defaultOutput: "raw" },
|
|
583
648
|
mcp: { tool: { name: "khotan_databases_branches_rotate_credentials", title: "Rotate branch credentials (secret)", description: "Rotate a branch's credentials and return the new connection. Secret-bearing." } }
|
|
584
649
|
},
|
|
650
|
+
{
|
|
651
|
+
id: "databases.schema-promotions.plan",
|
|
652
|
+
kind: "operation",
|
|
653
|
+
domain: "databases",
|
|
654
|
+
safety: "write",
|
|
655
|
+
auth: true,
|
|
656
|
+
title: "Plan a database schema promotion",
|
|
657
|
+
description: "Generate and validate a schema-only promotion plan from a ready branch to the production database. Data is never copied.",
|
|
658
|
+
input: [
|
|
659
|
+
idField("databaseId", "The database id."),
|
|
660
|
+
idField("branchId", "The branch id.")
|
|
661
|
+
],
|
|
662
|
+
http: { method: "POST", path: "/api/v1/databases/{databaseId}/branches/{branchId}/schema-promotions", operationId: "planDatabaseSchemaPromotion", bodyMode: "none" },
|
|
663
|
+
cli: { command: ["databases", "schema-promotions", "plan"], summary: "Plan a schema-only promotion", defaultOutput: "detail" },
|
|
664
|
+
mcp: { tool: { name: "khotan_databases_schema_promotions_plan", title: "Plan a schema promotion", description: "Generate and validate a schema-only promotion plan from a ready branch to production. Data is never copied." } }
|
|
665
|
+
},
|
|
666
|
+
{
|
|
667
|
+
id: "databases.schema-promotions.get",
|
|
668
|
+
kind: "operation",
|
|
669
|
+
domain: "databases",
|
|
670
|
+
safety: "read",
|
|
671
|
+
auth: true,
|
|
672
|
+
title: "Get a database schema promotion",
|
|
673
|
+
description: "Read one schema promotion record, including summary, blocked changes, failure state, and SQL.",
|
|
674
|
+
input: [
|
|
675
|
+
idField("databaseId", "The database id."),
|
|
676
|
+
idField("promotionId", "The schema promotion id.")
|
|
677
|
+
],
|
|
678
|
+
http: { method: "GET", path: "/api/v1/databases/{databaseId}/schema-promotions/{promotionId}", operationId: "getDatabaseSchemaPromotion" },
|
|
679
|
+
cli: { command: ["databases", "schema-promotions", "get"], summary: "Get a schema promotion", defaultOutput: "detail" },
|
|
680
|
+
mcp: { tool: { name: "khotan_databases_schema_promotions_get", title: "Get a schema promotion", description: "Read one schema promotion record, including generated SQL." } }
|
|
681
|
+
},
|
|
682
|
+
{
|
|
683
|
+
id: "databases.schema-promotions.apply",
|
|
684
|
+
kind: "operation",
|
|
685
|
+
domain: "databases",
|
|
686
|
+
safety: "destructive",
|
|
687
|
+
auth: true,
|
|
688
|
+
title: "Apply a database schema promotion",
|
|
689
|
+
description: "Apply the exact reviewed schema-only promotion SQL to the production database. Fails if branch or production schema hashes changed after planning.",
|
|
690
|
+
input: [
|
|
691
|
+
idField("databaseId", "The database id."),
|
|
692
|
+
idField("promotionId", "The schema promotion id.")
|
|
693
|
+
],
|
|
694
|
+
http: { method: "POST", path: "/api/v1/databases/{databaseId}/schema-promotions/{promotionId}/apply", operationId: "applyDatabaseSchemaPromotion", bodyMode: "none" },
|
|
695
|
+
cli: { command: ["databases", "schema-promotions", "apply"], summary: "Apply a schema promotion" },
|
|
696
|
+
mcp: { tool: { name: "khotan_databases_schema_promotions_apply", title: "Apply a schema promotion", description: "Apply reviewed schema-only SQL to production. Requires confirmation." } }
|
|
697
|
+
},
|
|
585
698
|
{
|
|
586
699
|
id: "databases.branches.resource",
|
|
587
700
|
kind: "resource",
|
|
@@ -607,6 +720,84 @@ var init_catalog = __esm(() => {
|
|
|
607
720
|
}
|
|
608
721
|
}
|
|
609
722
|
];
|
|
723
|
+
redisCapabilities = [
|
|
724
|
+
{
|
|
725
|
+
id: "redis.list",
|
|
726
|
+
kind: "operation",
|
|
727
|
+
domain: "redis",
|
|
728
|
+
safety: "read",
|
|
729
|
+
auth: true,
|
|
730
|
+
title: "List Redis databases",
|
|
731
|
+
description: "List the organization's Redis (KV) databases (no credentials).",
|
|
732
|
+
input: [],
|
|
733
|
+
http: { method: "GET", path: "/api/v1/redis-databases", operationId: "listRedisDatabases" },
|
|
734
|
+
cli: { command: ["redis", "list"], summary: "List Redis (KV) databases", defaultOutput: "table" },
|
|
735
|
+
mcp: { tool: { name: "khotan_redis_list", title: "List Redis databases", description: "List the organization's Redis (KV) databases (no credentials)." } }
|
|
736
|
+
},
|
|
737
|
+
{
|
|
738
|
+
id: "redis.get",
|
|
739
|
+
kind: "operation",
|
|
740
|
+
domain: "redis",
|
|
741
|
+
safety: "read",
|
|
742
|
+
auth: true,
|
|
743
|
+
title: "Get a Redis database",
|
|
744
|
+
description: "Get one Redis (KV) database by id (no credentials).",
|
|
745
|
+
input: [idField("databaseId", "The Redis database id.")],
|
|
746
|
+
http: { method: "GET", path: "/api/v1/redis-databases/{databaseId}", operationId: "getRedisDatabase" },
|
|
747
|
+
cli: { command: ["redis", "get"], summary: "Get a Redis (KV) database", defaultOutput: "detail" },
|
|
748
|
+
mcp: { tool: { name: "khotan_redis_get", title: "Get a Redis database", description: "Get one Redis (KV) database by id (no credentials)." } }
|
|
749
|
+
},
|
|
750
|
+
{
|
|
751
|
+
id: "redis.create",
|
|
752
|
+
kind: "operation",
|
|
753
|
+
domain: "redis",
|
|
754
|
+
safety: "write",
|
|
755
|
+
auth: true,
|
|
756
|
+
title: "Create a Redis database",
|
|
757
|
+
description: "Provision a global Upstash Redis (KV) database. Not idempotent: retrying a succeeded create provisions a second database. Omit primaryRegion for the default region.",
|
|
758
|
+
input: [
|
|
759
|
+
{ name: "name", location: "body", type: "string", required: true, description: "The database name." },
|
|
760
|
+
{ name: "primaryRegion", location: "body", type: "string", description: "Optional primary region id; omit for the default region." }
|
|
761
|
+
],
|
|
762
|
+
http: { method: "POST", path: "/api/v1/redis-databases", operationId: "createRedisDatabase", bodyMode: "json" },
|
|
763
|
+
cli: { command: ["redis", "create"], summary: "Create a Redis (KV) database", defaultOutput: "detail" },
|
|
764
|
+
mcp: { tool: { name: "khotan_redis_create", title: "Create a Redis database", description: "Provision a global Upstash Redis (KV) database. Not idempotent — retrying provisions a second database." } }
|
|
765
|
+
},
|
|
766
|
+
{
|
|
767
|
+
id: "redis.delete",
|
|
768
|
+
kind: "operation",
|
|
769
|
+
domain: "redis",
|
|
770
|
+
safety: "destructive",
|
|
771
|
+
auth: true,
|
|
772
|
+
title: "Delete a Redis database",
|
|
773
|
+
description: "Permanently delete a Redis (KV) database and tear down its Upstash resources. Fails if deletion protection is enabled; remove protection from the dashboard first.",
|
|
774
|
+
input: [idField("databaseId", "The Redis database id.")],
|
|
775
|
+
http: { method: "DELETE", path: "/api/v1/redis-databases/{databaseId}", operationId: "deleteRedisDatabase" },
|
|
776
|
+
cli: { command: ["redis", "delete"], summary: "Delete a Redis (KV) database" },
|
|
777
|
+
mcp: { tool: { name: "khotan_redis_delete", title: "Delete a Redis database", description: "Permanently delete a Redis (KV) database. Requires confirmation; fails if deletion protection is enabled." } }
|
|
778
|
+
},
|
|
779
|
+
{
|
|
780
|
+
id: "redis.resource",
|
|
781
|
+
kind: "resource",
|
|
782
|
+
domain: "redis",
|
|
783
|
+
safety: "read",
|
|
784
|
+
auth: true,
|
|
785
|
+
title: "Redis database summary",
|
|
786
|
+
description: "Credential-free Redis (KV) database summary addressed by id.",
|
|
787
|
+
input: [idField("databaseId", "The Redis database id.")],
|
|
788
|
+
readsVia: "redis.get",
|
|
789
|
+
http: { method: "GET", path: "/api/v1/redis-databases/{databaseId}", operationId: "getRedisDatabase" },
|
|
790
|
+
mcp: {
|
|
791
|
+
resource: {
|
|
792
|
+
uriTemplate: "khotan://redis/{databaseId}",
|
|
793
|
+
name: "redis-database-summary",
|
|
794
|
+
title: "Redis database summary",
|
|
795
|
+
description: "Credential-free Redis (KV) database summary as JSON.",
|
|
796
|
+
mimeType: "application/json"
|
|
797
|
+
}
|
|
798
|
+
}
|
|
799
|
+
}
|
|
800
|
+
];
|
|
610
801
|
fileCapabilities = [
|
|
611
802
|
{
|
|
612
803
|
id: "files.list",
|
|
@@ -715,6 +906,19 @@ var init_catalog = __esm(() => {
|
|
|
715
906
|
cli: { command: ["files", "download-url"], summary: "Get a file download URL", defaultOutput: "detail" },
|
|
716
907
|
mcp: { tool: { name: "khotan_files_download", title: "Get a file download URL", description: "Return a short-lived presigned GET URL for a ready file." } }
|
|
717
908
|
},
|
|
909
|
+
{
|
|
910
|
+
id: "files.preview",
|
|
911
|
+
kind: "operation",
|
|
912
|
+
domain: "files",
|
|
913
|
+
safety: "read",
|
|
914
|
+
auth: true,
|
|
915
|
+
title: "Get a file preview URL",
|
|
916
|
+
description: "Return a short-lived presigned inline GET URL for a ready, preview-eligible file (images and PDFs).",
|
|
917
|
+
input: [idField("fileId", "The file id.")],
|
|
918
|
+
http: { method: "GET", path: "/api/v1/files/{fileId}/preview", operationId: "getFilePreviewUrl" },
|
|
919
|
+
cli: { command: ["files", "preview-url"], summary: "Get a file preview URL", defaultOutput: "detail" },
|
|
920
|
+
mcp: { tool: { name: "khotan_files_preview", title: "Get a file preview URL", description: "Return a short-lived presigned inline GET URL for a ready, preview-eligible file (images and PDFs)." } }
|
|
921
|
+
},
|
|
718
922
|
{
|
|
719
923
|
id: "files.resource",
|
|
720
924
|
kind: "resource",
|
|
@@ -973,14 +1177,127 @@ var init_catalog = __esm(() => {
|
|
|
973
1177
|
envDelete: "deletePipelineEnvVar"
|
|
974
1178
|
}
|
|
975
1179
|
});
|
|
1180
|
+
integrationsCapabilities = [
|
|
1181
|
+
{
|
|
1182
|
+
id: "integrations.list",
|
|
1183
|
+
kind: "operation",
|
|
1184
|
+
domain: "integrations",
|
|
1185
|
+
safety: "read",
|
|
1186
|
+
auth: true,
|
|
1187
|
+
title: "List integrations",
|
|
1188
|
+
description: "List the organization's connected mailbox integrations (no token material).",
|
|
1189
|
+
input: [],
|
|
1190
|
+
http: { method: "GET", path: "/api/v1/integrations", operationId: "listIntegrations" },
|
|
1191
|
+
cli: { command: ["integrations", "list"], summary: "List integrations", defaultOutput: "table" },
|
|
1192
|
+
mcp: { tool: { name: "khotan_integrations_list", title: "List integrations", description: "List the organization's connected mailbox integrations (no token material)." } }
|
|
1193
|
+
},
|
|
1194
|
+
{
|
|
1195
|
+
id: "integrations.get",
|
|
1196
|
+
kind: "operation",
|
|
1197
|
+
domain: "integrations",
|
|
1198
|
+
safety: "read",
|
|
1199
|
+
auth: true,
|
|
1200
|
+
title: "Get an integration",
|
|
1201
|
+
description: "Get one integration by id (no token material).",
|
|
1202
|
+
input: [idField("integrationId", "The integration id.")],
|
|
1203
|
+
http: { method: "GET", path: "/api/v1/integrations/{integrationId}", operationId: "getIntegration" },
|
|
1204
|
+
cli: { command: ["integrations", "get"], summary: "Get an integration", defaultOutput: "detail" },
|
|
1205
|
+
mcp: { tool: { name: "khotan_integrations_get", title: "Get an integration", description: "Get one integration by id (no token material)." } }
|
|
1206
|
+
},
|
|
1207
|
+
{
|
|
1208
|
+
id: "integrations.connect",
|
|
1209
|
+
kind: "operation",
|
|
1210
|
+
domain: "integrations",
|
|
1211
|
+
safety: "write",
|
|
1212
|
+
auth: true,
|
|
1213
|
+
title: "Start connecting an integration",
|
|
1214
|
+
description: "Begin a mailbox connection and return a URL a human opens in a browser to authorize (OAuth consent cannot be completed headlessly).",
|
|
1215
|
+
input: [
|
|
1216
|
+
{ name: "provider", location: "body", type: "string", description: "The provider to connect. Defaults to google.", default: "google" }
|
|
1217
|
+
],
|
|
1218
|
+
http: { method: "POST", path: "/api/v1/integrations", operationId: "connectIntegration", bodyMode: "json" },
|
|
1219
|
+
cli: { command: ["integrations", "connect"], summary: "Start connecting a mailbox", defaultOutput: "detail" },
|
|
1220
|
+
mcp: { tool: { name: "khotan_integrations_connect", title: "Start connecting an integration", description: "Begin a mailbox connection and return a browser URL to complete OAuth consent." } }
|
|
1221
|
+
},
|
|
1222
|
+
{
|
|
1223
|
+
id: "integrations.rename",
|
|
1224
|
+
kind: "operation",
|
|
1225
|
+
domain: "integrations",
|
|
1226
|
+
safety: "write",
|
|
1227
|
+
auth: true,
|
|
1228
|
+
title: "Rename an integration",
|
|
1229
|
+
description: "Set an integration's display name. Pass an empty value to clear it.",
|
|
1230
|
+
input: [
|
|
1231
|
+
idField("integrationId", "The integration id."),
|
|
1232
|
+
{ name: "name", location: "body", type: "string", required: true, description: "The display name (empty to clear)." }
|
|
1233
|
+
],
|
|
1234
|
+
http: { method: "PATCH", path: "/api/v1/integrations/{integrationId}", operationId: "updateIntegration", bodyMode: "json" },
|
|
1235
|
+
cli: { command: ["integrations", "rename"], summary: "Rename an integration", defaultOutput: "detail" },
|
|
1236
|
+
mcp: { tool: { name: "khotan_integrations_rename", title: "Rename an integration", description: "Set an integration's display name." } }
|
|
1237
|
+
},
|
|
1238
|
+
{
|
|
1239
|
+
id: "integrations.delete",
|
|
1240
|
+
kind: "operation",
|
|
1241
|
+
domain: "integrations",
|
|
1242
|
+
safety: "destructive",
|
|
1243
|
+
auth: true,
|
|
1244
|
+
title: "Disconnect an integration",
|
|
1245
|
+
description: "Permanently disconnect a mailbox integration (revokes the stored credentials).",
|
|
1246
|
+
input: [idField("integrationId", "The integration id.")],
|
|
1247
|
+
http: { method: "DELETE", path: "/api/v1/integrations/{integrationId}", operationId: "deleteIntegration" },
|
|
1248
|
+
cli: { command: ["integrations", "delete"], summary: "Disconnect an integration" },
|
|
1249
|
+
mcp: { tool: { name: "khotan_integrations_delete", title: "Disconnect an integration", description: "Permanently disconnect a mailbox integration. Requires confirmation." } }
|
|
1250
|
+
},
|
|
1251
|
+
{
|
|
1252
|
+
id: "integrations.token",
|
|
1253
|
+
kind: "operation",
|
|
1254
|
+
domain: "integrations",
|
|
1255
|
+
safety: "secret",
|
|
1256
|
+
auth: true,
|
|
1257
|
+
title: "Mint an access token (secret)",
|
|
1258
|
+
description: "Mint a fresh OAuth access token for the integration so a service can call the provider on the user's behalf. Secret-bearing; explicit-only.",
|
|
1259
|
+
input: [idField("integrationId", "The integration id.")],
|
|
1260
|
+
http: { method: "POST", path: "/api/v1/integrations/{integrationId}/token", operationId: "mintIntegrationToken", bodyMode: "none" },
|
|
1261
|
+
cli: { command: ["integrations", "token"], summary: "Mint an access token (secret)", defaultOutput: "detail" },
|
|
1262
|
+
mcp: { tool: { name: "khotan_integrations_token", title: "Mint an access token (secret)", description: "Mint a fresh OAuth access token for the integration. Secret-bearing — returns a credential." } }
|
|
1263
|
+
},
|
|
1264
|
+
{
|
|
1265
|
+
id: "integrations.resource",
|
|
1266
|
+
kind: "resource",
|
|
1267
|
+
domain: "integrations",
|
|
1268
|
+
safety: "read",
|
|
1269
|
+
auth: true,
|
|
1270
|
+
title: "Integration summary",
|
|
1271
|
+
description: "Credential-free integration summary addressed by id.",
|
|
1272
|
+
input: [idField("integrationId", "The integration id.")],
|
|
1273
|
+
readsVia: "integrations.get",
|
|
1274
|
+
http: { method: "GET", path: "/api/v1/integrations/{integrationId}", operationId: "getIntegration" },
|
|
1275
|
+
mcp: {
|
|
1276
|
+
resource: {
|
|
1277
|
+
uriTemplate: "khotan://integrations/{integrationId}",
|
|
1278
|
+
name: "integration-summary",
|
|
1279
|
+
title: "Integration summary",
|
|
1280
|
+
description: "Credential-free integration summary as JSON.",
|
|
1281
|
+
mimeType: "application/json"
|
|
1282
|
+
}
|
|
1283
|
+
}
|
|
1284
|
+
}
|
|
1285
|
+
];
|
|
1286
|
+
emailSubscriptionCapabilities = [
|
|
1287
|
+
...emailSubscriptionDomain({ domain: "apps", noun: "deployment", resourceParam: "appId", cap: "App" }),
|
|
1288
|
+
...emailSubscriptionDomain({ domain: "pipelines", noun: "pipeline", resourceParam: "pipelineId", cap: "Pipeline" })
|
|
1289
|
+
];
|
|
976
1290
|
capabilities = [
|
|
977
1291
|
...identityCapabilities,
|
|
978
1292
|
...appsCapabilities,
|
|
979
1293
|
...pipelinesCapabilities,
|
|
980
1294
|
...databaseCapabilities,
|
|
1295
|
+
...redisCapabilities,
|
|
981
1296
|
...fileCapabilities,
|
|
982
1297
|
...folderCapabilities,
|
|
983
|
-
...contextCapabilities
|
|
1298
|
+
...contextCapabilities,
|
|
1299
|
+
...integrationsCapabilities,
|
|
1300
|
+
...emailSubscriptionCapabilities
|
|
984
1301
|
];
|
|
985
1302
|
khotanCatalog = {
|
|
986
1303
|
schemaVersion: CATALOG_SCHEMA_VERSION,
|
|
@@ -1341,6 +1658,66 @@ var init_profiles = __esm(() => {
|
|
|
1341
1658
|
init_errors();
|
|
1342
1659
|
});
|
|
1343
1660
|
|
|
1661
|
+
// ../khotan-core/src/profiles/repo-env.ts
|
|
1662
|
+
import { existsSync, readFileSync as readFileSync2 } from "node:fs";
|
|
1663
|
+
import { dirname as dirname2, join as join2 } from "node:path";
|
|
1664
|
+
function findRepoEnvFile(startDir) {
|
|
1665
|
+
let dir = startDir;
|
|
1666
|
+
for (;; ) {
|
|
1667
|
+
const candidate = join2(dir, REPO_ENV_FILENAME);
|
|
1668
|
+
if (existsSync(candidate))
|
|
1669
|
+
return candidate;
|
|
1670
|
+
if (existsSync(join2(dir, ".git")))
|
|
1671
|
+
return;
|
|
1672
|
+
const parent = dirname2(dir);
|
|
1673
|
+
if (parent === dir)
|
|
1674
|
+
return;
|
|
1675
|
+
dir = parent;
|
|
1676
|
+
}
|
|
1677
|
+
}
|
|
1678
|
+
function parseRepoEnvFile(contents) {
|
|
1679
|
+
const map = new Map;
|
|
1680
|
+
for (const rawLine of contents.split(`
|
|
1681
|
+
`)) {
|
|
1682
|
+
const line = rawLine.trim();
|
|
1683
|
+
if (!line || line.startsWith("#"))
|
|
1684
|
+
continue;
|
|
1685
|
+
const eq = line.indexOf("=");
|
|
1686
|
+
if (eq === -1)
|
|
1687
|
+
continue;
|
|
1688
|
+
const key = line.slice(0, eq).trim();
|
|
1689
|
+
let value = line.slice(eq + 1).trim();
|
|
1690
|
+
if (value.startsWith("'") && value.endsWith("'") || value.startsWith('"') && value.endsWith('"')) {
|
|
1691
|
+
value = value.slice(1, -1);
|
|
1692
|
+
}
|
|
1693
|
+
map.set(key, value);
|
|
1694
|
+
}
|
|
1695
|
+
return map;
|
|
1696
|
+
}
|
|
1697
|
+
function overlayRepoEnv(env, options) {
|
|
1698
|
+
const filePath = findRepoEnvFile(options.cwd);
|
|
1699
|
+
if (!filePath)
|
|
1700
|
+
return { env };
|
|
1701
|
+
let parsed;
|
|
1702
|
+
try {
|
|
1703
|
+
parsed = parseRepoEnvFile(readFileSync2(filePath, "utf8"));
|
|
1704
|
+
} catch {
|
|
1705
|
+
return { env, filePath };
|
|
1706
|
+
}
|
|
1707
|
+
const merged = { ...env };
|
|
1708
|
+
for (const key of OVERLAY_KEYS) {
|
|
1709
|
+
const value = parsed.get(key)?.trim();
|
|
1710
|
+
if (value)
|
|
1711
|
+
merged[key] = value;
|
|
1712
|
+
}
|
|
1713
|
+
return { env: merged, filePath, orgId: merged[ENV_ORG_ID]?.trim() || undefined };
|
|
1714
|
+
}
|
|
1715
|
+
var ENV_ORG_ID = "KHOTAN_ORG_ID", REPO_ENV_FILENAME = "env.khotan.local", OVERLAY_KEYS;
|
|
1716
|
+
var init_repo_env = __esm(() => {
|
|
1717
|
+
init_profiles();
|
|
1718
|
+
OVERLAY_KEYS = [ENV_API_URL, ENV_API_KEY, ENV_PROFILE, ENV_ORG_ID];
|
|
1719
|
+
});
|
|
1720
|
+
|
|
1344
1721
|
// ../khotan-core/src/index.ts
|
|
1345
1722
|
var exports_src = {};
|
|
1346
1723
|
__export(exports_src, {
|
|
@@ -1351,6 +1728,8 @@ __export(exports_src, {
|
|
|
1351
1728
|
saveProfileStore: () => saveProfileStore,
|
|
1352
1729
|
resolveProfile: () => resolveProfile,
|
|
1353
1730
|
requiresConfirmation: () => requiresConfirmation,
|
|
1731
|
+
parseRepoEnvFile: () => parseRepoEnvFile,
|
|
1732
|
+
overlayRepoEnv: () => overlayRepoEnv,
|
|
1354
1733
|
loadProfileStore: () => loadProfileStore,
|
|
1355
1734
|
listDomains: () => listDomains,
|
|
1356
1735
|
listCapabilities: () => listCapabilities,
|
|
@@ -1364,12 +1743,14 @@ __export(exports_src, {
|
|
|
1364
1743
|
indexOpenApiOperations: () => indexOpenApiOperations,
|
|
1365
1744
|
getProfileStorePath: () => getProfileStorePath,
|
|
1366
1745
|
getCapability: () => getCapability,
|
|
1746
|
+
findRepoEnvFile: () => findRepoEnvFile,
|
|
1367
1747
|
createApiClient: () => createApiClient,
|
|
1368
1748
|
capabilitiesByOperationId: () => capabilitiesByOperationId,
|
|
1369
1749
|
buildHttpRequest: () => buildHttpRequest,
|
|
1370
1750
|
assertSupportedCatalogSchemaVersion: () => assertSupportedCatalogSchemaVersion,
|
|
1371
1751
|
SUPPORTED_CATALOG_SCHEMA_VERSIONS: () => SUPPORTED_CATALOG_SCHEMA_VERSIONS,
|
|
1372
1752
|
SAFETY_LEVELS: () => SAFETY_LEVELS,
|
|
1753
|
+
REPO_ENV_FILENAME: () => REPO_ENV_FILENAME,
|
|
1373
1754
|
KhotanUnknownCapabilityError: () => KhotanUnknownCapabilityError,
|
|
1374
1755
|
KhotanError: () => KhotanError,
|
|
1375
1756
|
KhotanConfirmationRequiredError: () => KhotanConfirmationRequiredError,
|
|
@@ -1379,6 +1760,7 @@ __export(exports_src, {
|
|
|
1379
1760
|
KHOTAN_ADAPTER_VERSION: () => KHOTAN_ADAPTER_VERSION,
|
|
1380
1761
|
KHOTAN_ADAPTER_NAME: () => KHOTAN_ADAPTER_NAME,
|
|
1381
1762
|
ENV_PROFILE: () => ENV_PROFILE,
|
|
1763
|
+
ENV_ORG_ID: () => ENV_ORG_ID,
|
|
1382
1764
|
ENV_API_URL: () => ENV_API_URL,
|
|
1383
1765
|
ENV_API_KEY: () => ENV_API_KEY,
|
|
1384
1766
|
CATALOG_VERSION: () => CATALOG_VERSION,
|
|
@@ -1392,6 +1774,7 @@ var init_src = __esm(() => {
|
|
|
1392
1774
|
init_validate();
|
|
1393
1775
|
init_api_client();
|
|
1394
1776
|
init_profiles();
|
|
1777
|
+
init_repo_env();
|
|
1395
1778
|
});
|
|
1396
1779
|
|
|
1397
1780
|
// src/cli/io.ts
|
|
@@ -1428,6 +1811,65 @@ function errLine(io, line = "") {
|
|
|
1428
1811
|
}
|
|
1429
1812
|
var init_io = () => {};
|
|
1430
1813
|
|
|
1814
|
+
// src/cli/assert-org.ts
|
|
1815
|
+
import { createHash } from "node:crypto";
|
|
1816
|
+
import { existsSync as existsSync3, mkdirSync as mkdirSync3, readFileSync as readFileSync4, writeFileSync as writeFileSync4 } from "node:fs";
|
|
1817
|
+
import { homedir as homedir2 } from "node:os";
|
|
1818
|
+
import { dirname as dirname4, join as join5 } from "node:path";
|
|
1819
|
+
function cacheDir(env) {
|
|
1820
|
+
const xdg = env.XDG_CACHE_HOME;
|
|
1821
|
+
const base = xdg && xdg.trim() ? xdg : join5(homedir2(), ".cache");
|
|
1822
|
+
return join5(base, "khotan");
|
|
1823
|
+
}
|
|
1824
|
+
function cacheFilePath(env, apiUrl, orgId) {
|
|
1825
|
+
const hash = createHash("sha256").update(`${apiUrl}
|
|
1826
|
+
${orgId}`).digest("hex").slice(0, 32);
|
|
1827
|
+
return join5(cacheDir(env), `org-assert-${hash}.json`);
|
|
1828
|
+
}
|
|
1829
|
+
function readAssertionCache(path, expectedOrgId) {
|
|
1830
|
+
if (!existsSync3(path))
|
|
1831
|
+
return false;
|
|
1832
|
+
try {
|
|
1833
|
+
const parsed = JSON.parse(readFileSync4(path, "utf8"));
|
|
1834
|
+
if (parsed.organizationId !== expectedOrgId)
|
|
1835
|
+
return false;
|
|
1836
|
+
if (Date.now() - parsed.verifiedAt > CACHE_TTL_MS)
|
|
1837
|
+
return false;
|
|
1838
|
+
return true;
|
|
1839
|
+
} catch {
|
|
1840
|
+
return false;
|
|
1841
|
+
}
|
|
1842
|
+
}
|
|
1843
|
+
function writeAssertionCache(path, organizationId) {
|
|
1844
|
+
try {
|
|
1845
|
+
mkdirSync3(dirname4(path), { recursive: true });
|
|
1846
|
+
const payload = { organizationId, verifiedAt: Date.now() };
|
|
1847
|
+
writeFileSync4(path, `${JSON.stringify(payload)}
|
|
1848
|
+
`);
|
|
1849
|
+
} catch {}
|
|
1850
|
+
}
|
|
1851
|
+
async function assertOrg(session, expectedOrgId, options = {}) {
|
|
1852
|
+
const expected = expectedOrgId.trim();
|
|
1853
|
+
if (!expected)
|
|
1854
|
+
return;
|
|
1855
|
+
const env = options.env ?? process.env;
|
|
1856
|
+
const cachePath = cacheFilePath(env, session.apiUrl, expected);
|
|
1857
|
+
if (!options.skipCache && readAssertionCache(cachePath, expected)) {
|
|
1858
|
+
return;
|
|
1859
|
+
}
|
|
1860
|
+
const principal = await session.client.execute("identity.whoami");
|
|
1861
|
+
const actual = String(principal.organizationId ?? "");
|
|
1862
|
+
if (actual !== expected) {
|
|
1863
|
+
throw new KhotanClientError("org_mismatch", `Khotan org mismatch: expected organizationId=${expected} (KHOTAN_ORG_ID / --assert-org), ` + `but whoami returned organizationId=${actual || "(empty)"}. ` + `You may be authenticated to a different customer's org. ` + `Fix credentials in env.khotan.local, pass --assert-org, or select a per-customer profile.`);
|
|
1864
|
+
}
|
|
1865
|
+
writeAssertionCache(cachePath, actual);
|
|
1866
|
+
}
|
|
1867
|
+
var CACHE_TTL_MS;
|
|
1868
|
+
var init_assert_org = __esm(() => {
|
|
1869
|
+
init_src();
|
|
1870
|
+
CACHE_TTL_MS = 5 * 60 * 1000;
|
|
1871
|
+
});
|
|
1872
|
+
|
|
1431
1873
|
// src/cli/session.ts
|
|
1432
1874
|
function resolveSession(options) {
|
|
1433
1875
|
const resolved = resolveProfile({
|
|
@@ -1447,8 +1889,17 @@ function resolveSession(options) {
|
|
|
1447
1889
|
apiKey
|
|
1448
1890
|
};
|
|
1449
1891
|
}
|
|
1892
|
+
async function resolveSessionAsserted(options) {
|
|
1893
|
+
const session = resolveSession(options);
|
|
1894
|
+
const expectedOrgId = options.assertOrgOverride?.trim() || options.env[ENV_ORG_ID]?.trim();
|
|
1895
|
+
if (expectedOrgId) {
|
|
1896
|
+
await assertOrg(session, expectedOrgId, { env: options.env });
|
|
1897
|
+
}
|
|
1898
|
+
return session;
|
|
1899
|
+
}
|
|
1450
1900
|
var init_session = __esm(() => {
|
|
1451
1901
|
init_src();
|
|
1902
|
+
init_assert_org();
|
|
1452
1903
|
});
|
|
1453
1904
|
|
|
1454
1905
|
// src/mcp/protocol.ts
|
|
@@ -1699,13 +2150,14 @@ __export(exports_serve, {
|
|
|
1699
2150
|
import { createInterface as createInterface2 } from "node:readline";
|
|
1700
2151
|
async function serveMcp(deps) {
|
|
1701
2152
|
assertSupportedCatalogSchemaVersion();
|
|
1702
|
-
const session =
|
|
2153
|
+
const session = await resolveSessionAsserted({
|
|
1703
2154
|
env: deps.env,
|
|
1704
2155
|
storeOptions: deps.storeOptions,
|
|
1705
2156
|
profileName: deps.profileName,
|
|
1706
2157
|
apiUrlOverride: deps.apiUrlOverride,
|
|
1707
2158
|
apiKeyOverride: deps.apiKeyOverride,
|
|
1708
|
-
fetch: deps.fetch
|
|
2159
|
+
fetch: deps.fetch,
|
|
2160
|
+
assertOrgOverride: deps.assertOrgOverride
|
|
1709
2161
|
});
|
|
1710
2162
|
if (!session.apiKey) {
|
|
1711
2163
|
throw new KhotanClientError("not_authenticated", "The MCP server needs an API key. Run `khotan auth set-key` or set KHOTAN_API_KEY.");
|
|
@@ -2079,8 +2531,8 @@ async function downloadFile(deps, options) {
|
|
|
2079
2531
|
|
|
2080
2532
|
// src/cli/commands/init.ts
|
|
2081
2533
|
init_io();
|
|
2082
|
-
import { existsSync, mkdirSync as mkdirSync2, readFileSync as
|
|
2083
|
-
import { dirname as
|
|
2534
|
+
import { existsSync as existsSync2, mkdirSync as mkdirSync2, readdirSync, readFileSync as readFileSync3, writeFileSync as writeFileSync2 } from "node:fs";
|
|
2535
|
+
import { dirname as dirname3, join as join3, relative } from "node:path";
|
|
2084
2536
|
|
|
2085
2537
|
// src/cli/skills.ts
|
|
2086
2538
|
init_src();
|
|
@@ -2187,9 +2639,11 @@ var DOMAIN_TITLES = {
|
|
|
2187
2639
|
apps: "Apps",
|
|
2188
2640
|
pipelines: "Pipelines",
|
|
2189
2641
|
databases: "Databases",
|
|
2642
|
+
redis: "Redis (KV)",
|
|
2190
2643
|
files: "Files",
|
|
2191
2644
|
folders: "Folders",
|
|
2192
|
-
context: "Context documents"
|
|
2645
|
+
context: "Context documents",
|
|
2646
|
+
integrations: "Integrations"
|
|
2193
2647
|
};
|
|
2194
2648
|
function commandWords(capability) {
|
|
2195
2649
|
return capability.cli?.command.join(" ");
|
|
@@ -2255,6 +2709,8 @@ function printTopLevelHelp(io) {
|
|
|
2255
2709
|
outLine(io, " files download <fileId> Download a file to --output (presigned flow)");
|
|
2256
2710
|
outLine(io);
|
|
2257
2711
|
outLine(io, "Workspace:");
|
|
2712
|
+
outLine(io, " status Show the workspace topology (apps, pipelines, databases)");
|
|
2713
|
+
outLine(io, " status --write Refresh a committable WORKSPACE.md from live data");
|
|
2258
2714
|
outLine(io, " init Scaffold MCP config + agent guidance into this workspace");
|
|
2259
2715
|
outLine(io);
|
|
2260
2716
|
outLine(io, "Agent:");
|
|
@@ -2266,10 +2722,33 @@ function printTopLevelHelp(io) {
|
|
|
2266
2722
|
outLine(io, " --profile <name> Use a specific stored profile");
|
|
2267
2723
|
outLine(io, " --api-url <url> Override the API origin");
|
|
2268
2724
|
outLine(io, " --api-key <key> Override the API key");
|
|
2725
|
+
outLine(io, " --assert-org <id> Require whoami.organizationId to equal <id> (fail closed)");
|
|
2269
2726
|
outLine(io, " --help Show help for a command");
|
|
2270
2727
|
outLine(io);
|
|
2271
2728
|
outLine(io, `khotan v${KHOTAN_ADAPTER_VERSION}`);
|
|
2272
2729
|
}
|
|
2730
|
+
function groupCommands(prefix) {
|
|
2731
|
+
if (prefix.length === 0) {
|
|
2732
|
+
return [];
|
|
2733
|
+
}
|
|
2734
|
+
return listCapabilities().filter((capability) => {
|
|
2735
|
+
if (!isOperationCapability(capability) || !capability.cli) {
|
|
2736
|
+
return false;
|
|
2737
|
+
}
|
|
2738
|
+
const words = capability.cli.command;
|
|
2739
|
+
return words.length > prefix.length && prefix.every((word, index) => words[index] === word);
|
|
2740
|
+
});
|
|
2741
|
+
}
|
|
2742
|
+
function printGroupHelp(io, prefix, commands) {
|
|
2743
|
+
outLine(io, `khotan ${prefix.join(" ")} — commands`);
|
|
2744
|
+
outLine(io);
|
|
2745
|
+
for (const capability of commands) {
|
|
2746
|
+
const words = commandWords(capability) ?? capability.id;
|
|
2747
|
+
outLine(io, ` ${words.padEnd(30)} ${capability.cli?.summary ?? capability.title}`);
|
|
2748
|
+
}
|
|
2749
|
+
outLine(io);
|
|
2750
|
+
outLine(io, "Run `khotan <command> --help` for details.");
|
|
2751
|
+
}
|
|
2273
2752
|
function printCommandHelp(io, capability) {
|
|
2274
2753
|
outLine(io, capability.title);
|
|
2275
2754
|
outLine(io);
|
|
@@ -2302,6 +2781,7 @@ var DOMAIN_TITLES2 = {
|
|
|
2302
2781
|
apps: "Apps",
|
|
2303
2782
|
pipelines: "Pipelines",
|
|
2304
2783
|
databases: "Databases",
|
|
2784
|
+
redis: "Redis (KV)",
|
|
2305
2785
|
files: "Files",
|
|
2306
2786
|
folders: "Folders",
|
|
2307
2787
|
context: "Context documents"
|
|
@@ -2310,6 +2790,7 @@ var DOMAIN_LABELS = {
|
|
|
2310
2790
|
apps: "apps",
|
|
2311
2791
|
pipelines: "pipelines",
|
|
2312
2792
|
databases: "databases and database branches",
|
|
2793
|
+
redis: "Redis (KV) databases",
|
|
2313
2794
|
files: "files",
|
|
2314
2795
|
folders: "folders",
|
|
2315
2796
|
context: "context documents"
|
|
@@ -2484,32 +2965,54 @@ var SKILL_BASES = [".cursor", ".agents", ".claude"];
|
|
|
2484
2965
|
function mcpServerEntry() {
|
|
2485
2966
|
return {
|
|
2486
2967
|
command: "khotan",
|
|
2487
|
-
args: ["mcp", "serve"]
|
|
2488
|
-
env: {
|
|
2489
|
-
KHOTAN_API_URL: "<your-khotan-api-url>",
|
|
2490
|
-
KHOTAN_API_KEY: "<your-khotan-api-key>"
|
|
2491
|
-
}
|
|
2968
|
+
args: ["mcp", "serve"]
|
|
2492
2969
|
};
|
|
2493
2970
|
}
|
|
2494
2971
|
var GUIDE_BODY = `This workspace is wired to Khotan through the \`khotan\` CLI and its MCP server.
|
|
2495
2972
|
Use them to inspect and configure apps, pipelines, databases, files, folders, and
|
|
2496
2973
|
context documents through the Khotan \`/api/v1\` surface.
|
|
2497
2974
|
|
|
2975
|
+
## Org isolation (never cross customers)
|
|
2976
|
+
|
|
2977
|
+
Khotan credentials are machine-global unless scoped per repo. Pin this repo to
|
|
2978
|
+
its organization with \`KHOTAN_ORG_ID\` in \`env.khotan.local\`: the CLI and MCP
|
|
2979
|
+
server load that file and assert \`whoami.organizationId === KHOTAN_ORG_ID\`
|
|
2980
|
+
before trusting any output, failing closed on mismatch. You can also pass
|
|
2981
|
+
\`--assert-org <id>\` on any command.
|
|
2982
|
+
|
|
2983
|
+
If \`apps list\` / \`context list\` returns another customer's data (foreign product
|
|
2984
|
+
names, wrong org slug), **stop** — credentials are wrong. Do not infer
|
|
2985
|
+
deployment or env from CLI output until the org assertion passes.
|
|
2986
|
+
|
|
2498
2987
|
## Authentication (do this once — never commit secrets)
|
|
2499
2988
|
|
|
2500
|
-
|
|
2501
|
-
|
|
2989
|
+
Repo credentials live in \`env.khotan.local\` (\`KHOTAN_API_URL\`, \`KHOTAN_API_KEY\`,
|
|
2990
|
+
\`KHOTAN_ORG_ID\`), which the CLI auto-loads. Alternatively:
|
|
2502
2991
|
|
|
2503
|
-
- Profile
|
|
2504
|
-
|
|
2505
|
-
|
|
2506
|
-
|
|
2992
|
+
- Profile: \`khotan auth set-key --api-url <url> --api-key <key>\` stores the key in
|
|
2993
|
+
an owner-only file outside the repository (use a per-customer profile name, not
|
|
2994
|
+
\`default\`).
|
|
2995
|
+
- Environment: export \`KHOTAN_API_URL\`, \`KHOTAN_API_KEY\`, and \`KHOTAN_ORG_ID\`.
|
|
2507
2996
|
|
|
2508
2997
|
Never put an API key in a file committed to the repository. The generated MCP
|
|
2509
|
-
config
|
|
2998
|
+
config carries no secrets — the server resolves credentials at startup.
|
|
2999
|
+
|
|
3000
|
+
## Workspace layout
|
|
3001
|
+
|
|
3002
|
+
A Khotan workspace is an **organization mono-root repo**: one GitHub repo whose
|
|
3003
|
+
top-level subdirectories are each an independent deployable (an app or a
|
|
3004
|
+
pipeline). The repo root holds shared agent skills and instruction files;
|
|
3005
|
+
per-app guidance lives in each subdirectory's own \`AGENTS.md\` / \`CLAUDE.md\`.
|
|
3006
|
+
|
|
3007
|
+
- Run \`khotan status\` to see which subdirectory maps to which app/pipeline and
|
|
3008
|
+
database, and which repo it deploys from. \`khotan status --write\` refreshes a
|
|
3009
|
+
committable \`WORKSPACE.md\` with that map.
|
|
3010
|
+
- Each app lives in its own subdirectory — **do not run app scaffolders (e.g.
|
|
3011
|
+
\`khotan-data init\`) at the repo root.** \`cd\` into the app's subdirectory first.
|
|
2510
3012
|
|
|
2511
3013
|
## CLI surface
|
|
2512
3014
|
|
|
3015
|
+
- \`khotan status\` — the workspace topology (apps, pipelines, databases) at a glance
|
|
2513
3016
|
- \`khotan apps list|get|create|delete|redeploy\` and \`khotan apps env …\`
|
|
2514
3017
|
- \`khotan pipelines …\`, \`khotan databases …\`
|
|
2515
3018
|
- \`khotan files list|upload|download|…\`, \`khotan folders …\`
|
|
@@ -2529,7 +3032,9 @@ surface.
|
|
|
2529
3032
|
## MCP
|
|
2530
3033
|
|
|
2531
3034
|
\`khotan mcp serve\` exposes the same operations as MCP tools and durable reads as
|
|
2532
|
-
MCP resources,
|
|
3035
|
+
MCP resources, scoping credentials from \`env.khotan.local\` (or your stored
|
|
3036
|
+
profile) and asserting the org at startup. Restart the MCP server after changing
|
|
3037
|
+
\`env.khotan.local\`.
|
|
2533
3038
|
`;
|
|
2534
3039
|
var CURSOR_RULES = `---
|
|
2535
3040
|
description: Use the Khotan CLI and MCP tools to configure and operate this workspace's Khotan resources
|
|
@@ -2545,31 +3050,59 @@ ${GUIDE_BODY}`;
|
|
|
2545
3050
|
function toRelative(cwd, absolutePath) {
|
|
2546
3051
|
return relative(cwd, absolutePath) || absolutePath;
|
|
2547
3052
|
}
|
|
3053
|
+
function detectMonoRoot(cwd) {
|
|
3054
|
+
if (hasWorkspacesManifest(join3(cwd, "package.json")))
|
|
3055
|
+
return true;
|
|
3056
|
+
if (existsSync2(join3(cwd, "turbo.json")))
|
|
3057
|
+
return true;
|
|
3058
|
+
if (existsSync2(join3(cwd, "WORKSPACE.md")))
|
|
3059
|
+
return true;
|
|
3060
|
+
if (existsSync2(join3(cwd, "package.json")))
|
|
3061
|
+
return false;
|
|
3062
|
+
try {
|
|
3063
|
+
return readdirSync(cwd, { withFileTypes: true }).some((entry) => entry.isDirectory() && !entry.name.startsWith(".") && entry.name !== "node_modules" && existsSync2(join3(cwd, entry.name, "package.json")));
|
|
3064
|
+
} catch {
|
|
3065
|
+
return false;
|
|
3066
|
+
}
|
|
3067
|
+
}
|
|
3068
|
+
function hasWorkspacesManifest(packageJsonPath) {
|
|
3069
|
+
if (!existsSync2(packageJsonPath))
|
|
3070
|
+
return false;
|
|
3071
|
+
try {
|
|
3072
|
+
const parsed = JSON.parse(readFileSync3(packageJsonPath, "utf8"));
|
|
3073
|
+
if (typeof parsed !== "object" || parsed === null)
|
|
3074
|
+
return false;
|
|
3075
|
+
const workspaces = parsed.workspaces;
|
|
3076
|
+
return Array.isArray(workspaces) && workspaces.length > 0;
|
|
3077
|
+
} catch {
|
|
3078
|
+
return false;
|
|
3079
|
+
}
|
|
3080
|
+
}
|
|
2548
3081
|
function writeTextAsset(ctx, absolutePath, content) {
|
|
2549
3082
|
const path = toRelative(ctx.cwd, absolutePath);
|
|
2550
|
-
const existed =
|
|
3083
|
+
const existed = existsSync2(absolutePath);
|
|
2551
3084
|
if (existed && !ctx.force) {
|
|
2552
3085
|
ctx.results.push({ path, action: "skipped" });
|
|
2553
3086
|
return;
|
|
2554
3087
|
}
|
|
2555
|
-
mkdirSync2(
|
|
3088
|
+
mkdirSync2(dirname3(absolutePath), { recursive: true });
|
|
2556
3089
|
writeFileSync2(absolutePath, content);
|
|
2557
3090
|
ctx.results.push({ path, action: existed ? "forced" : "created" });
|
|
2558
3091
|
}
|
|
2559
3092
|
function writeManagedAsset(ctx, absolutePath, content) {
|
|
2560
3093
|
const path = toRelative(ctx.cwd, absolutePath);
|
|
2561
|
-
const existed =
|
|
2562
|
-
mkdirSync2(
|
|
3094
|
+
const existed = existsSync2(absolutePath);
|
|
3095
|
+
mkdirSync2(dirname3(absolutePath), { recursive: true });
|
|
2563
3096
|
writeFileSync2(absolutePath, content);
|
|
2564
3097
|
ctx.results.push({ path, action: existed ? "updated" : "created" });
|
|
2565
3098
|
}
|
|
2566
3099
|
function writeMcpConfig(ctx, absolutePath) {
|
|
2567
3100
|
const path = toRelative(ctx.cwd, absolutePath);
|
|
2568
3101
|
let config = {};
|
|
2569
|
-
const existed =
|
|
3102
|
+
const existed = existsSync2(absolutePath);
|
|
2570
3103
|
if (existed) {
|
|
2571
3104
|
try {
|
|
2572
|
-
config = JSON.parse(
|
|
3105
|
+
config = JSON.parse(readFileSync3(absolutePath, "utf8"));
|
|
2573
3106
|
} catch {
|
|
2574
3107
|
ctx.results.push({ path, action: "skipped" });
|
|
2575
3108
|
return;
|
|
@@ -2585,7 +3118,7 @@ function writeMcpConfig(ctx, absolutePath) {
|
|
|
2585
3118
|
return;
|
|
2586
3119
|
}
|
|
2587
3120
|
servers[MCP_SERVER_KEY] = mcpServerEntry();
|
|
2588
|
-
mkdirSync2(
|
|
3121
|
+
mkdirSync2(dirname3(absolutePath), { recursive: true });
|
|
2589
3122
|
writeFileSync2(absolutePath, `${JSON.stringify(config, null, 2)}
|
|
2590
3123
|
`);
|
|
2591
3124
|
ctx.results.push({
|
|
@@ -2597,19 +3130,20 @@ function runInit(options) {
|
|
|
2597
3130
|
const { io, cwd, client, force, json } = options;
|
|
2598
3131
|
const ctx = { cwd, force, results: [] };
|
|
2599
3132
|
if (client === "cursor") {
|
|
2600
|
-
writeMcpConfig(ctx,
|
|
2601
|
-
writeTextAsset(ctx,
|
|
3133
|
+
writeMcpConfig(ctx, join3(cwd, ".cursor", "mcp.json"));
|
|
3134
|
+
writeTextAsset(ctx, join3(cwd, ".cursor", "rules", "khotan.mdc"), CURSOR_RULES);
|
|
2602
3135
|
} else {
|
|
2603
|
-
writeMcpConfig(ctx,
|
|
2604
|
-
writeTextAsset(ctx,
|
|
3136
|
+
writeMcpConfig(ctx, join3(cwd, "mcp.json"));
|
|
3137
|
+
writeTextAsset(ctx, join3(cwd, "khotan-agents.md"), GENERIC_GUIDE);
|
|
2605
3138
|
}
|
|
2606
3139
|
for (const skill of generateSkills()) {
|
|
2607
3140
|
for (const base of SKILL_BASES) {
|
|
2608
|
-
writeManagedAsset(ctx,
|
|
3141
|
+
writeManagedAsset(ctx, join3(cwd, base, "skills", skill.name, "SKILL.md"), skill.content);
|
|
2609
3142
|
}
|
|
2610
3143
|
}
|
|
3144
|
+
const monoRoot = detectMonoRoot(cwd);
|
|
2611
3145
|
if (json) {
|
|
2612
|
-
io.out(`${JSON.stringify({ client, actions: ctx.results }, null, 2)}
|
|
3146
|
+
io.out(`${JSON.stringify({ client, monoRoot, actions: ctx.results }, null, 2)}
|
|
2613
3147
|
`);
|
|
2614
3148
|
return 0;
|
|
2615
3149
|
}
|
|
@@ -2618,12 +3152,322 @@ function runInit(options) {
|
|
|
2618
3152
|
errLine(io, ` ${result.action.padEnd(8)} ${result.path}`);
|
|
2619
3153
|
}
|
|
2620
3154
|
errLine(io);
|
|
3155
|
+
if (monoRoot) {
|
|
3156
|
+
errLine(io, "Detected a Khotan mono-root workspace (multiple apps under one repo).");
|
|
3157
|
+
errLine(io, "Each app lives in its own subdirectory — do NOT run app scaffolders");
|
|
3158
|
+
errLine(io, "(e.g. `khotan-data init`) at this root. `cd` into an app subdirectory");
|
|
3159
|
+
errLine(io, "first. Run `khotan status` to see the layout.");
|
|
3160
|
+
errLine(io);
|
|
3161
|
+
}
|
|
2621
3162
|
errLine(io, "Next steps:");
|
|
2622
3163
|
errLine(io, " 1. Authenticate: khotan auth set-key --api-url <url> --api-key <key>");
|
|
2623
3164
|
errLine(io, " 2. Open this workspace in your agent and ask it to configure Khotan.");
|
|
2624
3165
|
return 0;
|
|
2625
3166
|
}
|
|
2626
3167
|
|
|
3168
|
+
// src/cli/commands/status.ts
|
|
3169
|
+
init_io();
|
|
3170
|
+
import { writeFileSync as writeFileSync3 } from "node:fs";
|
|
3171
|
+
import { join as join4, relative as relative2 } from "node:path";
|
|
3172
|
+
function str(value) {
|
|
3173
|
+
return typeof value === "string" && value.length > 0 ? value : null;
|
|
3174
|
+
}
|
|
3175
|
+
function record(value) {
|
|
3176
|
+
return value && typeof value === "object" && !Array.isArray(value) ? value : null;
|
|
3177
|
+
}
|
|
3178
|
+
function listOf(result, key) {
|
|
3179
|
+
const container = record(result);
|
|
3180
|
+
const candidate = container && Array.isArray(container[key]) && container[key] || Array.isArray(result) && result || container && soleArrayProperty(container) || [];
|
|
3181
|
+
return candidate.map(record).filter((r) => r !== null);
|
|
3182
|
+
}
|
|
3183
|
+
function soleArrayProperty(container) {
|
|
3184
|
+
const arrays = Object.keys(container).filter((k) => Array.isArray(container[k]));
|
|
3185
|
+
return arrays.length === 1 ? container[arrays[0]] : null;
|
|
3186
|
+
}
|
|
3187
|
+
function toResourceRow(kind, raw) {
|
|
3188
|
+
const repo = record(raw.repo);
|
|
3189
|
+
return {
|
|
3190
|
+
kind,
|
|
3191
|
+
name: str(raw.displayName) ?? str(raw.slug) ?? "(unnamed)",
|
|
3192
|
+
slug: str(raw.slug) ?? "",
|
|
3193
|
+
status: str(raw.status) ?? "unknown",
|
|
3194
|
+
subdirectory: repo ? str(repo.subdirectory) : null,
|
|
3195
|
+
repo: repo ? str(repo.fullName) : null,
|
|
3196
|
+
branch: repo ? str(repo.defaultBranch) : null,
|
|
3197
|
+
hostname: str(raw.liveUrl) ?? str(raw.primaryHostname) ?? null
|
|
3198
|
+
};
|
|
3199
|
+
}
|
|
3200
|
+
function toDatabaseRow(raw) {
|
|
3201
|
+
return {
|
|
3202
|
+
name: str(raw.displayName) ?? "(unnamed)",
|
|
3203
|
+
status: str(raw.status) ?? "unknown",
|
|
3204
|
+
environment: str(raw.environment),
|
|
3205
|
+
region: str(raw.regionId),
|
|
3206
|
+
host: str(raw.endpointHost)
|
|
3207
|
+
};
|
|
3208
|
+
}
|
|
3209
|
+
function toRedisRow(raw) {
|
|
3210
|
+
return {
|
|
3211
|
+
name: str(raw.displayName) ?? "(unnamed)",
|
|
3212
|
+
status: str(raw.status) ?? "unknown",
|
|
3213
|
+
environment: null,
|
|
3214
|
+
region: str(raw.primaryRegion),
|
|
3215
|
+
host: str(raw.endpointHost)
|
|
3216
|
+
};
|
|
3217
|
+
}
|
|
3218
|
+
async function readList(session, capabilityId, domain, errors, map, responseKey = domain) {
|
|
3219
|
+
try {
|
|
3220
|
+
const result = await session.client.execute(capabilityId);
|
|
3221
|
+
return map(listOf(result, responseKey));
|
|
3222
|
+
} catch (error) {
|
|
3223
|
+
errors.push({
|
|
3224
|
+
domain,
|
|
3225
|
+
message: error instanceof Error ? error.message : String(error)
|
|
3226
|
+
});
|
|
3227
|
+
return [];
|
|
3228
|
+
}
|
|
3229
|
+
}
|
|
3230
|
+
async function loadTopology(session) {
|
|
3231
|
+
const errors = [];
|
|
3232
|
+
const apps = await readList(session, "apps.list", "apps", errors, (rows) => rows.map((r) => toResourceRow("app", r)));
|
|
3233
|
+
const pipelines = await readList(session, "pipelines.list", "pipelines", errors, (rows) => rows.map((r) => toResourceRow("pipeline", r)));
|
|
3234
|
+
const databases = await readList(session, "databases.list", "databases", errors, (rows) => rows.map(toDatabaseRow));
|
|
3235
|
+
const redisDatabases = await readList(session, "redis.list", "redis", errors, (rows) => rows.map(toRedisRow), "databases");
|
|
3236
|
+
const repoBearer = [...apps, ...pipelines].find((r) => r.repo);
|
|
3237
|
+
return {
|
|
3238
|
+
repo: repoBearer?.repo ?? null,
|
|
3239
|
+
defaultBranch: repoBearer?.branch ?? null,
|
|
3240
|
+
apps,
|
|
3241
|
+
pipelines,
|
|
3242
|
+
databases,
|
|
3243
|
+
redisDatabases,
|
|
3244
|
+
errors
|
|
3245
|
+
};
|
|
3246
|
+
}
|
|
3247
|
+
var EM_DASH = "—";
|
|
3248
|
+
function table(rows) {
|
|
3249
|
+
if (rows.length === 0) {
|
|
3250
|
+
return [" (none)"];
|
|
3251
|
+
}
|
|
3252
|
+
const columns = Object.keys(rows[0]);
|
|
3253
|
+
const widths = columns.map((column) => Math.max(column.length, ...rows.map((row) => (row[column] ?? "").length)));
|
|
3254
|
+
const format = (cells) => ` ${cells.map((cell, index) => cell.padEnd(widths[index] ?? 0)).join(" ").trimEnd()}`;
|
|
3255
|
+
return [
|
|
3256
|
+
format(columns),
|
|
3257
|
+
format(widths.map((width) => "-".repeat(width))),
|
|
3258
|
+
...rows.map((row) => format(columns.map((column) => row[column] ?? "")))
|
|
3259
|
+
];
|
|
3260
|
+
}
|
|
3261
|
+
function resourceTable(rows) {
|
|
3262
|
+
return rows.map((row) => ({
|
|
3263
|
+
subdir: row.subdirectory ?? EM_DASH,
|
|
3264
|
+
name: row.name,
|
|
3265
|
+
status: row.status,
|
|
3266
|
+
hostname: row.hostname ?? EM_DASH
|
|
3267
|
+
}));
|
|
3268
|
+
}
|
|
3269
|
+
function deployables(topology) {
|
|
3270
|
+
return [...topology.apps, ...topology.pipelines];
|
|
3271
|
+
}
|
|
3272
|
+
function deployableLayout(topology) {
|
|
3273
|
+
if (topology.errors.some((error) => error.domain === "apps" || error.domain === "pipelines")) {
|
|
3274
|
+
return "partial";
|
|
3275
|
+
}
|
|
3276
|
+
const rows = deployables(topology);
|
|
3277
|
+
if (rows.length === 0)
|
|
3278
|
+
return "empty";
|
|
3279
|
+
const withSubdir = rows.filter((row) => row.subdirectory).length;
|
|
3280
|
+
if (withSubdir === rows.length)
|
|
3281
|
+
return "subdirectory";
|
|
3282
|
+
if (withSubdir === 0)
|
|
3283
|
+
return "whole-repo";
|
|
3284
|
+
return "mixed";
|
|
3285
|
+
}
|
|
3286
|
+
function renderLayoutIntro(io, topology) {
|
|
3287
|
+
const layout = deployableLayout(topology);
|
|
3288
|
+
if (layout === "subdirectory") {
|
|
3289
|
+
outLine(io, "Every app and pipeline below lives as a subdirectory of one shared");
|
|
3290
|
+
outLine(io, "organization mono-root repo — that repo is the deploy source. Each");
|
|
3291
|
+
outLine(io, "subdirectory is an independent deployable. Databases are separate");
|
|
3292
|
+
outLine(io, "resources, linked to apps via env vars (`khotan apps env list`), not");
|
|
3293
|
+
outLine(io, "by directory.");
|
|
3294
|
+
return;
|
|
3295
|
+
}
|
|
3296
|
+
if (layout === "mixed") {
|
|
3297
|
+
outLine(io, "Some deployables below live in subdirectories of the deploy repo;");
|
|
3298
|
+
outLine(io, "rows marked with — use the repo root or have no subdirectory metadata.");
|
|
3299
|
+
} else if (layout === "whole-repo") {
|
|
3300
|
+
outLine(io, "Deployables below use the deploy repo root or have no subdirectory");
|
|
3301
|
+
outLine(io, "metadata, rather than an organization mono-root subdirectory layout.");
|
|
3302
|
+
} else if (layout === "partial") {
|
|
3303
|
+
outLine(io, "Some app or pipeline data could not be read, so the deployable");
|
|
3304
|
+
outLine(io, "layout below is incomplete. Treat missing deployables as unknown.");
|
|
3305
|
+
} else {
|
|
3306
|
+
outLine(io, "No app or pipeline deployables were returned yet.");
|
|
3307
|
+
}
|
|
3308
|
+
outLine(io, "Databases are separate resources, linked to apps via env vars");
|
|
3309
|
+
outLine(io, "(`khotan apps env list`), not by directory.");
|
|
3310
|
+
}
|
|
3311
|
+
function renderHuman(io, topology) {
|
|
3312
|
+
outLine(io, "Workspace topology");
|
|
3313
|
+
outLine(io);
|
|
3314
|
+
renderLayoutIntro(io, topology);
|
|
3315
|
+
outLine(io);
|
|
3316
|
+
outLine(io, `Deploy repo: ${topology.repo ?? "(none provisioned yet)"}` + (topology.defaultBranch ? ` (branch: ${topology.defaultBranch})` : ""));
|
|
3317
|
+
outLine(io);
|
|
3318
|
+
outLine(io, `Apps (${topology.apps.length})`);
|
|
3319
|
+
for (const line of table(resourceTable(topology.apps)))
|
|
3320
|
+
outLine(io, line);
|
|
3321
|
+
outLine(io);
|
|
3322
|
+
outLine(io, `Pipelines (${topology.pipelines.length})`);
|
|
3323
|
+
for (const line of table(resourceTable(topology.pipelines)))
|
|
3324
|
+
outLine(io, line);
|
|
3325
|
+
outLine(io);
|
|
3326
|
+
outLine(io, `Databases (${topology.databases.length})`);
|
|
3327
|
+
for (const line of table(topology.databases.map((db) => ({
|
|
3328
|
+
name: db.name,
|
|
3329
|
+
status: db.status,
|
|
3330
|
+
region: db.region ?? EM_DASH,
|
|
3331
|
+
host: db.host ?? EM_DASH
|
|
3332
|
+
})))) {
|
|
3333
|
+
outLine(io, line);
|
|
3334
|
+
}
|
|
3335
|
+
outLine(io);
|
|
3336
|
+
outLine(io, `Redis (KV) (${topology.redisDatabases.length})`);
|
|
3337
|
+
for (const line of table(topology.redisDatabases.map((db) => ({
|
|
3338
|
+
name: db.name,
|
|
3339
|
+
status: db.status,
|
|
3340
|
+
region: db.region ?? EM_DASH,
|
|
3341
|
+
host: db.host ?? EM_DASH
|
|
3342
|
+
})))) {
|
|
3343
|
+
outLine(io, line);
|
|
3344
|
+
}
|
|
3345
|
+
for (const failure of topology.errors) {
|
|
3346
|
+
errLine(io, `! could not read ${failure.domain}: ${failure.message}`);
|
|
3347
|
+
}
|
|
3348
|
+
}
|
|
3349
|
+
function markdownTable(headers, rows) {
|
|
3350
|
+
const lines = [
|
|
3351
|
+
`| ${headers.join(" | ")} |`,
|
|
3352
|
+
`| ${headers.map(() => "---").join(" | ")} |`,
|
|
3353
|
+
...rows.map((row) => `| ${row.join(" | ")} |`)
|
|
3354
|
+
];
|
|
3355
|
+
return lines.join(`
|
|
3356
|
+
`);
|
|
3357
|
+
}
|
|
3358
|
+
function mdCell(value) {
|
|
3359
|
+
return value ? value : EM_DASH;
|
|
3360
|
+
}
|
|
3361
|
+
function workspaceIntro(topology) {
|
|
3362
|
+
const layout = deployableLayout(topology);
|
|
3363
|
+
if (layout === "subdirectory") {
|
|
3364
|
+
return [
|
|
3365
|
+
"This directory is one **organization mono-root repo**: a single GitHub repo",
|
|
3366
|
+
"whose top-level subdirectories are each an independent Khotan deployable (an",
|
|
3367
|
+
"app or a pipeline). Cloning the repo (or a sparse subdirectory checkout) gives",
|
|
3368
|
+
"you the code; each subdirectory deploys on its own from this same repo.",
|
|
3369
|
+
"",
|
|
3370
|
+
"**Do not run app scaffolders (e.g. `khotan-data init`) at this root.** Each",
|
|
3371
|
+
"app already lives in its own subdirectory — `cd` into it first."
|
|
3372
|
+
];
|
|
3373
|
+
}
|
|
3374
|
+
if (layout === "mixed") {
|
|
3375
|
+
return [
|
|
3376
|
+
"This file records Khotan deployables for this repository. Some deployables",
|
|
3377
|
+
"live in subdirectories; deployables marked with — use the repo root or have",
|
|
3378
|
+
"no subdirectory metadata.",
|
|
3379
|
+
"",
|
|
3380
|
+
"For deployables with a subdirectory, `cd` into that directory before running",
|
|
3381
|
+
"app scaffolders. For rows marked —, verify the resource layout before",
|
|
3382
|
+
"scaffolding."
|
|
3383
|
+
];
|
|
3384
|
+
}
|
|
3385
|
+
if (layout === "whole-repo") {
|
|
3386
|
+
return [
|
|
3387
|
+
"This file records Khotan deployables for this repository. The returned",
|
|
3388
|
+
"deployables use the repo root or have no subdirectory metadata, so this is",
|
|
3389
|
+
"not described as an organization mono-root subdirectory layout."
|
|
3390
|
+
];
|
|
3391
|
+
}
|
|
3392
|
+
if (layout === "partial") {
|
|
3393
|
+
return [
|
|
3394
|
+
"This file records Khotan deployables for this repository. Some app or",
|
|
3395
|
+
"pipeline data could not be read, so the deployable layout is incomplete.",
|
|
3396
|
+
"Use the Read Errors section before deciding where to run scaffolders."
|
|
3397
|
+
];
|
|
3398
|
+
}
|
|
3399
|
+
return [
|
|
3400
|
+
"This file records Khotan deployables for this repository. No apps or pipelines",
|
|
3401
|
+
"were returned yet; re-run `khotan status --write` after provisioning resources",
|
|
3402
|
+
"to capture their deploy mapping."
|
|
3403
|
+
];
|
|
3404
|
+
}
|
|
3405
|
+
function errorsSection(errors) {
|
|
3406
|
+
if (errors.length === 0)
|
|
3407
|
+
return [];
|
|
3408
|
+
return [
|
|
3409
|
+
"",
|
|
3410
|
+
"## Read Errors",
|
|
3411
|
+
"",
|
|
3412
|
+
"Some live data could not be read while generating this file. Treat affected",
|
|
3413
|
+
"empty sections as unknown, not as confirmed empty.",
|
|
3414
|
+
"",
|
|
3415
|
+
markdownTable(["Domain", "Error"], errors.map((error) => [error.domain, error.message]))
|
|
3416
|
+
];
|
|
3417
|
+
}
|
|
3418
|
+
function buildWorkspaceManifest(topology) {
|
|
3419
|
+
const rows = deployables(topology);
|
|
3420
|
+
const sections = [];
|
|
3421
|
+
sections.push("# Workspace", "", "> Generated by `khotan status --write`. Re-run to refresh; safe to commit.", "", ...workspaceIntro(topology), "", `- **Deploy repo:** ${topology.repo ? `\`${topology.repo}\`` : "_(none provisioned yet)_"}` + (topology.defaultBranch ? ` (branch \`${topology.defaultBranch}\`)` : ""));
|
|
3422
|
+
sections.push("", "## Deployables", "", rows.length === 0 ? "_No apps or pipelines provisioned yet._" : markdownTable(["Subdirectory", "Resource", "Type", "Status", "Live URL"], rows.map((row) => [
|
|
3423
|
+
row.subdirectory ? `\`${row.subdirectory}\`` : EM_DASH,
|
|
3424
|
+
row.name,
|
|
3425
|
+
row.kind,
|
|
3426
|
+
row.status,
|
|
3427
|
+
row.hostname ? `https://${row.hostname.replace(/^https?:\/\//, "")}` : EM_DASH
|
|
3428
|
+
])));
|
|
3429
|
+
sections.push("", "## Databases", "", "Databases are separate resources. An app connects to one through environment", "variables (`khotan apps env list <appId>`), not by directory.", "", topology.databases.length === 0 ? "_No databases provisioned yet._" : markdownTable(["Name", "Status", "Environment", "Region", "Host"], topology.databases.map((db) => [
|
|
3430
|
+
db.name,
|
|
3431
|
+
db.status,
|
|
3432
|
+
mdCell(db.environment),
|
|
3433
|
+
mdCell(db.region),
|
|
3434
|
+
mdCell(db.host)
|
|
3435
|
+
])));
|
|
3436
|
+
sections.push("", "## Redis (KV)", "", "Redis (KV) databases are separate resources. An app connects to one through", "environment variables (`khotan apps env list <appId>`), not by directory.", "", topology.redisDatabases.length === 0 ? "_No Redis (KV) databases provisioned yet._" : markdownTable(["Name", "Status", "Region", "Host"], topology.redisDatabases.map((db) => [
|
|
3437
|
+
db.name,
|
|
3438
|
+
db.status,
|
|
3439
|
+
mdCell(db.region),
|
|
3440
|
+
mdCell(db.host)
|
|
3441
|
+
])));
|
|
3442
|
+
sections.push(...errorsSection(topology.errors));
|
|
3443
|
+
sections.push("", "## Working here", "", "- `khotan status` — reprint this topology (`--json` for machine output).", "- `khotan status --write` — regenerate this `WORKSPACE.md` from live data.", "- `khotan apps list` · `khotan pipelines list` · `khotan databases list` · `khotan redis list` — per-domain detail.", "");
|
|
3444
|
+
return sections.join(`
|
|
3445
|
+
`);
|
|
3446
|
+
}
|
|
3447
|
+
async function runStatus(options) {
|
|
3448
|
+
const { session, io, json, writeDir } = options;
|
|
3449
|
+
const topology = await loadTopology(session);
|
|
3450
|
+
if (writeDir) {
|
|
3451
|
+
const target = join4(writeDir, "WORKSPACE.md");
|
|
3452
|
+
writeFileSync3(target, buildWorkspaceManifest(topology));
|
|
3453
|
+
const shown = relative2(process.cwd(), target) || target;
|
|
3454
|
+
if (json) {
|
|
3455
|
+
outLine(io, JSON.stringify({ wrote: shown, topology }, null, 2));
|
|
3456
|
+
} else {
|
|
3457
|
+
renderHuman(io, topology);
|
|
3458
|
+
errLine(io);
|
|
3459
|
+
errLine(io, `Wrote ${shown}`);
|
|
3460
|
+
}
|
|
3461
|
+
return 0;
|
|
3462
|
+
}
|
|
3463
|
+
if (json) {
|
|
3464
|
+
outLine(io, JSON.stringify(topology, null, 2));
|
|
3465
|
+
return 0;
|
|
3466
|
+
}
|
|
3467
|
+
renderHuman(io, topology);
|
|
3468
|
+
return 0;
|
|
3469
|
+
}
|
|
3470
|
+
|
|
2627
3471
|
// src/cli/confirm.ts
|
|
2628
3472
|
init_src();
|
|
2629
3473
|
init_io();
|
|
@@ -2679,13 +3523,14 @@ async function runCapability(match, args, options) {
|
|
|
2679
3523
|
printCommandHelp(options.io, capability);
|
|
2680
3524
|
return 0;
|
|
2681
3525
|
}
|
|
2682
|
-
const session =
|
|
3526
|
+
const session = await resolveSessionAsserted({
|
|
2683
3527
|
env: options.env,
|
|
2684
3528
|
storeOptions: options.storeOptions,
|
|
2685
3529
|
profileName: options.profileName,
|
|
2686
3530
|
apiUrlOverride: options.apiUrlOverride,
|
|
2687
3531
|
apiKeyOverride: options.apiKeyOverride,
|
|
2688
|
-
fetch: options.fetch
|
|
3532
|
+
fetch: options.fetch,
|
|
3533
|
+
assertOrgOverride: options.assertOrgOverride
|
|
2689
3534
|
});
|
|
2690
3535
|
const input = buildCapabilityInput(capability, match.rest, args);
|
|
2691
3536
|
const json = options.jsonFlag || session.resolved.defaultOutput === "json";
|
|
@@ -2702,7 +3547,7 @@ async function runCapability(match, args, options) {
|
|
|
2702
3547
|
}
|
|
2703
3548
|
async function run(argv, options = {}) {
|
|
2704
3549
|
const io = options.io ?? createNodeIo();
|
|
2705
|
-
const env = options.env ?? io.env;
|
|
3550
|
+
const env = overlayRepoEnv(options.env ?? io.env, { cwd: process.cwd() }).env;
|
|
2706
3551
|
const args = parseArgs(argv);
|
|
2707
3552
|
const { positionals } = args;
|
|
2708
3553
|
const jsonFlag = flagBool(args, "json");
|
|
@@ -2711,6 +3556,7 @@ async function run(argv, options = {}) {
|
|
|
2711
3556
|
const profileName = flagValue(args, "profile");
|
|
2712
3557
|
const apiUrlOverride = flagValue(args, "api-url");
|
|
2713
3558
|
const apiKeyOverride = flagValue(args, "api-key");
|
|
3559
|
+
const assertOrgOverride = flagValue(args, "assert-org");
|
|
2714
3560
|
if (flagBool(args, "version")) {
|
|
2715
3561
|
const { KHOTAN_ADAPTER_VERSION: KHOTAN_ADAPTER_VERSION2 } = await Promise.resolve().then(() => (init_src(), exports_src));
|
|
2716
3562
|
io.out(`${KHOTAN_ADAPTER_VERSION2}
|
|
@@ -2734,7 +3580,12 @@ async function run(argv, options = {}) {
|
|
|
2734
3580
|
if (match2 && isOperationCapability(match2.capability)) {
|
|
2735
3581
|
printCommandHelp(io, match2.capability);
|
|
2736
3582
|
} else {
|
|
2737
|
-
|
|
3583
|
+
const group2 = groupCommands(topic);
|
|
3584
|
+
if (group2.length > 0) {
|
|
3585
|
+
printGroupHelp(io, topic, group2);
|
|
3586
|
+
} else {
|
|
3587
|
+
printTopLevelHelp(io);
|
|
3588
|
+
}
|
|
2738
3589
|
}
|
|
2739
3590
|
return 0;
|
|
2740
3591
|
}
|
|
@@ -2750,6 +3601,26 @@ async function run(argv, options = {}) {
|
|
|
2750
3601
|
const cwd = flagValue(args, "dir") ?? process.cwd();
|
|
2751
3602
|
return runInit({ io, cwd, client: clientRaw, force: flagBool(args, "force"), json: jsonFlag });
|
|
2752
3603
|
}
|
|
3604
|
+
if (first === "status") {
|
|
3605
|
+
if (helpFlag) {
|
|
3606
|
+
errLine(io, "Usage: khotan status [--write [path]] [--dir <path>] [--json]");
|
|
3607
|
+
return 0;
|
|
3608
|
+
}
|
|
3609
|
+
const session = await resolveSessionAsserted({
|
|
3610
|
+
env,
|
|
3611
|
+
storeOptions: options.storeOptions,
|
|
3612
|
+
profileName,
|
|
3613
|
+
apiUrlOverride,
|
|
3614
|
+
apiKeyOverride,
|
|
3615
|
+
fetch: options.fetch,
|
|
3616
|
+
assertOrgOverride
|
|
3617
|
+
});
|
|
3618
|
+
const json = jsonFlag || session.resolved.defaultOutput === "json";
|
|
3619
|
+
const writeValue = flagValue(args, "write");
|
|
3620
|
+
const write = flagBool(args, "write");
|
|
3621
|
+
const writeDir = write ? flagValue(args, "dir") ?? (writeValue && writeValue !== "true" ? writeValue : undefined) ?? process.cwd() : undefined;
|
|
3622
|
+
return await runStatus({ session, io, json, writeDir });
|
|
3623
|
+
}
|
|
2753
3624
|
if (first === "mcp" && positionals[1] === "serve") {
|
|
2754
3625
|
const launcher = options.startMcpServer ?? (async (deps) => {
|
|
2755
3626
|
const { serveMcp: serveMcp2 } = await Promise.resolve().then(() => (init_serve(), exports_serve));
|
|
@@ -2762,7 +3633,8 @@ async function run(argv, options = {}) {
|
|
|
2762
3633
|
fetch: options.fetch,
|
|
2763
3634
|
profileName,
|
|
2764
3635
|
apiUrlOverride,
|
|
2765
|
-
apiKeyOverride
|
|
3636
|
+
apiKeyOverride,
|
|
3637
|
+
assertOrgOverride
|
|
2766
3638
|
});
|
|
2767
3639
|
}
|
|
2768
3640
|
if (first === "login") {
|
|
@@ -2795,13 +3667,14 @@ async function run(argv, options = {}) {
|
|
|
2795
3667
|
errLine(io, positionals[1] === "upload" ? "Usage: khotan files upload <localPath> [--name <n>] [--folder-id <id>] [--folder-path <p>] [--content-type <t>]" : "Usage: khotan files download <fileId> --output <path>");
|
|
2796
3668
|
return 0;
|
|
2797
3669
|
}
|
|
2798
|
-
const session =
|
|
3670
|
+
const session = await resolveSessionAsserted({
|
|
2799
3671
|
env,
|
|
2800
3672
|
storeOptions: options.storeOptions,
|
|
2801
3673
|
profileName,
|
|
2802
3674
|
apiUrlOverride,
|
|
2803
3675
|
apiKeyOverride,
|
|
2804
|
-
fetch: options.fetch
|
|
3676
|
+
fetch: options.fetch,
|
|
3677
|
+
assertOrgOverride
|
|
2805
3678
|
});
|
|
2806
3679
|
const json = jsonFlag || session.resolved.defaultOutput === "json";
|
|
2807
3680
|
if (positionals[1] === "upload") {
|
|
@@ -2834,9 +3707,15 @@ async function run(argv, options = {}) {
|
|
|
2834
3707
|
yes,
|
|
2835
3708
|
profileName,
|
|
2836
3709
|
apiUrlOverride,
|
|
2837
|
-
apiKeyOverride
|
|
3710
|
+
apiKeyOverride,
|
|
3711
|
+
assertOrgOverride
|
|
2838
3712
|
});
|
|
2839
3713
|
}
|
|
3714
|
+
const group = groupCommands(positionals);
|
|
3715
|
+
if (group.length > 0) {
|
|
3716
|
+
printGroupHelp(io, positionals, group);
|
|
3717
|
+
return 0;
|
|
3718
|
+
}
|
|
2840
3719
|
errLine(io, `Unknown command "${positionals.join(" ")}". Run \`khotan help\` for usage.`);
|
|
2841
3720
|
return 1;
|
|
2842
3721
|
} catch (error) {
|