@edgestore/cli 1.0.0-next.2 → 1.0.0-next.4
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 +137 -7
- package/agent-assets/skills.json +9 -0
- package/dist/bin.mjs +2079 -513
- package/package.json +10 -4
package/dist/bin.mjs
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { createRequire } from 'node:module';
|
|
3
|
+
import { fileURLToPath } from 'node:url';
|
|
3
4
|
import { CommanderError, Command } from 'commander';
|
|
4
|
-
import { randomUUID } from 'node:crypto';
|
|
5
|
-
import { rm, rmdir, readFile, mkdir, writeFile, rename, stat,
|
|
5
|
+
import { randomUUID, createHash } from 'node:crypto';
|
|
6
|
+
import { rm, rmdir, readFile, mkdir, writeFile, rename, stat, unlink, lstat, readdir, realpath, access, chmod, glob } from 'node:fs/promises';
|
|
6
7
|
import path from 'node:path';
|
|
7
8
|
import { z } from 'zod';
|
|
8
9
|
import { EdgeStoreApiError, EdgeStoreNetworkError, EdgeStoreTimeoutError, EdgeStoreAbortError, createEdgeStoreSdk, EdgeStoreUploadCleanupError } from '@edgestore/sdk';
|
|
@@ -13,13 +14,19 @@ import open from 'open';
|
|
|
13
14
|
import * as oauth from 'openid-client';
|
|
14
15
|
import { createServer } from 'node:http';
|
|
15
16
|
import { password, isCancel, text, confirm, select } from '@clack/prompts';
|
|
16
|
-
import {
|
|
17
|
+
import { parseTree, findNodeAtLocation, applyEdits, modify } from 'jsonc-parser';
|
|
18
|
+
import { parse } from 'smol-toml';
|
|
19
|
+
import { homedir } from 'node:os';
|
|
17
20
|
import { getPackages } from '@manypkg/get-packages';
|
|
21
|
+
import { Writable, Readable } from 'node:stream';
|
|
22
|
+
import { detect } from 'package-manager-detector/detect';
|
|
23
|
+
import { findRoot, NoPkgJsonFound } from '@manypkg/find-root';
|
|
24
|
+
import { parse as parse$1 } from '@babel/parser';
|
|
25
|
+
import { isIdentifier, traverseFast, isStringLiteral, isObjectProperty } from '@babel/types';
|
|
18
26
|
import { createWriteStream, constants, openAsBlob } from 'node:fs';
|
|
19
|
-
import { Readable, Writable } from 'node:stream';
|
|
20
27
|
import { pipeline } from 'node:stream/promises';
|
|
21
|
-
import { detect } from 'package-manager-detector/detect';
|
|
22
28
|
import { promisify } from 'node:util';
|
|
29
|
+
import { createLogUpdate } from 'log-update';
|
|
23
30
|
|
|
24
31
|
function renderCliCommand(flags, command, options = {}) {
|
|
25
32
|
const args = [
|
|
@@ -133,7 +140,7 @@ function resolveApiUrl(flagValue, envValue) {
|
|
|
133
140
|
try {
|
|
134
141
|
url = new URL(rawValue);
|
|
135
142
|
} catch {
|
|
136
|
-
throw usageError('invalid_api_url',
|
|
143
|
+
throw usageError('invalid_api_url', 'Invalid EdgeStore API URL. Supply an http or https origin.');
|
|
137
144
|
}
|
|
138
145
|
if (url.protocol !== 'http:' && url.protocol !== 'https:') {
|
|
139
146
|
throw usageError('invalid_api_url', 'The EdgeStore API URL must use http or https.');
|
|
@@ -414,12 +421,21 @@ const oauthCredentialSchema = z.object({
|
|
|
414
421
|
resource: z.string().url(),
|
|
415
422
|
scope: z.string().optional()
|
|
416
423
|
}).strict();
|
|
417
|
-
const oauthClientRegistrationSchema = z.
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
424
|
+
const oauthClientRegistrationSchema = z.discriminatedUnion('flow', [
|
|
425
|
+
z.object({
|
|
426
|
+
version: z.literal(2),
|
|
427
|
+
flow: z.literal('browser'),
|
|
428
|
+
clientId: z.string().min(1),
|
|
429
|
+
issuer: z.string().url(),
|
|
430
|
+
redirectUri: z.string().url()
|
|
431
|
+
}).strict(),
|
|
432
|
+
z.object({
|
|
433
|
+
version: z.literal(2),
|
|
434
|
+
flow: z.literal('device'),
|
|
435
|
+
clientId: z.string().min(1),
|
|
436
|
+
issuer: z.string().url()
|
|
437
|
+
}).strict()
|
|
438
|
+
]);
|
|
423
439
|
class KeyringCredentialStore {
|
|
424
440
|
async get(apiOrigin) {
|
|
425
441
|
const entry = await createEntry(CREDENTIAL_NAME, apiOrigin);
|
|
@@ -735,6 +751,7 @@ const resourceMetadataSchema = z.object({
|
|
|
735
751
|
scopes_supported: z.array(z.string().min(1)).min(1),
|
|
736
752
|
bearer_methods_supported: z.array(z.string()).optional()
|
|
737
753
|
}).passthrough();
|
|
754
|
+
const deviceGrantType = 'urn:ietf:params:oauth:grant-type:device_code';
|
|
738
755
|
class DefaultOAuthService {
|
|
739
756
|
fetchImplementation;
|
|
740
757
|
constructor(fetchImplementation = fetch){
|
|
@@ -753,6 +770,19 @@ class DefaultOAuthService {
|
|
|
753
770
|
});
|
|
754
771
|
}
|
|
755
772
|
}
|
|
773
|
+
async loginWithDeviceCode(input) {
|
|
774
|
+
try {
|
|
775
|
+
return await this.performDeviceLoginWithRecovery(input);
|
|
776
|
+
} catch (error) {
|
|
777
|
+
if (input.signal.aborted || error instanceof CliError) throw error;
|
|
778
|
+
throw new CliError('oauth_login_failed', oauthErrorMessage(error), {
|
|
779
|
+
suggestions: [
|
|
780
|
+
'edgestore login --device',
|
|
781
|
+
'edgestore login --token'
|
|
782
|
+
]
|
|
783
|
+
});
|
|
784
|
+
}
|
|
785
|
+
}
|
|
756
786
|
async performLoginWithRecovery(input) {
|
|
757
787
|
try {
|
|
758
788
|
return await this.performLogin(input);
|
|
@@ -764,6 +794,17 @@ class DefaultOAuthService {
|
|
|
764
794
|
});
|
|
765
795
|
}
|
|
766
796
|
}
|
|
797
|
+
async performDeviceLoginWithRecovery(input) {
|
|
798
|
+
try {
|
|
799
|
+
return await this.performDeviceLogin(input);
|
|
800
|
+
} catch (error) {
|
|
801
|
+
if (!input.client || !isInvalidClientError(error)) throw error;
|
|
802
|
+
return await this.performDeviceLogin({
|
|
803
|
+
...input,
|
|
804
|
+
client: undefined
|
|
805
|
+
});
|
|
806
|
+
}
|
|
807
|
+
}
|
|
767
808
|
async refresh(credential, signal) {
|
|
768
809
|
try {
|
|
769
810
|
const config = await this.discover(new URL(credential.issuer), {
|
|
@@ -782,10 +823,11 @@ class DefaultOAuthService {
|
|
|
782
823
|
});
|
|
783
824
|
} catch (error) {
|
|
784
825
|
if (signal.aborted || error instanceof CliError) throw error;
|
|
785
|
-
throw new CliError('oauth_refresh_failed', 'The
|
|
826
|
+
throw new CliError('oauth_refresh_failed', 'The OAuth login could not be refreshed.', {
|
|
786
827
|
details: oauthErrorMessage(error),
|
|
787
828
|
suggestions: [
|
|
788
829
|
'edgestore login',
|
|
830
|
+
'edgestore login --device',
|
|
789
831
|
'edgestore login --token'
|
|
790
832
|
]
|
|
791
833
|
});
|
|
@@ -819,7 +861,8 @@ class DefaultOAuthService {
|
|
|
819
861
|
throw new CliError('oauth_registration_failed', 'The OAuth server did not return a client ID.');
|
|
820
862
|
}
|
|
821
863
|
const clientRegistration = {
|
|
822
|
-
version:
|
|
864
|
+
version: 2,
|
|
865
|
+
flow: 'browser',
|
|
823
866
|
clientId,
|
|
824
867
|
issuer: issuerIdentifier,
|
|
825
868
|
redirectUri: callback.redirectUri
|
|
@@ -864,13 +907,69 @@ class DefaultOAuthService {
|
|
|
864
907
|
await callback.close();
|
|
865
908
|
}
|
|
866
909
|
}
|
|
910
|
+
async performDeviceLogin(input) {
|
|
911
|
+
const metadata = await this.protectedResourceMetadata(input.apiOrigin, input.resource, input.signal);
|
|
912
|
+
const issuer = new URL(metadata.authorization_servers[0]);
|
|
913
|
+
const issuerIdentifier = normalizeUrl(issuer.toString());
|
|
914
|
+
const reusableClient = reusableDeviceClientRegistration(input.client, issuer);
|
|
915
|
+
const config = reusableClient ? await this.discover(issuer, {
|
|
916
|
+
clientId: reusableClient.clientId,
|
|
917
|
+
signal: input.signal
|
|
918
|
+
}) : await this.registerDeviceClient(issuer, input.signal);
|
|
919
|
+
const clientId = config.clientMetadata().client_id;
|
|
920
|
+
if (!clientId) {
|
|
921
|
+
throw new CliError('oauth_registration_failed', 'The OAuth server did not return a client ID.');
|
|
922
|
+
}
|
|
923
|
+
const clientRegistration = reusableClient ?? {
|
|
924
|
+
version: 2,
|
|
925
|
+
flow: 'device',
|
|
926
|
+
clientId,
|
|
927
|
+
issuer: issuerIdentifier
|
|
928
|
+
};
|
|
929
|
+
if (!reusableClient) {
|
|
930
|
+
await input.onClientRegistered?.(clientRegistration);
|
|
931
|
+
}
|
|
932
|
+
const authorization = await oauth.initiateDeviceAuthorization(config, {
|
|
933
|
+
resource: metadata.resource,
|
|
934
|
+
scope: [
|
|
935
|
+
...new Set(metadata.scopes_supported)
|
|
936
|
+
].join(' ')
|
|
937
|
+
});
|
|
938
|
+
input.onDeviceAuthorization?.({
|
|
939
|
+
userCode: authorization.user_code,
|
|
940
|
+
verificationUri: authorization.verification_uri,
|
|
941
|
+
...authorization.verification_uri_complete ? {
|
|
942
|
+
verificationUriComplete: authorization.verification_uri_complete
|
|
943
|
+
} : {},
|
|
944
|
+
expiresIn: authorization.expires_in
|
|
945
|
+
});
|
|
946
|
+
const verificationUrl = authorization.verification_uri_complete ?? authorization.verification_uri;
|
|
947
|
+
try {
|
|
948
|
+
await input.openUrl(verificationUrl);
|
|
949
|
+
} catch (error) {
|
|
950
|
+
input.onBrowserOpenFailed?.(verificationUrl, error);
|
|
951
|
+
}
|
|
952
|
+
const tokens = await oauth.pollDeviceAuthorizationGrant(config, authorization, {
|
|
953
|
+
resource: metadata.resource
|
|
954
|
+
}, {
|
|
955
|
+
signal: input.signal
|
|
956
|
+
});
|
|
957
|
+
return {
|
|
958
|
+
credential: credentialFromTokens(tokens, {
|
|
959
|
+
clientId,
|
|
960
|
+
issuer: issuerIdentifier,
|
|
961
|
+
resource: metadata.resource
|
|
962
|
+
}),
|
|
963
|
+
client: clientRegistration
|
|
964
|
+
};
|
|
965
|
+
}
|
|
867
966
|
async protectedResourceMetadata(apiOrigin, expectedResource, signal) {
|
|
868
967
|
const metadataUrl = new URL('/.well-known/oauth-protected-resource/v2', apiOrigin);
|
|
869
968
|
const response = await this.fetchImplementation(metadataUrl, {
|
|
870
969
|
signal
|
|
871
970
|
});
|
|
872
971
|
if (!response.ok) {
|
|
873
|
-
throw new CliError('oauth_metadata_unavailable', `The EdgeStore API does not advertise
|
|
972
|
+
throw new CliError('oauth_metadata_unavailable', `The EdgeStore API does not advertise OAuth login (${response.status}).`, {
|
|
874
973
|
suggestions: [
|
|
875
974
|
'edgestore login --token'
|
|
876
975
|
]
|
|
@@ -904,6 +1003,19 @@ class DefaultOAuthService {
|
|
|
904
1003
|
]
|
|
905
1004
|
}, oauth.None(), this.discoveryOptions(issuer, signal));
|
|
906
1005
|
}
|
|
1006
|
+
async registerDeviceClient(issuer, signal) {
|
|
1007
|
+
return await oauth.dynamicClientRegistration(issuer, {
|
|
1008
|
+
application_type: 'native',
|
|
1009
|
+
client_name: 'EdgeStore CLI',
|
|
1010
|
+
redirect_uris: [],
|
|
1011
|
+
token_endpoint_auth_method: 'none',
|
|
1012
|
+
grant_types: [
|
|
1013
|
+
deviceGrantType,
|
|
1014
|
+
'refresh_token'
|
|
1015
|
+
],
|
|
1016
|
+
response_types: []
|
|
1017
|
+
}, oauth.None(), this.discoveryOptions(issuer, signal));
|
|
1018
|
+
}
|
|
907
1019
|
async discover(issuer, options) {
|
|
908
1020
|
return await oauth.discovery(issuer, options.clientId, {
|
|
909
1021
|
token_endpoint_auth_method: 'none',
|
|
@@ -950,7 +1062,13 @@ async function openCallback(state, signal, preferredRedirectUri) {
|
|
|
950
1062
|
}
|
|
951
1063
|
}
|
|
952
1064
|
function reusableClientRegistration(client, issuer) {
|
|
953
|
-
|
|
1065
|
+
if (client?.flow !== 'browser' || normalizeUrl(client.issuer) !== normalizeUrl(issuer.toString()) || !isReusableOAuthRedirectUri(client.redirectUri)) {
|
|
1066
|
+
return undefined;
|
|
1067
|
+
}
|
|
1068
|
+
return client;
|
|
1069
|
+
}
|
|
1070
|
+
function reusableDeviceClientRegistration(client, issuer) {
|
|
1071
|
+
return client?.flow === 'device' && normalizeUrl(client.issuer) === normalizeUrl(issuer.toString()) ? client : undefined;
|
|
954
1072
|
}
|
|
955
1073
|
function credentialFromTokens(tokens, context) {
|
|
956
1074
|
const refreshToken = tokens.refresh_token ?? context.fallbackRefreshToken;
|
|
@@ -973,7 +1091,7 @@ function normalizeUrl(value) {
|
|
|
973
1091
|
return new URL(value).toString().replace(/\/$/, '');
|
|
974
1092
|
}
|
|
975
1093
|
function isInvalidClientError(error) {
|
|
976
|
-
return (error instanceof oauth.AuthorizationResponseError || error instanceof oauth.ResponseBodyError) && error.error === 'invalid_client';
|
|
1094
|
+
return (error instanceof oauth.AuthorizationResponseError || error instanceof oauth.ResponseBodyError) && (error.error === 'invalid_client' || error.error === 'unauthorized_client');
|
|
977
1095
|
}
|
|
978
1096
|
function isLoopback(url) {
|
|
979
1097
|
return url.hostname === 'localhost' || url.hostname === '127.0.0.1';
|
|
@@ -983,7 +1101,7 @@ function isAddressInUse(error) {
|
|
|
983
1101
|
}
|
|
984
1102
|
function oauthErrorMessage(error) {
|
|
985
1103
|
if (error instanceof Error && error.message) return error.message;
|
|
986
|
-
return '
|
|
1104
|
+
return 'OAuth login failed.';
|
|
987
1105
|
}
|
|
988
1106
|
|
|
989
1107
|
class DefaultCliPrompts {
|
|
@@ -1070,7 +1188,8 @@ function createDefaultRuntime(signal) {
|
|
|
1070
1188
|
stdout: process.stdout,
|
|
1071
1189
|
stderr: process.stderr,
|
|
1072
1190
|
inputIsTty: Boolean(process.stdin.isTTY),
|
|
1073
|
-
outputIsTty: Boolean(process.stdout.isTTY)
|
|
1191
|
+
outputIsTty: Boolean(process.stdout.isTTY),
|
|
1192
|
+
stderrIsTty: Boolean(process.stderr.isTTY)
|
|
1074
1193
|
},
|
|
1075
1194
|
signal,
|
|
1076
1195
|
setCwd (cwd) {
|
|
@@ -1354,6 +1473,545 @@ function missingAccountError() {
|
|
|
1354
1473
|
]);
|
|
1355
1474
|
}
|
|
1356
1475
|
|
|
1476
|
+
const MCP_URL = 'https://api.edgestore.dev/mcp';
|
|
1477
|
+
const CLIENTS = [
|
|
1478
|
+
'codex',
|
|
1479
|
+
'claude',
|
|
1480
|
+
'cursor'
|
|
1481
|
+
];
|
|
1482
|
+
function clientConfig(client, root, { global, codexHome }) {
|
|
1483
|
+
if (client === 'codex') {
|
|
1484
|
+
const directory = global && codexHome ? path.resolve(codexHome) : path.join(root, '.codex');
|
|
1485
|
+
return {
|
|
1486
|
+
file: path.join(directory, 'config.toml'),
|
|
1487
|
+
root: global && codexHome ? directory : root
|
|
1488
|
+
};
|
|
1489
|
+
}
|
|
1490
|
+
return {
|
|
1491
|
+
root,
|
|
1492
|
+
file: path.join(root, client === 'claude' ? global ? '.claude.json' : '.mcp.json' : '.cursor/mcp.json')
|
|
1493
|
+
};
|
|
1494
|
+
}
|
|
1495
|
+
function expectedEntry(client) {
|
|
1496
|
+
return client === 'claude' ? {
|
|
1497
|
+
type: 'http',
|
|
1498
|
+
url: MCP_URL
|
|
1499
|
+
} : {
|
|
1500
|
+
url: MCP_URL
|
|
1501
|
+
};
|
|
1502
|
+
}
|
|
1503
|
+
function object(value) {
|
|
1504
|
+
return value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
1505
|
+
}
|
|
1506
|
+
function parseConfig(client, source) {
|
|
1507
|
+
if (source === undefined) return {};
|
|
1508
|
+
try {
|
|
1509
|
+
if (client === 'codex') return parse(source);
|
|
1510
|
+
const errors = [];
|
|
1511
|
+
const tree = parseTree(source, errors, {
|
|
1512
|
+
allowTrailingComma: true
|
|
1513
|
+
});
|
|
1514
|
+
if (errors.length || tree?.type !== 'object') throw new Error();
|
|
1515
|
+
return readTree(tree);
|
|
1516
|
+
} catch {
|
|
1517
|
+
throw usageError('invalid_agent_config', 'Agent configuration is malformed or contains duplicate keys. Repair it before continuing.');
|
|
1518
|
+
}
|
|
1519
|
+
}
|
|
1520
|
+
function readTree(node) {
|
|
1521
|
+
if (node.type === 'object') {
|
|
1522
|
+
const result = Object.create(null);
|
|
1523
|
+
for (const property of node.children ?? []){
|
|
1524
|
+
const key = property.children[0].value;
|
|
1525
|
+
if (Object.hasOwn(result, key)) throw new Error();
|
|
1526
|
+
result[key] = readTree(property.children[1]);
|
|
1527
|
+
}
|
|
1528
|
+
return result;
|
|
1529
|
+
}
|
|
1530
|
+
if (node.type === 'array') return node.children?.map(readTree);
|
|
1531
|
+
return node.value;
|
|
1532
|
+
}
|
|
1533
|
+
function serverEntries(client, config) {
|
|
1534
|
+
const servers = config[client === 'codex' ? 'mcp_servers' : 'mcpServers'];
|
|
1535
|
+
if (servers === undefined) return {};
|
|
1536
|
+
if (!object(servers)) throw usageError('invalid_agent_config', 'The MCP server configuration must be an object/table.');
|
|
1537
|
+
return servers;
|
|
1538
|
+
}
|
|
1539
|
+
const CODEX_BLOCK = `# BEGIN EDGESTORE MCP\n[mcp_servers.edgestore]\nurl = "${MCP_URL}"\n# END EDGESTORE MCP\n`;
|
|
1540
|
+
function entrySource(client, source) {
|
|
1541
|
+
if (source === undefined) return '';
|
|
1542
|
+
if (client === 'codex') {
|
|
1543
|
+
const entry = serverEntries(client, parseConfig(client, source)).edgestore;
|
|
1544
|
+
return JSON.stringify({
|
|
1545
|
+
entry,
|
|
1546
|
+
managedBlock: source.includes(CODEX_BLOCK)
|
|
1547
|
+
});
|
|
1548
|
+
}
|
|
1549
|
+
const node = findNodeAtLocation(parseTree(source), [
|
|
1550
|
+
'mcpServers',
|
|
1551
|
+
'edgestore'
|
|
1552
|
+
]);
|
|
1553
|
+
return node ? source.slice(node.offset, node.offset + node.length) : '';
|
|
1554
|
+
}
|
|
1555
|
+
function editConnection(client, source, remove) {
|
|
1556
|
+
let result;
|
|
1557
|
+
if (client === 'codex') {
|
|
1558
|
+
if (remove) {
|
|
1559
|
+
if (!source?.includes(CODEX_BLOCK) || source.indexOf(CODEX_BLOCK) !== source.lastIndexOf(CODEX_BLOCK)) {
|
|
1560
|
+
throw usageError('modified_agent_config', 'The managed Codex block was edited; remove it manually.');
|
|
1561
|
+
}
|
|
1562
|
+
result = source.replace(CODEX_BLOCK, '');
|
|
1563
|
+
} else {
|
|
1564
|
+
result = `${source ?? ''}${source && !source.endsWith('\n') ? '\n' : ''}\n${CODEX_BLOCK}`;
|
|
1565
|
+
}
|
|
1566
|
+
} else {
|
|
1567
|
+
const text = source ?? '{}\n';
|
|
1568
|
+
result = applyEdits(text, modify(text, [
|
|
1569
|
+
'mcpServers',
|
|
1570
|
+
'edgestore'
|
|
1571
|
+
], remove ? undefined : expectedEntry(client), {
|
|
1572
|
+
formattingOptions: {
|
|
1573
|
+
insertSpaces: true,
|
|
1574
|
+
tabSize: 2
|
|
1575
|
+
}
|
|
1576
|
+
}));
|
|
1577
|
+
}
|
|
1578
|
+
const parsed = parseConfig(client, result);
|
|
1579
|
+
const entry = serverEntries(client, parsed).edgestore;
|
|
1580
|
+
if (remove ? entry !== undefined : !object(entry) || entry.url !== MCP_URL) {
|
|
1581
|
+
throw usageError('agent_config_conflict', 'The existing config layout cannot safely accept this MCP entry. Configure it manually.');
|
|
1582
|
+
}
|
|
1583
|
+
return result;
|
|
1584
|
+
}
|
|
1585
|
+
function hasPlugin(config) {
|
|
1586
|
+
return object(config.plugins) && Object.entries(config.plugins).some(([name, entry])=>name.split('@')[0] === 'edgestore' && object(entry) && entry.enabled === true);
|
|
1587
|
+
}
|
|
1588
|
+
|
|
1589
|
+
function digest(value) {
|
|
1590
|
+
return createHash('sha256').update(value).digest('hex');
|
|
1591
|
+
}
|
|
1592
|
+
// Reject symlinks below the explicitly selected root, including dangling ones.
|
|
1593
|
+
async function validatePath(file, root) {
|
|
1594
|
+
const relative = path.relative(root, file);
|
|
1595
|
+
if (!relative || relative.startsWith(`..${path.sep}`) || relative === '..' || path.isAbsolute(relative)) {
|
|
1596
|
+
throw usageError('unsafe_agent_path', 'Agent asset path is outside its selected root.');
|
|
1597
|
+
}
|
|
1598
|
+
let current = root;
|
|
1599
|
+
for (const part of [
|
|
1600
|
+
'',
|
|
1601
|
+
...relative.split(path.sep)
|
|
1602
|
+
]){
|
|
1603
|
+
current = path.join(current, part);
|
|
1604
|
+
try {
|
|
1605
|
+
if ((await lstat(current)).isSymbolicLink()) {
|
|
1606
|
+
throw usageError('unsafe_agent_path', 'Refusing to follow a symlink in an agent asset path.');
|
|
1607
|
+
}
|
|
1608
|
+
} catch (error) {
|
|
1609
|
+
if (error.code !== 'ENOENT') throw error;
|
|
1610
|
+
}
|
|
1611
|
+
}
|
|
1612
|
+
}
|
|
1613
|
+
async function readOptional(file, root) {
|
|
1614
|
+
await validatePath(file, root);
|
|
1615
|
+
try {
|
|
1616
|
+
return await readFile(file, 'utf8');
|
|
1617
|
+
} catch (error) {
|
|
1618
|
+
if (error.code === 'ENOENT') return undefined;
|
|
1619
|
+
throw new CliError('agent_file_unreadable', 'Could not read an agent configuration file.', {
|
|
1620
|
+
details: {
|
|
1621
|
+
file
|
|
1622
|
+
}
|
|
1623
|
+
});
|
|
1624
|
+
}
|
|
1625
|
+
}
|
|
1626
|
+
async function applyChanges(changes) {
|
|
1627
|
+
for (const change of changes)await checkUnchanged(change);
|
|
1628
|
+
const applied = [];
|
|
1629
|
+
try {
|
|
1630
|
+
for (const change of changes){
|
|
1631
|
+
await validatePath(change.file, change.root);
|
|
1632
|
+
if (change.after === undefined) {
|
|
1633
|
+
await checkUnchanged(change);
|
|
1634
|
+
await unlink(change.file);
|
|
1635
|
+
applied.push(change.file);
|
|
1636
|
+
continue;
|
|
1637
|
+
}
|
|
1638
|
+
await mkdir(path.dirname(change.file), {
|
|
1639
|
+
recursive: true
|
|
1640
|
+
});
|
|
1641
|
+
await checkUnchanged(change);
|
|
1642
|
+
const temporary = `${change.file}.${randomUUID()}.tmp`;
|
|
1643
|
+
try {
|
|
1644
|
+
await writeFile(temporary, change.after, {
|
|
1645
|
+
flag: 'wx',
|
|
1646
|
+
mode: 0o600
|
|
1647
|
+
});
|
|
1648
|
+
await rename(temporary, change.file);
|
|
1649
|
+
applied.push(change.file);
|
|
1650
|
+
} finally{
|
|
1651
|
+
await unlink(temporary).catch(()=>undefined);
|
|
1652
|
+
}
|
|
1653
|
+
}
|
|
1654
|
+
} catch {
|
|
1655
|
+
// Underlying errors or file contents can contain credentials. Return only paths.
|
|
1656
|
+
throw new CliError('agent_write_failed', 'Agent configuration was not fully applied. Inspect the listed files before retrying.', {
|
|
1657
|
+
details: {
|
|
1658
|
+
applied
|
|
1659
|
+
}
|
|
1660
|
+
});
|
|
1661
|
+
}
|
|
1662
|
+
}
|
|
1663
|
+
async function checkUnchanged(change) {
|
|
1664
|
+
if (await readOptional(change.file, change.root) !== change.before) {
|
|
1665
|
+
throw usageError('agent_file_changed', 'An agent configuration file changed after inspection; retry the command.');
|
|
1666
|
+
}
|
|
1667
|
+
}
|
|
1668
|
+
|
|
1669
|
+
const stateSchema$1 = z.object({
|
|
1670
|
+
schemaVersion: z.literal(1),
|
|
1671
|
+
mcp: z.record(z.string(), z.object({
|
|
1672
|
+
location: z.string(),
|
|
1673
|
+
hash: z.string()
|
|
1674
|
+
})).default({})
|
|
1675
|
+
}).passthrough();
|
|
1676
|
+
async function planMcp(options, action) {
|
|
1677
|
+
const { client, project, home, global = false } = options;
|
|
1678
|
+
const target = clientConfig(client, global ? home : project, {
|
|
1679
|
+
global,
|
|
1680
|
+
codexHome: options.codexHome
|
|
1681
|
+
});
|
|
1682
|
+
const before = await readOptional(target.file, target.root);
|
|
1683
|
+
const config = parseConfig(client, before);
|
|
1684
|
+
const servers = serverEntries(client, config);
|
|
1685
|
+
const entry = servers.edgestore;
|
|
1686
|
+
const stateRoot = global ? options.stateRoot : project;
|
|
1687
|
+
const stateFile = path.join(stateRoot, global ? 'agent-assets.json' : '.edgestore/agent-assets.json');
|
|
1688
|
+
const stateBefore = await readOptional(stateFile, stateRoot);
|
|
1689
|
+
let state;
|
|
1690
|
+
try {
|
|
1691
|
+
state = stateSchema$1.parse(stateBefore === undefined ? {
|
|
1692
|
+
schemaVersion: 1
|
|
1693
|
+
} : JSON.parse(stateBefore));
|
|
1694
|
+
} catch {
|
|
1695
|
+
throw usageError('invalid_agent_state', 'Agent installation metadata is malformed; preserve it and inspect before continuing.');
|
|
1696
|
+
}
|
|
1697
|
+
const location = global ? target.file : path.relative(project, target.file);
|
|
1698
|
+
const owned = state.mcp[client];
|
|
1699
|
+
const matches = owned?.location === location && owned.hash === digest(entrySource(client, before));
|
|
1700
|
+
const duplicate = Object.entries(servers).some(([name, value])=>name !== 'edgestore' && isEdgeStore(value));
|
|
1701
|
+
const globalTarget = clientConfig(client, home, {
|
|
1702
|
+
global: true,
|
|
1703
|
+
codexHome: options.codexHome
|
|
1704
|
+
});
|
|
1705
|
+
const globalConfig = global ? config : parseConfig(client, await readOptional(globalTarget.file, globalTarget.root));
|
|
1706
|
+
const inherited = global ? undefined : inheritedConnection(options, globalConfig);
|
|
1707
|
+
const plugin = client === 'codex' && (hasPlugin(config) || hasPlugin(globalConfig));
|
|
1708
|
+
let status = owned ? 'missing' : 'not-configured';
|
|
1709
|
+
if (inherited) status = inherited;
|
|
1710
|
+
if (duplicate) status = 'configured-under-other-name';
|
|
1711
|
+
if (entry !== undefined) {
|
|
1712
|
+
status = isEdgeStore(entry) ? 'unmanaged' : 'conflict';
|
|
1713
|
+
if (owned) status = 'modified';
|
|
1714
|
+
if (matches) status = 'configured';
|
|
1715
|
+
}
|
|
1716
|
+
// Claude's private local scope overrides project config. Be conservative on conflicts.
|
|
1717
|
+
if (inherited === 'conflict') status = 'conflict';
|
|
1718
|
+
const changes = [];
|
|
1719
|
+
if (action !== 'status') {
|
|
1720
|
+
if (status === 'modified' || status === 'conflict') {
|
|
1721
|
+
throw usageError('agent_config_conflict', 'The EdgeStore entry is user-owned or modified. It was preserved; inspect it before continuing.');
|
|
1722
|
+
}
|
|
1723
|
+
if (action === 'setup' && (status === 'not-configured' || status === 'missing')) {
|
|
1724
|
+
const after = editConnection(client, before, false);
|
|
1725
|
+
changes.push({
|
|
1726
|
+
...target,
|
|
1727
|
+
before,
|
|
1728
|
+
after
|
|
1729
|
+
});
|
|
1730
|
+
state.mcp[client] = {
|
|
1731
|
+
location,
|
|
1732
|
+
hash: digest(entrySource(client, after))
|
|
1733
|
+
};
|
|
1734
|
+
}
|
|
1735
|
+
if (action === 'remove' && owned?.location === location && (matches || entry === undefined)) {
|
|
1736
|
+
if (matches) changes.push({
|
|
1737
|
+
...target,
|
|
1738
|
+
before,
|
|
1739
|
+
after: editConnection(client, before, true)
|
|
1740
|
+
});
|
|
1741
|
+
delete state.mcp[client];
|
|
1742
|
+
}
|
|
1743
|
+
if (changes.length || action === 'remove' && owned && !state.mcp[client]) {
|
|
1744
|
+
changes.push({
|
|
1745
|
+
file: stateFile,
|
|
1746
|
+
root: stateRoot,
|
|
1747
|
+
before: stateBefore,
|
|
1748
|
+
after: `${JSON.stringify(state, null, 2)}\n`
|
|
1749
|
+
});
|
|
1750
|
+
}
|
|
1751
|
+
}
|
|
1752
|
+
return {
|
|
1753
|
+
// Never expose config contents: other servers may contain credentials.
|
|
1754
|
+
result: {
|
|
1755
|
+
schemaVersion: 1,
|
|
1756
|
+
client,
|
|
1757
|
+
scope: global ? 'global' : 'project',
|
|
1758
|
+
configPath: target.file,
|
|
1759
|
+
status,
|
|
1760
|
+
endpoint: MCP_URL,
|
|
1761
|
+
authentication: 'not-checked',
|
|
1762
|
+
pluginDiscovery: 'visible-config-only',
|
|
1763
|
+
warnings: plugin ? [
|
|
1764
|
+
'An EdgeStore plugin is enabled. Check its MCP connection before adding a separate one.'
|
|
1765
|
+
] : [],
|
|
1766
|
+
plannedFiles: changes.map(({ file })=>file)
|
|
1767
|
+
},
|
|
1768
|
+
changes
|
|
1769
|
+
};
|
|
1770
|
+
}
|
|
1771
|
+
function isEdgeStore(entry) {
|
|
1772
|
+
return object(entry) && entry.url === MCP_URL;
|
|
1773
|
+
}
|
|
1774
|
+
function inheritedConnection(options, config) {
|
|
1775
|
+
// A named global entry with another endpoint is ambiguous, not a connection to adopt.
|
|
1776
|
+
const servers = serverEntries(options.client, config);
|
|
1777
|
+
if (servers.edgestore !== undefined && !isEdgeStore(servers.edgestore)) {
|
|
1778
|
+
return 'conflict';
|
|
1779
|
+
}
|
|
1780
|
+
if (options.client === 'claude' && object(config.projects)) {
|
|
1781
|
+
const project = config.projects[options.project];
|
|
1782
|
+
if (object(project) && object(project.mcpServers)) {
|
|
1783
|
+
if (project.mcpServers.edgestore !== undefined && !isEdgeStore(project.mcpServers.edgestore)) {
|
|
1784
|
+
return 'conflict';
|
|
1785
|
+
}
|
|
1786
|
+
if (Object.values(project.mcpServers).some(isEdgeStore)) return 'inherited';
|
|
1787
|
+
}
|
|
1788
|
+
}
|
|
1789
|
+
return Object.values(servers).some(isEdgeStore) ? 'inherited' : undefined;
|
|
1790
|
+
}
|
|
1791
|
+
|
|
1792
|
+
async function agentOptions(runtime, flags, input) {
|
|
1793
|
+
let client = input.client;
|
|
1794
|
+
if (!client && isInteractive(runtime, flags)) client = await runtime.prompts.select('Coding agent', CLIENTS.map((value)=>({
|
|
1795
|
+
value,
|
|
1796
|
+
label: value
|
|
1797
|
+
})));
|
|
1798
|
+
if (!CLIENTS.includes(client)) throw usageError('agent_client_required', 'Select --client codex, claude, or cursor.');
|
|
1799
|
+
return {
|
|
1800
|
+
client: client,
|
|
1801
|
+
project: await findGitRoot(runtime.cwd) ?? await findPackageRoot(runtime.cwd),
|
|
1802
|
+
home: path.resolve(runtime.env.HOME ?? homedir()),
|
|
1803
|
+
stateRoot: path.dirname(runtime.globalConfig.path),
|
|
1804
|
+
global: input.global,
|
|
1805
|
+
codexHome: runtime.env.CODEX_HOME
|
|
1806
|
+
};
|
|
1807
|
+
}
|
|
1808
|
+
|
|
1809
|
+
const bundleSchema = z.object({
|
|
1810
|
+
schemaVersion: z.literal(1),
|
|
1811
|
+
revision: z.string(),
|
|
1812
|
+
files: z.record(z.string(), z.string())
|
|
1813
|
+
});
|
|
1814
|
+
const stateSchema = z.object({
|
|
1815
|
+
schemaVersion: z.literal(1),
|
|
1816
|
+
clients: z.record(z.string(), z.object({
|
|
1817
|
+
location: z.string(),
|
|
1818
|
+
revision: z.string(),
|
|
1819
|
+
hashes: z.record(z.string(), z.string())
|
|
1820
|
+
}))
|
|
1821
|
+
});
|
|
1822
|
+
const SKILL_NAME = 'edgestore-setup';
|
|
1823
|
+
async function loadSkillBundle(file) {
|
|
1824
|
+
try {
|
|
1825
|
+
const bundle = bundleSchema.parse(JSON.parse(await readFile(file, 'utf8')));
|
|
1826
|
+
if (digest(JSON.stringify(bundle.files)) !== bundle.revision || !bundle.files[`${SKILL_NAME}/SKILL.md`]) throw new Error();
|
|
1827
|
+
for (const name of Object.keys(bundle.files))validateAssetName(name);
|
|
1828
|
+
return bundle;
|
|
1829
|
+
} catch {
|
|
1830
|
+
throw usageError('invalid_skill_bundle', 'The CLI skill bundle is missing or invalid. Reinstall a complete CLI artifact.');
|
|
1831
|
+
}
|
|
1832
|
+
}
|
|
1833
|
+
function skillDirectory(client, root) {
|
|
1834
|
+
return path.join(root, client === 'codex' ? '.agents' : client === 'claude' ? '.claude' : '.cursor', 'skills');
|
|
1835
|
+
}
|
|
1836
|
+
async function planSkills(options, bundle, action) {
|
|
1837
|
+
const root = options.global ? options.home : options.project;
|
|
1838
|
+
const directory = skillDirectory(options.client, root);
|
|
1839
|
+
const stateRoot = options.global ? options.stateRoot : options.project;
|
|
1840
|
+
const stateFile = path.join(stateRoot, options.global ? 'skill-assets.json' : '.edgestore/skill-assets.json');
|
|
1841
|
+
const stateBefore = await readOptional(stateFile, stateRoot);
|
|
1842
|
+
let state;
|
|
1843
|
+
try {
|
|
1844
|
+
state = stateSchema.parse(stateBefore === undefined ? {
|
|
1845
|
+
schemaVersion: 1,
|
|
1846
|
+
clients: {}
|
|
1847
|
+
} : JSON.parse(stateBefore));
|
|
1848
|
+
for (const entry of Object.values(state.clients))for (const name of Object.keys(entry.hashes))validateAssetName(name);
|
|
1849
|
+
} catch {
|
|
1850
|
+
throw usageError('invalid_skill_state', 'Skill ownership metadata is invalid; inspect it before continuing.');
|
|
1851
|
+
}
|
|
1852
|
+
const location = options.global ? directory : path.relative(root, directory);
|
|
1853
|
+
const owned = state.clients[options.client];
|
|
1854
|
+
const names = new Set([
|
|
1855
|
+
...Object.keys(bundle.files),
|
|
1856
|
+
...Object.keys(owned?.hashes ?? {})
|
|
1857
|
+
]);
|
|
1858
|
+
const current = {};
|
|
1859
|
+
for (const name of names)current[name] = await readOptional(path.join(directory, name), root);
|
|
1860
|
+
const existingFiles = await installedFiles(path.join(directory, SKILL_NAME), root);
|
|
1861
|
+
const untracked = existingFiles.filter((name)=>!owned?.hashes[name]);
|
|
1862
|
+
const modified = owned && (owned.location !== location || Object.entries(owned.hashes).some(([name, hash])=>current[name] === undefined || digest(current[name]) !== hash));
|
|
1863
|
+
const otherSources = await visibleSkills(options, directory);
|
|
1864
|
+
let status = existingFiles.length ? 'unmanaged' : 'not-installed';
|
|
1865
|
+
if (!existingFiles.length && otherSources.length) status = 'other-source';
|
|
1866
|
+
if (owned) status = owned.revision === bundle.revision ? 'current' : 'update-available';
|
|
1867
|
+
if (owned && (modified || untracked.length)) status = 'modified';
|
|
1868
|
+
const changes = [];
|
|
1869
|
+
const needsWrite = status === 'not-installed' || status === 'update-available' && action === 'update';
|
|
1870
|
+
if (action !== 'status' && (status === 'modified' || status === 'unmanaged')) throw usageError('skill_conflict', 'The skill contains user-owned or modified files. They were preserved; review them before installation or update.');
|
|
1871
|
+
if (action !== 'status' && needsWrite) {
|
|
1872
|
+
for (const name of names){
|
|
1873
|
+
const after = bundle.files[name];
|
|
1874
|
+
if (current[name] !== after) changes.push({
|
|
1875
|
+
file: path.join(directory, name),
|
|
1876
|
+
root,
|
|
1877
|
+
before: current[name],
|
|
1878
|
+
after
|
|
1879
|
+
});
|
|
1880
|
+
}
|
|
1881
|
+
state.clients[options.client] = {
|
|
1882
|
+
location,
|
|
1883
|
+
revision: bundle.revision,
|
|
1884
|
+
hashes: Object.fromEntries(Object.entries(bundle.files).map(([name, text])=>[
|
|
1885
|
+
name,
|
|
1886
|
+
digest(text)
|
|
1887
|
+
]))
|
|
1888
|
+
};
|
|
1889
|
+
changes.push({
|
|
1890
|
+
file: stateFile,
|
|
1891
|
+
root: stateRoot,
|
|
1892
|
+
before: stateBefore,
|
|
1893
|
+
after: `${JSON.stringify(state, null, 2)}\n`
|
|
1894
|
+
});
|
|
1895
|
+
}
|
|
1896
|
+
return {
|
|
1897
|
+
result: {
|
|
1898
|
+
status,
|
|
1899
|
+
directory: path.join(directory, SKILL_NAME),
|
|
1900
|
+
installedRevision: owned?.revision,
|
|
1901
|
+
bundledRevision: bundle.revision,
|
|
1902
|
+
otherSources,
|
|
1903
|
+
discovery: 'visible-files-only'
|
|
1904
|
+
},
|
|
1905
|
+
changes
|
|
1906
|
+
};
|
|
1907
|
+
}
|
|
1908
|
+
function validateAssetName(name) {
|
|
1909
|
+
if (!name.startsWith(`${SKILL_NAME}/`) || name.split('/').some((part)=>part === '..' || part === '.' || !part) || name.includes('\\') || path.isAbsolute(name)) throw new Error('Invalid skill asset path');
|
|
1910
|
+
}
|
|
1911
|
+
async function installedFiles(directory, root) {
|
|
1912
|
+
const base = path.dirname(directory);
|
|
1913
|
+
const files = [];
|
|
1914
|
+
let visited = 0;
|
|
1915
|
+
async function visit(directory) {
|
|
1916
|
+
await validatePath(directory, root);
|
|
1917
|
+
if (path.relative(base, directory).split(path.sep).length > 8) throw usageError('skill_conflict', 'Skill directory is too deeply nested to inspect safely.');
|
|
1918
|
+
const entries = await readdir(directory, {
|
|
1919
|
+
withFileTypes: true
|
|
1920
|
+
}).catch((error)=>{
|
|
1921
|
+
if (error.code === 'ENOENT') return [];
|
|
1922
|
+
throw usageError('skill_conflict', 'The skill directory could not be inspected safely.');
|
|
1923
|
+
});
|
|
1924
|
+
for (const entry of entries){
|
|
1925
|
+
if (++visited > 200) throw usageError('skill_conflict', 'Skill directory is too large to inspect safely.');
|
|
1926
|
+
if (entry.isSymbolicLink()) throw usageError('skill_conflict', 'A skill entry is symlinked; it was preserved.');
|
|
1927
|
+
const file = path.join(directory, entry.name);
|
|
1928
|
+
if (entry.isDirectory()) await visit(file);
|
|
1929
|
+
else files.push(path.relative(base, file).replaceAll(path.sep, '/'));
|
|
1930
|
+
}
|
|
1931
|
+
}
|
|
1932
|
+
await visit(directory);
|
|
1933
|
+
return files;
|
|
1934
|
+
}
|
|
1935
|
+
async function visibleSkills(options, target) {
|
|
1936
|
+
const candidates = [
|
|
1937
|
+
skillDirectory(options.client, options.home)
|
|
1938
|
+
];
|
|
1939
|
+
if (options.client === 'cursor') candidates.push(path.join(options.project, '.agents/skills'), path.join(options.home, '.agents/skills'));
|
|
1940
|
+
const visible = [];
|
|
1941
|
+
for (const directory of new Set(candidates)){
|
|
1942
|
+
if (directory === target) continue;
|
|
1943
|
+
const file = path.join(directory, SKILL_NAME, 'SKILL.md');
|
|
1944
|
+
// Presence only: do not follow another installer's symlink or read its content.
|
|
1945
|
+
if (await lstat(file).then(()=>true, ()=>false)) visible.push(file);
|
|
1946
|
+
}
|
|
1947
|
+
return visible;
|
|
1948
|
+
}
|
|
1949
|
+
|
|
1950
|
+
async function configurationStatus(runtime, flags, assetFile) {
|
|
1951
|
+
const bundle = await loadSkillBundle(assetFile).catch(()=>undefined);
|
|
1952
|
+
return Promise.all(CLIENTS.map(async (client)=>{
|
|
1953
|
+
const options = await agentOptions(runtime, flags, {
|
|
1954
|
+
client
|
|
1955
|
+
});
|
|
1956
|
+
const skills = bundle ? await planSkills(options, bundle, 'status').then((plan)=>plan.result, unavailable) : {
|
|
1957
|
+
status: 'bundle-unavailable'
|
|
1958
|
+
};
|
|
1959
|
+
const mcp = await planMcp(options, 'status').then((plan)=>plan.result, unavailable);
|
|
1960
|
+
return {
|
|
1961
|
+
client,
|
|
1962
|
+
skills,
|
|
1963
|
+
mcp
|
|
1964
|
+
};
|
|
1965
|
+
}));
|
|
1966
|
+
}
|
|
1967
|
+
function unavailable(error) {
|
|
1968
|
+
return {
|
|
1969
|
+
status: 'not-inspected',
|
|
1970
|
+
code: error instanceof CliError ? error.code : 'configuration_unreadable'
|
|
1971
|
+
};
|
|
1972
|
+
}
|
|
1973
|
+
|
|
1974
|
+
function applicationKind(dependencies) {
|
|
1975
|
+
if (dependencies['@tanstack/react-start']) return {
|
|
1976
|
+
framework: 'tanstack-start',
|
|
1977
|
+
role: 'fullstack'
|
|
1978
|
+
};
|
|
1979
|
+
if (dependencies.next) return {
|
|
1980
|
+
framework: 'next',
|
|
1981
|
+
role: 'fullstack'
|
|
1982
|
+
};
|
|
1983
|
+
if (dependencies['@react-router/dev'] || dependencies['@remix-run/react']) return {
|
|
1984
|
+
framework: 'react-router',
|
|
1985
|
+
role: 'fullstack'
|
|
1986
|
+
};
|
|
1987
|
+
if (dependencies.astro) return {
|
|
1988
|
+
framework: 'astro',
|
|
1989
|
+
role: dependencies.react ? 'fullstack' : 'backend'
|
|
1990
|
+
};
|
|
1991
|
+
for (const framework of [
|
|
1992
|
+
'hono',
|
|
1993
|
+
'express',
|
|
1994
|
+
'fastify'
|
|
1995
|
+
]){
|
|
1996
|
+
if (dependencies[framework]) return {
|
|
1997
|
+
framework,
|
|
1998
|
+
role: dependencies.react ? 'fullstack' : 'backend'
|
|
1999
|
+
};
|
|
2000
|
+
}
|
|
2001
|
+
if (dependencies.vite && dependencies.react) return {
|
|
2002
|
+
framework: 'vite',
|
|
2003
|
+
role: 'frontend'
|
|
2004
|
+
};
|
|
2005
|
+
if (dependencies.react) return {
|
|
2006
|
+
framework: 'react',
|
|
2007
|
+
role: 'frontend'
|
|
2008
|
+
};
|
|
2009
|
+
return {
|
|
2010
|
+
framework: 'unknown',
|
|
2011
|
+
role: 'unknown'
|
|
2012
|
+
};
|
|
2013
|
+
}
|
|
2014
|
+
|
|
1357
2015
|
async function resolveWorkingDirectory(current, requested) {
|
|
1358
2016
|
const resolved = path.resolve(current, requested);
|
|
1359
2017
|
try {
|
|
@@ -1469,38 +2127,561 @@ async function exists(candidate) {
|
|
|
1469
2127
|
}
|
|
1470
2128
|
}
|
|
1471
2129
|
|
|
1472
|
-
async function
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
|
|
2130
|
+
async function detectPackages(cwd, options = {}) {
|
|
2131
|
+
const resolvedCwd = path.resolve(cwd);
|
|
2132
|
+
const workspaceRoot = await findWorkspaceRoot(resolvedCwd);
|
|
2133
|
+
const packagePath = path.join(cwd, 'package.json');
|
|
2134
|
+
let manifest;
|
|
2135
|
+
let contents;
|
|
2136
|
+
try {
|
|
2137
|
+
contents = await readFile(packagePath, 'utf8');
|
|
2138
|
+
} catch (error) {
|
|
2139
|
+
if (!isMissingPathError(error)) {
|
|
2140
|
+
throw new CliError('package_manifest_unreadable', `Could not read package manifest at ${packagePath}.`, {
|
|
2141
|
+
details: {
|
|
2142
|
+
path: packagePath,
|
|
2143
|
+
cause: error instanceof Error ? error.message : String(error)
|
|
2144
|
+
}
|
|
2145
|
+
});
|
|
2146
|
+
}
|
|
2147
|
+
return {
|
|
2148
|
+
framework: 'unknown',
|
|
2149
|
+
manager: await detectPackageManager(cwd),
|
|
2150
|
+
missing: []
|
|
2151
|
+
};
|
|
1492
2152
|
}
|
|
1493
|
-
const output = outputFor(runtime, flags);
|
|
1494
|
-
output.result(loginResult(identity.actor, environmentTokenActive), `Logged in as ${actorLabel(identity.actor)}.`, actorLabel(identity.actor));
|
|
1495
|
-
warnWhenStoredLoginIsInactive(output, environmentTokenActive);
|
|
1496
|
-
}
|
|
1497
|
-
async function logoutCommand(runtime, flags) {
|
|
1498
|
-
const apiOrigin = apiUrlFor(runtime, flags).displayUrl;
|
|
1499
|
-
const output = outputFor(runtime, flags);
|
|
1500
|
-
const stored = await runtime.credentials.get(apiOrigin);
|
|
1501
|
-
let oauthCredential;
|
|
1502
2153
|
try {
|
|
1503
|
-
|
|
2154
|
+
manifest = JSON.parse(contents);
|
|
2155
|
+
} catch (error) {
|
|
2156
|
+
throw new CliError('invalid_package_manifest', `Invalid package manifest at ${packagePath}.`, {
|
|
2157
|
+
details: {
|
|
2158
|
+
path: packagePath,
|
|
2159
|
+
cause: error instanceof Error ? error.message : String(error)
|
|
2160
|
+
},
|
|
2161
|
+
exitCode: 2
|
|
2162
|
+
});
|
|
2163
|
+
}
|
|
2164
|
+
const dependencies = {
|
|
2165
|
+
...manifest.dependencies,
|
|
2166
|
+
...manifest.devDependencies
|
|
2167
|
+
};
|
|
2168
|
+
const { framework, role } = applicationKind(dependencies);
|
|
2169
|
+
const wanted = role === 'fullstack' ? [
|
|
2170
|
+
'@edgestore/server',
|
|
2171
|
+
'@edgestore/react'
|
|
2172
|
+
] : role === 'frontend' ? [
|
|
2173
|
+
'@edgestore/react'
|
|
2174
|
+
] : [
|
|
2175
|
+
'@edgestore/server'
|
|
2176
|
+
];
|
|
2177
|
+
return {
|
|
2178
|
+
framework: framework === 'unknown' ? 'node' : framework,
|
|
2179
|
+
manager: await detectPackageManager(cwd),
|
|
2180
|
+
missing: wanted.filter((name)=>!dependencies[name]),
|
|
2181
|
+
installAtWorkspaceRoot: options.installAtWorkspaceRoot,
|
|
2182
|
+
...workspaceRoot && workspaceRoot !== resolvedCwd ? {
|
|
2183
|
+
workspace: {
|
|
2184
|
+
root: workspaceRoot,
|
|
2185
|
+
...manifest.name ? {
|
|
2186
|
+
packageName: manifest.name
|
|
2187
|
+
} : {}
|
|
2188
|
+
}
|
|
2189
|
+
} : {}
|
|
2190
|
+
};
|
|
2191
|
+
}
|
|
2192
|
+
async function installPackages(runtime, plan, options) {
|
|
2193
|
+
if (!plan.manager || !plan.missing.length) return {
|
|
2194
|
+
ran: false
|
|
2195
|
+
};
|
|
2196
|
+
const args = installArgs(plan.manager, plan.missing, plan.installAtWorkspaceRoot);
|
|
2197
|
+
const command = renderInstallCommand(plan.manager, args, {
|
|
2198
|
+
plan,
|
|
2199
|
+
packageCwd: options.cwd,
|
|
2200
|
+
invocationCwd: options.invocationCwd
|
|
2201
|
+
});
|
|
2202
|
+
const shouldInstall = options.requested ?? (options.interactive ? await runtime.prompts.confirm(`Install EdgeStore packages with ${plan.manager}?`, true) : false);
|
|
2203
|
+
if (!shouldInstall) return {
|
|
2204
|
+
command,
|
|
2205
|
+
cwd: options.cwd,
|
|
2206
|
+
ran: false
|
|
2207
|
+
};
|
|
2208
|
+
const captured = options.structured ? createOutputCapture() : undefined;
|
|
2209
|
+
try {
|
|
2210
|
+
await runtime.runCommand(plan.manager, args, {
|
|
2211
|
+
cwd: options.cwd,
|
|
2212
|
+
...captured ? {
|
|
2213
|
+
stdout: captured.stream,
|
|
2214
|
+
stderr: captured.stream
|
|
2215
|
+
} : {}
|
|
2216
|
+
});
|
|
2217
|
+
} catch (error) {
|
|
2218
|
+
const diagnostics = captured?.read().trim();
|
|
2219
|
+
if (diagnostics && !options.json) {
|
|
2220
|
+
runtime.io.stderr.write(`${diagnostics}\n`);
|
|
2221
|
+
}
|
|
2222
|
+
throw new CliError('package_install_failed', error instanceof Error ? error.message : 'Package installation failed.', {
|
|
2223
|
+
...diagnostics ? {
|
|
2224
|
+
details: {
|
|
2225
|
+
diagnostics
|
|
2226
|
+
}
|
|
2227
|
+
} : {},
|
|
2228
|
+
suggestions: [
|
|
2229
|
+
command
|
|
2230
|
+
]
|
|
2231
|
+
});
|
|
2232
|
+
}
|
|
2233
|
+
const diagnostics = captured?.read();
|
|
2234
|
+
if (diagnostics) {
|
|
2235
|
+
runtime.io.stderr.write(diagnostics.endsWith('\n') ? diagnostics : `${diagnostics}\n`);
|
|
2236
|
+
}
|
|
2237
|
+
return {
|
|
2238
|
+
command,
|
|
2239
|
+
cwd: options.cwd,
|
|
2240
|
+
ran: true
|
|
2241
|
+
};
|
|
2242
|
+
}
|
|
2243
|
+
function renderInstallCommand(manager, args, context) {
|
|
2244
|
+
const { plan, packageCwd, invocationCwd } = context;
|
|
2245
|
+
const workspace = plan.workspace;
|
|
2246
|
+
if (invocationCwd === workspace?.root) {
|
|
2247
|
+
const workspacePath = path.relative(workspace.root, packageCwd).replaceAll(path.sep, '/');
|
|
2248
|
+
if (manager === 'pnpm') {
|
|
2249
|
+
return renderShellCommand([
|
|
2250
|
+
manager,
|
|
2251
|
+
'--filter',
|
|
2252
|
+
`./${workspacePath}`,
|
|
2253
|
+
...args
|
|
2254
|
+
]);
|
|
2255
|
+
}
|
|
2256
|
+
if (manager === 'npm') {
|
|
2257
|
+
return renderShellCommand([
|
|
2258
|
+
manager,
|
|
2259
|
+
...args,
|
|
2260
|
+
'--workspace',
|
|
2261
|
+
workspacePath
|
|
2262
|
+
]);
|
|
2263
|
+
}
|
|
2264
|
+
if (manager === 'yarn' && workspace.packageName) {
|
|
2265
|
+
return renderShellCommand([
|
|
2266
|
+
manager,
|
|
2267
|
+
'workspace',
|
|
2268
|
+
workspace.packageName,
|
|
2269
|
+
...args
|
|
2270
|
+
]);
|
|
2271
|
+
}
|
|
2272
|
+
}
|
|
2273
|
+
const relativeCwd = path.relative(invocationCwd, packageCwd) || '.';
|
|
2274
|
+
if (relativeCwd === '.') return renderShellCommand([
|
|
2275
|
+
manager,
|
|
2276
|
+
...args
|
|
2277
|
+
]);
|
|
2278
|
+
if (manager === 'npm') {
|
|
2279
|
+
return renderShellCommand([
|
|
2280
|
+
manager,
|
|
2281
|
+
'--prefix',
|
|
2282
|
+
relativeCwd,
|
|
2283
|
+
...args
|
|
2284
|
+
]);
|
|
2285
|
+
}
|
|
2286
|
+
if (manager === 'pnpm') {
|
|
2287
|
+
return renderShellCommand([
|
|
2288
|
+
manager,
|
|
2289
|
+
'--dir',
|
|
2290
|
+
relativeCwd,
|
|
2291
|
+
...args
|
|
2292
|
+
]);
|
|
2293
|
+
}
|
|
2294
|
+
return renderShellCommand([
|
|
2295
|
+
manager,
|
|
2296
|
+
'--cwd',
|
|
2297
|
+
relativeCwd,
|
|
2298
|
+
...args
|
|
2299
|
+
]);
|
|
2300
|
+
}
|
|
2301
|
+
function packageNextSteps(framework) {
|
|
2302
|
+
if (framework === 'next' || framework === 'tanstack-start') {
|
|
2303
|
+
return [
|
|
2304
|
+
'Next steps:',
|
|
2305
|
+
` Configure an EdgeStore router using the ${framework} adapter.`,
|
|
2306
|
+
' Add the EdgeStore provider to your client layout.'
|
|
2307
|
+
];
|
|
2308
|
+
}
|
|
2309
|
+
if (framework === 'react' || framework === 'vite') {
|
|
2310
|
+
return [
|
|
2311
|
+
'Next steps:',
|
|
2312
|
+
' Reuse or choose a separate backend for the EdgeStore server endpoint.',
|
|
2313
|
+
' Add the EdgeStore provider to your React app.'
|
|
2314
|
+
];
|
|
2315
|
+
}
|
|
2316
|
+
return [
|
|
2317
|
+
'Next steps:',
|
|
2318
|
+
' Configure an EdgeStore router and server endpoint.'
|
|
2319
|
+
];
|
|
2320
|
+
}
|
|
2321
|
+
function createOutputCapture(limit = 32_768) {
|
|
2322
|
+
let output = '';
|
|
2323
|
+
return {
|
|
2324
|
+
stream: new Writable({
|
|
2325
|
+
write (chunk, _encoding, callback) {
|
|
2326
|
+
output = `${output}${Buffer.from(chunk).toString('utf8')}`.slice(-limit);
|
|
2327
|
+
callback();
|
|
2328
|
+
}
|
|
2329
|
+
}),
|
|
2330
|
+
read: ()=>output
|
|
2331
|
+
};
|
|
2332
|
+
}
|
|
2333
|
+
function isMissingPathError(error) {
|
|
2334
|
+
return error instanceof Error && 'code' in error && (error.code === 'ENOENT' || error.code === 'ENOTDIR');
|
|
2335
|
+
}
|
|
2336
|
+
async function detectPackageManager(cwd) {
|
|
2337
|
+
const start = path.resolve(cwd);
|
|
2338
|
+
const boundary = await findWorkspaceRoot(start) ?? await findGitRoot(start) ?? start;
|
|
2339
|
+
let directory = start;
|
|
2340
|
+
while(true){
|
|
2341
|
+
for (const strategy of [
|
|
2342
|
+
'packageManager-field',
|
|
2343
|
+
'lockfile',
|
|
2344
|
+
'devEngines-field'
|
|
2345
|
+
]){
|
|
2346
|
+
const result = await detect({
|
|
2347
|
+
cwd: directory,
|
|
2348
|
+
stopDir: directory,
|
|
2349
|
+
strategies: [
|
|
2350
|
+
strategy
|
|
2351
|
+
],
|
|
2352
|
+
packageJsonParser: (contents)=>packageJsonForStrategy(contents, strategy)
|
|
2353
|
+
});
|
|
2354
|
+
if (result?.name === 'pnpm' || result?.name === 'npm' || result?.name === 'yarn' || result?.name === 'bun') {
|
|
2355
|
+
return result.name;
|
|
2356
|
+
}
|
|
2357
|
+
}
|
|
2358
|
+
if (directory === boundary) break;
|
|
2359
|
+
directory = path.dirname(directory);
|
|
2360
|
+
}
|
|
2361
|
+
return 'npm';
|
|
2362
|
+
}
|
|
2363
|
+
function packageJsonForStrategy(contents, strategy) {
|
|
2364
|
+
const manifest = JSON.parse(contents);
|
|
2365
|
+
if (strategy !== 'packageManager-field') delete manifest.packageManager;
|
|
2366
|
+
if (strategy !== 'devEngines-field') delete manifest.devEngines;
|
|
2367
|
+
return manifest;
|
|
2368
|
+
}
|
|
2369
|
+
function installArgs(manager, packages, installAtWorkspaceRoot = false) {
|
|
2370
|
+
if (manager === 'npm') return [
|
|
2371
|
+
'install',
|
|
2372
|
+
...packages
|
|
2373
|
+
];
|
|
2374
|
+
if (manager === 'pnpm' && installAtWorkspaceRoot) {
|
|
2375
|
+
return [
|
|
2376
|
+
'add',
|
|
2377
|
+
'-w',
|
|
2378
|
+
...packages
|
|
2379
|
+
];
|
|
2380
|
+
}
|
|
2381
|
+
return [
|
|
2382
|
+
'add',
|
|
2383
|
+
...packages
|
|
2384
|
+
];
|
|
2385
|
+
}
|
|
2386
|
+
|
|
2387
|
+
const manifestSchema = z.object({
|
|
2388
|
+
name: z.string().optional(),
|
|
2389
|
+
dependencies: z.record(z.string(), z.string()).optional(),
|
|
2390
|
+
devDependencies: z.record(z.string(), z.string()).optional()
|
|
2391
|
+
});
|
|
2392
|
+
const installedSchema = z.object({
|
|
2393
|
+
name: z.string(),
|
|
2394
|
+
version: z.string()
|
|
2395
|
+
});
|
|
2396
|
+
const packageNames = [
|
|
2397
|
+
'@edgestore/server',
|
|
2398
|
+
'@edgestore/react',
|
|
2399
|
+
'@edgestore/sdk'
|
|
2400
|
+
];
|
|
2401
|
+
async function inspectApplication(directory) {
|
|
2402
|
+
const manifestPath = path.join(directory, 'package.json');
|
|
2403
|
+
let manifest;
|
|
2404
|
+
try {
|
|
2405
|
+
manifest = manifestSchema.parse(JSON.parse(await readFile(manifestPath, 'utf8')));
|
|
2406
|
+
} catch {
|
|
2407
|
+
// Never include arbitrary manifest contents or parser snippets in agent output.
|
|
2408
|
+
throw usageError('invalid_package_manifest', `Could not read a valid package manifest at ${manifestPath}.`);
|
|
2409
|
+
}
|
|
2410
|
+
const dependencies = {
|
|
2411
|
+
...manifest.dependencies,
|
|
2412
|
+
...manifest.devDependencies
|
|
2413
|
+
};
|
|
2414
|
+
const workspaceRoot = await findWorkspaceRoot(directory);
|
|
2415
|
+
const resolutionRoot = workspaceRoot ?? await findGitRoot(directory) ?? directory;
|
|
2416
|
+
const resolutionFiles = await readdir(resolutionRoot);
|
|
2417
|
+
const plugAndPlay = resolutionFiles.includes('.pnp.cjs') || resolutionFiles.includes('.pnp.js');
|
|
2418
|
+
const candidates = workspaceRoot === directory ? (await getPackages(directory)).packages.filter((pkg)=>pkg.dir !== directory).map((pkg)=>({
|
|
2419
|
+
directory: pkg.dir,
|
|
2420
|
+
name: pkg.packageJson.name
|
|
2421
|
+
})) : [];
|
|
2422
|
+
const packages = await Promise.all(packageNames.map((name)=>inspectPackage(directory, name, {
|
|
2423
|
+
declaredVersion: dependencies[name],
|
|
2424
|
+
plugAndPlay
|
|
2425
|
+
})));
|
|
2426
|
+
const versions = packages.flatMap((pkg)=>pkg.version ? [
|
|
2427
|
+
pkg.version
|
|
2428
|
+
] : []);
|
|
2429
|
+
const families = new Set(versions.map(versionFamily));
|
|
2430
|
+
const compatibility = packages.some((pkg)=>pkg.status === 'unsupported-resolution') ? 'unresolved' : families.size > 1 ? 'mixed' : [
|
|
2431
|
+
...families
|
|
2432
|
+
][0] ?? 'not-installed';
|
|
2433
|
+
const warnings = [];
|
|
2434
|
+
if (compatibility === 'unresolved') warnings.push("Yarn Plug'n'Play package resolution is not supported. Installed versions and bundled references were not inspected; this does not mean dependencies are missing.");
|
|
2435
|
+
if (compatibility === 'legacy') warnings.push('Choose whether to maintain 0.2 or migrate before changing APIs. Do not upgrade implicitly.');
|
|
2436
|
+
if (compatibility === 'mixed' || compatibility === 'unsupported') warnings.push('Resolve package compatibility explicitly before implementing; do not mix API generations.');
|
|
2437
|
+
if (new Set(versions).size > 1) warnings.push('Installed EdgeStore package versions differ. Read each package’s own references and verify compatibility.');
|
|
2438
|
+
const files = (await readdir(directory)).filter((name)=>/^\.env(?:\.[\w.-]+)?$/.test(name));
|
|
2439
|
+
const environmentFiles = [];
|
|
2440
|
+
for (const name of files.sort()){
|
|
2441
|
+
if ((await stat(path.join(directory, name))).isFile()) environmentFiles.push(path.join(directory, name));
|
|
2442
|
+
}
|
|
2443
|
+
return {
|
|
2444
|
+
directory,
|
|
2445
|
+
name: manifest.name,
|
|
2446
|
+
...applicationKind(dependencies),
|
|
2447
|
+
packageManager: await detectPackageManager(directory),
|
|
2448
|
+
workspaceRoot,
|
|
2449
|
+
candidateWorkspaces: candidates,
|
|
2450
|
+
packages,
|
|
2451
|
+
compatibility,
|
|
2452
|
+
warnings,
|
|
2453
|
+
environmentFiles
|
|
2454
|
+
};
|
|
2455
|
+
}
|
|
2456
|
+
function versionFamily(version) {
|
|
2457
|
+
if (/^1\.\d+\.\d+(?:[-+].*)?$/.test(version)) return 'v1';
|
|
2458
|
+
if (/^0\.2\.\d+(?:[-+].*)?$/.test(version)) return 'legacy';
|
|
2459
|
+
return 'unsupported';
|
|
2460
|
+
}
|
|
2461
|
+
async function inspectPackage(directory, name, { declaredVersion, plugAndPlay }) {
|
|
2462
|
+
const base = {
|
|
2463
|
+
name,
|
|
2464
|
+
declaredVersion
|
|
2465
|
+
};
|
|
2466
|
+
if (!declaredVersion) return {
|
|
2467
|
+
...base,
|
|
2468
|
+
version: undefined,
|
|
2469
|
+
status: 'not-declared'
|
|
2470
|
+
};
|
|
2471
|
+
if (plugAndPlay) return {
|
|
2472
|
+
...base,
|
|
2473
|
+
version: undefined,
|
|
2474
|
+
status: 'unsupported-resolution'
|
|
2475
|
+
};
|
|
2476
|
+
let manifestPath;
|
|
2477
|
+
try {
|
|
2478
|
+
manifestPath = await resolveApplicationPackage(directory, name);
|
|
2479
|
+
} catch {
|
|
2480
|
+
return {
|
|
2481
|
+
...base,
|
|
2482
|
+
version: undefined,
|
|
2483
|
+
status: 'not-resolved'
|
|
2484
|
+
};
|
|
2485
|
+
}
|
|
2486
|
+
let installed;
|
|
2487
|
+
try {
|
|
2488
|
+
installed = installedSchema.parse(JSON.parse(await readFile(manifestPath, 'utf8')));
|
|
2489
|
+
if (installed.name !== name) throw new Error('Package name mismatch');
|
|
2490
|
+
} catch {
|
|
2491
|
+
throw usageError('invalid_installed_package', `Invalid installed metadata for ${name}.`);
|
|
2492
|
+
}
|
|
2493
|
+
const { version } = installed;
|
|
2494
|
+
const root = path.dirname(manifestPath);
|
|
2495
|
+
const docsPath = path.join(root, 'agent-docs/README.md');
|
|
2496
|
+
let bundled = false;
|
|
2497
|
+
try {
|
|
2498
|
+
const header = (await readFile(docsPath, 'utf8')).split('\n')[0];
|
|
2499
|
+
bundled = header === `# ${name} ${version}: agent references`;
|
|
2500
|
+
} catch (error) {
|
|
2501
|
+
if (error.code !== 'ENOENT') throw error;
|
|
2502
|
+
}
|
|
2503
|
+
const family = versionFamily(version);
|
|
2504
|
+
const docs = bundled ? {
|
|
2505
|
+
kind: 'bundled',
|
|
2506
|
+
path: docsPath,
|
|
2507
|
+
version
|
|
2508
|
+
} : {
|
|
2509
|
+
kind: 'online-fallback',
|
|
2510
|
+
version,
|
|
2511
|
+
// Update the 0.2 URL to its archive as part of stable v1 docs promotion.
|
|
2512
|
+
url: family === 'legacy' ? 'https://edgestore.dev/docs/quick-start' : family === 'v1' ? `${version.includes('-') ? 'https://next.edgestore.dev' : 'https://edgestore.dev'}/docs/agents` : undefined,
|
|
2513
|
+
warning: 'No matching bundled references found. Online guidance may differ from this installed version; inspect installed types before using it.'
|
|
2514
|
+
};
|
|
2515
|
+
return {
|
|
2516
|
+
...base,
|
|
2517
|
+
status: 'installed',
|
|
2518
|
+
version,
|
|
2519
|
+
root,
|
|
2520
|
+
docs
|
|
2521
|
+
};
|
|
2522
|
+
}
|
|
2523
|
+
async function resolveApplicationPackage(directory, name) {
|
|
2524
|
+
const boundary = await findWorkspaceRoot(directory) ?? await findGitRoot(directory) ?? directory;
|
|
2525
|
+
let current = directory;
|
|
2526
|
+
while(true){
|
|
2527
|
+
try {
|
|
2528
|
+
// Follow package-manager symlinks, but never fall back to NODE_PATH or
|
|
2529
|
+
// a global CLI's modules when the app has not installed its dependency.
|
|
2530
|
+
return await realpath(path.join(current, 'node_modules', name, 'package.json'));
|
|
2531
|
+
} catch (error) {
|
|
2532
|
+
if (error.code !== 'ENOENT') throw error;
|
|
2533
|
+
}
|
|
2534
|
+
if (current === boundary || path.dirname(current) === current) break;
|
|
2535
|
+
current = path.dirname(current);
|
|
2536
|
+
}
|
|
2537
|
+
throw new Error(`Package not installed in the selected application: ${name}`);
|
|
2538
|
+
}
|
|
2539
|
+
|
|
2540
|
+
async function agentContextCommand(runtime, flags, input) {
|
|
2541
|
+
await selectWorkspaceContext(runtime, flags, 'read');
|
|
2542
|
+
const application = await inspectApplication(runtime.workspaceCwd);
|
|
2543
|
+
const linked = await runtime.repoConfig.read();
|
|
2544
|
+
const configurations = await configurationStatus(runtime, flags, input.assetFile);
|
|
2545
|
+
const selectionRequired = !flags.cwd && !linked && application.candidateWorkspaces.length > 0;
|
|
2546
|
+
const result = {
|
|
2547
|
+
schemaVersion: 1,
|
|
2548
|
+
cliVersion: input.cliVersion,
|
|
2549
|
+
selectionRequired,
|
|
2550
|
+
application,
|
|
2551
|
+
...application.compatibility === 'not-installed' ? {
|
|
2552
|
+
referenceFallback: {
|
|
2553
|
+
url: `${/^[^+]*-/.test(input.cliVersion) ? 'https://next.edgestore.dev' : 'https://edgestore.dev'}/docs/agents`,
|
|
2554
|
+
warning: 'Setup guidance follows the CLI release channel; resolve installed package references after installation.'
|
|
2555
|
+
}
|
|
2556
|
+
} : {},
|
|
2557
|
+
project: linked ? {
|
|
2558
|
+
account: linked.config.account,
|
|
2559
|
+
project: linked.config.project,
|
|
2560
|
+
apiUrl: resolveApiUrl(apiOriginForRepoConfig(linked.config), undefined).displayUrl,
|
|
2561
|
+
configPath: linked.path,
|
|
2562
|
+
envFile: linked.config.envFile
|
|
2563
|
+
} : null,
|
|
2564
|
+
agentConfiguration: configurations.map(({ client, skills })=>({
|
|
2565
|
+
client,
|
|
2566
|
+
...skills
|
|
2567
|
+
})),
|
|
2568
|
+
mcpConfiguration: configurations.map(({ client, mcp })=>({
|
|
2569
|
+
client,
|
|
2570
|
+
...mcp
|
|
2571
|
+
}))
|
|
2572
|
+
};
|
|
2573
|
+
if (selectionRequired) runtime.exitCode = 2;
|
|
2574
|
+
outputFor(runtime, flags).result(result, [
|
|
2575
|
+
`Application: ${application.directory}`,
|
|
2576
|
+
`Framework: ${application.framework} (${application.role})`,
|
|
2577
|
+
`Package manager: ${application.packageManager}`,
|
|
2578
|
+
...application.packages.map((pkg)=>`${pkg.name}: ${pkg.version ?? pkg.status}`),
|
|
2579
|
+
...application.warnings,
|
|
2580
|
+
...selectionRequired ? [
|
|
2581
|
+
'Select an application workspace with --cwd before implementing.',
|
|
2582
|
+
...application.candidateWorkspaces.map((pkg)=>pkg.directory)
|
|
2583
|
+
] : []
|
|
2584
|
+
].join('\n'));
|
|
2585
|
+
}
|
|
2586
|
+
|
|
2587
|
+
async function agentAssetsCommand(runtime, flags, input) {
|
|
2588
|
+
const options = await agentOptions(runtime, flags, input);
|
|
2589
|
+
const bundle = await loadSkillBundle(input.assetFile);
|
|
2590
|
+
const skills = await planSkills(options, bundle, input.action);
|
|
2591
|
+
const mcp = input.skillsOnly ? undefined : await planMcp(options, input.action === 'status' ? 'status' : 'setup');
|
|
2592
|
+
const changes = [
|
|
2593
|
+
...skills.changes,
|
|
2594
|
+
...mcp?.changes ?? []
|
|
2595
|
+
];
|
|
2596
|
+
if (changes.length && !input.dryRun) {
|
|
2597
|
+
if (!input.yes) {
|
|
2598
|
+
if (!isInteractive(runtime, flags)) throw usageError('confirmation_required', 'Use --yes to apply changes or --dry-run to preview them.');
|
|
2599
|
+
if (!await runtime.prompts.confirm(`Apply ${input.skillsOnly ? 'skill' : 'skill and MCP'} changes for ${options.client} at ${options.global ? options.home : options.project}?`)) return;
|
|
2600
|
+
}
|
|
2601
|
+
// Both plans are preflighted before any asset or MCP configuration is changed.
|
|
2602
|
+
await applyChanges(changes);
|
|
2603
|
+
}
|
|
2604
|
+
const current = !input.dryRun && changes.length ? await planSkills(options, bundle, 'status') : skills;
|
|
2605
|
+
const connection = mcp && !input.dryRun && changes.length ? await planMcp(options, 'status') : mcp;
|
|
2606
|
+
const result = {
|
|
2607
|
+
schemaVersion: 1,
|
|
2608
|
+
client: options.client,
|
|
2609
|
+
scope: options.global ? 'global' : 'project',
|
|
2610
|
+
action: input.action,
|
|
2611
|
+
dryRun: Boolean(input.dryRun),
|
|
2612
|
+
skills: current.result,
|
|
2613
|
+
mcp: connection?.result ?? {
|
|
2614
|
+
status: 'not-inspected',
|
|
2615
|
+
reason: 'skills-only'
|
|
2616
|
+
},
|
|
2617
|
+
changes: changes.map(({ file, before, after })=>({
|
|
2618
|
+
file,
|
|
2619
|
+
action: after === undefined ? 'remove' : before === undefined ? 'create' : 'update',
|
|
2620
|
+
applied: !input.dryRun
|
|
2621
|
+
}))
|
|
2622
|
+
};
|
|
2623
|
+
outputFor(runtime, flags).result(result, [
|
|
2624
|
+
`${options.client}: skill ${current.result.status}`,
|
|
2625
|
+
current.result.directory,
|
|
2626
|
+
`Bundled revision: ${bundle.revision}`,
|
|
2627
|
+
...changes.map(({ file })=>`${input.dryRun ? 'Would change' : 'Changed'}: ${file}`),
|
|
2628
|
+
...current.result.otherSources.map((file)=>`Skill found at: ${file}`),
|
|
2629
|
+
...connection?.result.warnings ?? [],
|
|
2630
|
+
...current.result.status === 'update-available' ? [
|
|
2631
|
+
'Run edgestore agent update to install the skill from this CLI.'
|
|
2632
|
+
] : [],
|
|
2633
|
+
...!input.dryRun && skills.changes.length ? [
|
|
2634
|
+
'Restart your client or open a new task to load the skill.'
|
|
2635
|
+
] : [],
|
|
2636
|
+
...!input.dryRun && mcp?.changes.length ? [
|
|
2637
|
+
'Open your client to connect EdgeStore.'
|
|
2638
|
+
] : []
|
|
2639
|
+
].join('\n'));
|
|
2640
|
+
}
|
|
2641
|
+
|
|
2642
|
+
async function loginCommand(runtime, flags, options) {
|
|
2643
|
+
if (options.device && options.token) {
|
|
2644
|
+
throw usageError('conflicting_login_modes', '--device and --token cannot be used together.');
|
|
2645
|
+
}
|
|
2646
|
+
const machineOutputFlag = flags.json ? '--json' : flags.plain ? '--plain' : undefined;
|
|
2647
|
+
if (!options.token && machineOutputFlag) {
|
|
2648
|
+
throw usageError('interactive_input_disabled', `OAuth login is interactive and cannot be used with ${machineOutputFlag}.`, [
|
|
2649
|
+
'edgestore login',
|
|
2650
|
+
'edgestore login --device'
|
|
2651
|
+
]);
|
|
2652
|
+
}
|
|
2653
|
+
if (options.device) return await oauthLogin(runtime, flags, 'device');
|
|
2654
|
+
if (!options.token) return await oauthLogin(runtime, flags, 'browser');
|
|
2655
|
+
if (machineOutputFlag && runtime.io.inputIsTty) {
|
|
2656
|
+
throw usageError('interactive_input_disabled', `Interactive token input is disabled with ${machineOutputFlag}.`, [
|
|
2657
|
+
`printf %s "$EDGESTORE_TOKEN" | edgestore login --token ${machineOutputFlag}`
|
|
2658
|
+
]);
|
|
2659
|
+
}
|
|
2660
|
+
const token = await runtime.prompts.readToken(runtime.io.stdin, runtime.io.inputIsTty);
|
|
2661
|
+
const apiUrl = apiUrlFor(runtime, flags);
|
|
2662
|
+
const sdk = runtime.sdkFactory({
|
|
2663
|
+
token,
|
|
2664
|
+
baseUrl: apiUrl.sdkBaseUrl
|
|
2665
|
+
});
|
|
2666
|
+
const identity = await sdk.management.whoami({
|
|
2667
|
+
signal: runtime.signal
|
|
2668
|
+
});
|
|
2669
|
+
const environmentTokenActive = hasEnvironmentToken(runtime);
|
|
2670
|
+
await runtime.credentials.set(apiUrl.displayUrl, token);
|
|
2671
|
+
if (!environmentTokenActive) {
|
|
2672
|
+
await updateActiveAccountAfterLogin(runtime, flags, identity.actor);
|
|
2673
|
+
}
|
|
2674
|
+
const output = outputFor(runtime, flags);
|
|
2675
|
+
output.result(loginResult(identity.actor, environmentTokenActive), `Logged in as ${actorLabel(identity.actor)}.`, actorLabel(identity.actor));
|
|
2676
|
+
warnWhenStoredLoginIsInactive(output, environmentTokenActive);
|
|
2677
|
+
}
|
|
2678
|
+
async function logoutCommand(runtime, flags) {
|
|
2679
|
+
const apiOrigin = apiUrlFor(runtime, flags).displayUrl;
|
|
2680
|
+
const output = outputFor(runtime, flags);
|
|
2681
|
+
const stored = await runtime.credentials.get(apiOrigin);
|
|
2682
|
+
let oauthCredential;
|
|
2683
|
+
try {
|
|
2684
|
+
oauthCredential = parseStoredOAuthCredential(stored);
|
|
1504
2685
|
} catch {
|
|
1505
2686
|
output.warning('The stored OAuth login is invalid. Skipping remote revocation.');
|
|
1506
2687
|
}
|
|
@@ -1633,24 +2814,34 @@ function actorLabel(actor) {
|
|
|
1633
2814
|
}
|
|
1634
2815
|
return actor.user.email;
|
|
1635
2816
|
}
|
|
1636
|
-
async function
|
|
2817
|
+
async function oauthLogin(runtime, flags, mode) {
|
|
1637
2818
|
const apiUrl = apiUrlFor(runtime, flags);
|
|
1638
2819
|
const output = outputFor(runtime, flags);
|
|
1639
|
-
const
|
|
2820
|
+
const sharedInput = {
|
|
1640
2821
|
apiOrigin: apiUrl.displayUrl,
|
|
1641
2822
|
resource: apiUrl.sdkBaseUrl,
|
|
1642
2823
|
client: await runtime.credentials.getCachedOAuthClient(apiUrl.displayUrl),
|
|
1643
2824
|
signal: runtime.signal,
|
|
1644
2825
|
openUrl: (url)=>runtime.openUrl(url),
|
|
1645
|
-
onAuthorizationUrl: (url)=>{
|
|
1646
|
-
if (output.options.mode === 'human') {
|
|
1647
|
-
runtime.io.stderr.write(`Opening a browser to continue login...\nIf it does not open, visit:\n ${url}\n`);
|
|
1648
|
-
}
|
|
1649
|
-
},
|
|
1650
2826
|
onBrowserOpenFailed: (url)=>{
|
|
1651
2827
|
output.warning(`Could not open a browser automatically. Open this URL:\n ${url}`);
|
|
1652
2828
|
},
|
|
1653
2829
|
onClientRegistered: (client)=>runtime.credentials.setCachedOAuthClient(apiUrl.displayUrl, client)
|
|
2830
|
+
};
|
|
2831
|
+
const result = mode === 'device' ? await runtime.oauth.loginWithDeviceCode({
|
|
2832
|
+
...sharedInput,
|
|
2833
|
+
onDeviceAuthorization: (authorization)=>{
|
|
2834
|
+
if (output.options.mode !== 'human') return;
|
|
2835
|
+
const url = authorization.verificationUriComplete ?? authorization.verificationUri;
|
|
2836
|
+
runtime.io.stderr.write(`Confirm this one-time code in your browser:\n ${authorization.userCode}\nOpening a browser to continue login...\nIf it does not open, visit:\n ${url}\n`);
|
|
2837
|
+
}
|
|
2838
|
+
}) : await runtime.oauth.login({
|
|
2839
|
+
...sharedInput,
|
|
2840
|
+
onAuthorizationUrl: (url)=>{
|
|
2841
|
+
if (output.options.mode === 'human') {
|
|
2842
|
+
runtime.io.stderr.write(`Opening a browser to continue login...\nIf it does not open, visit:\n ${url}\n`);
|
|
2843
|
+
}
|
|
2844
|
+
}
|
|
1654
2845
|
});
|
|
1655
2846
|
let identity;
|
|
1656
2847
|
let credentialStored = false;
|
|
@@ -1815,10 +3006,9 @@ async function projectShowCommand(runtime, flags, projectRef) {
|
|
|
1815
3006
|
].join('\n'), project.basePath);
|
|
1816
3007
|
}
|
|
1817
3008
|
async function projectCreateCommand(runtime, flags, options) {
|
|
1818
|
-
if (flags
|
|
1819
|
-
throw usageError('one_time_secret_delivery_required', '
|
|
1820
|
-
'
|
|
1821
|
-
'Use human or --json output to receive the one-time key.'
|
|
3009
|
+
if (!isInteractive(runtime, flags) && !options.withoutKey) {
|
|
3010
|
+
throw usageError('one_time_secret_delivery_required', 'Automated project creation requires --without-key.', [
|
|
3011
|
+
'Create the project with --without-key, then use project key create with --output to deliver a protected key.'
|
|
1822
3012
|
]);
|
|
1823
3013
|
}
|
|
1824
3014
|
const account = await activeAccount(runtime, flags, options.account);
|
|
@@ -1830,14 +3020,16 @@ async function projectCreateCommand(runtime, flags, options) {
|
|
|
1830
3020
|
allowOverage: Boolean(options.allowOverage),
|
|
1831
3021
|
signal: runtime.signal
|
|
1832
3022
|
});
|
|
1833
|
-
const keyLines = result.projectKey ? [
|
|
3023
|
+
const keyLines = isInteractive(runtime, flags) && result.projectKey ? [
|
|
1834
3024
|
'',
|
|
1835
3025
|
`EDGE_STORE_ACCESS_KEY=${result.projectKey.key.accessKey}`,
|
|
1836
3026
|
`EDGE_STORE_SECRET_KEY=${result.projectKey.secretKey}`,
|
|
1837
3027
|
'',
|
|
1838
3028
|
'Save this secret now. You will not be able to view it again.'
|
|
1839
3029
|
] : [];
|
|
1840
|
-
outputFor(runtime, flags).result(
|
|
3030
|
+
outputFor(runtime, flags).result({
|
|
3031
|
+
project: result.project
|
|
3032
|
+
}, [
|
|
1841
3033
|
`Created project "${result.project.name}" (${result.project.basePath}).`,
|
|
1842
3034
|
...keyLines,
|
|
1843
3035
|
'',
|
|
@@ -2470,21 +3662,276 @@ function findClosingQuote(value, quote) {
|
|
|
2470
3662
|
return -1;
|
|
2471
3663
|
}
|
|
2472
3664
|
|
|
2473
|
-
async function
|
|
3665
|
+
async function localApplicationChecks(directory) {
|
|
3666
|
+
const checks = [];
|
|
3667
|
+
let application;
|
|
3668
|
+
try {
|
|
3669
|
+
application = await inspectApplication(directory);
|
|
3670
|
+
} catch {
|
|
3671
|
+
return [
|
|
3672
|
+
{
|
|
3673
|
+
name: 'Application',
|
|
3674
|
+
status: 'skip',
|
|
3675
|
+
detail: 'No readable package manifest; select an application with --cwd.'
|
|
3676
|
+
}
|
|
3677
|
+
];
|
|
3678
|
+
}
|
|
3679
|
+
checks.push({
|
|
3680
|
+
name: 'Application',
|
|
3681
|
+
status: application.framework === 'unknown' ? 'skip' : 'pass',
|
|
3682
|
+
detail: `${application.framework} (${application.role})`
|
|
3683
|
+
});
|
|
3684
|
+
checks.push({
|
|
3685
|
+
name: 'Package compatibility',
|
|
3686
|
+
status: [
|
|
3687
|
+
'mixed',
|
|
3688
|
+
'unsupported'
|
|
3689
|
+
].includes(application.compatibility) ? 'fail' : application.compatibility === 'v1' ? 'pass' : 'warn',
|
|
3690
|
+
detail: application.compatibility === 'legacy' ? 'Installed 0.2 packages: choose maintenance or migration explicitly.' : `${application.compatibility}; API authority is each installed package's references.`
|
|
3691
|
+
});
|
|
3692
|
+
for (const pkg of application.packages){
|
|
3693
|
+
if (pkg.status === 'unsupported-resolution') checks.push({
|
|
3694
|
+
name: pkg.name,
|
|
3695
|
+
status: 'warn',
|
|
3696
|
+
detail: "Yarn Plug'n'Play resolution is unsupported; installed versions and references could not be inspected."
|
|
3697
|
+
});
|
|
3698
|
+
if (pkg.status === 'not-resolved') checks.push({
|
|
3699
|
+
name: pkg.name,
|
|
3700
|
+
status: 'warn',
|
|
3701
|
+
detail: 'Declared but not resolved; install dependencies in the selected workspace.'
|
|
3702
|
+
});
|
|
3703
|
+
}
|
|
3704
|
+
for (const file of application.environmentFiles.filter((file)=>!/\.(example|sample|template)$/.test(file))){
|
|
3705
|
+
try {
|
|
3706
|
+
await validatePath(file, directory);
|
|
3707
|
+
if ((await stat(file)).size > 128_000) throw new Error();
|
|
3708
|
+
const contents = await readFile(file, 'utf8');
|
|
3709
|
+
const exposed = [
|
|
3710
|
+
...contents.matchAll(/^(?:export\s+)?((?:VITE_|NEXT_PUBLIC_|PUBLIC_)\w*EDGE_?STORE\w*(?:SECRET|ACCESS)_KEY)\s*=/gm)
|
|
3711
|
+
].some((match)=>Boolean(dotenvValue(contents, match[1])));
|
|
3712
|
+
const backendKeys = Boolean(dotenvValue(contents, 'EDGE_STORE_SECRET_KEY') || dotenvValue(contents, 'EDGE_STORE_ACCESS_KEY'));
|
|
3713
|
+
if (exposed || backendKeys && application.role === 'frontend') {
|
|
3714
|
+
checks.push({
|
|
3715
|
+
name: 'Environment boundary',
|
|
3716
|
+
status: 'fail',
|
|
3717
|
+
detail: `${path.basename(file)} contains project credentials in a frontend destination. Move them to the backend and rotate any exposed secret.`
|
|
3718
|
+
});
|
|
3719
|
+
}
|
|
3720
|
+
} catch {
|
|
3721
|
+
checks.push({
|
|
3722
|
+
name: 'Environment inspection',
|
|
3723
|
+
status: 'skip',
|
|
3724
|
+
detail: `${path.basename(file)} was not safely readable.`
|
|
3725
|
+
});
|
|
3726
|
+
}
|
|
3727
|
+
}
|
|
3728
|
+
const sources = await readSources(directory);
|
|
3729
|
+
let adapter = false;
|
|
3730
|
+
let provider = false;
|
|
3731
|
+
let parsedFiles = 0;
|
|
3732
|
+
for (const source of sources.files){
|
|
3733
|
+
const facts = inspectSource(source.text, application.role === 'frontend');
|
|
3734
|
+
if (!facts) continue;
|
|
3735
|
+
parsedFiles++;
|
|
3736
|
+
adapter ||= facts.adapter;
|
|
3737
|
+
provider ||= facts.provider;
|
|
3738
|
+
for (const detail of facts.warnings)checks.push({
|
|
3739
|
+
name: `Source: ${source.name}`,
|
|
3740
|
+
status: 'warn',
|
|
3741
|
+
detail
|
|
3742
|
+
});
|
|
3743
|
+
}
|
|
3744
|
+
checks.push({
|
|
3745
|
+
name: 'Source inspection',
|
|
3746
|
+
status: 'skip',
|
|
3747
|
+
detail: `Parsed ${parsedFiles} files within a 200-file/2MB bound; ${sources.skipped || parsedFiles !== sources.files.length ? 'some files were skipped. ' : ''}Static observations do not prove route mounting, provider ancestry, or runtime behavior.`
|
|
3748
|
+
});
|
|
3749
|
+
if (application.role === 'backend' || application.role === 'fullstack') {
|
|
3750
|
+
checks.push({
|
|
3751
|
+
name: 'Server adapter',
|
|
3752
|
+
status: 'skip',
|
|
3753
|
+
detail: adapter ? 'An adapter import was found; verify its route mounts at the client endpoint.' : 'No direct adapter import found; inspect custom/re-exported routes using installed adapter references.'
|
|
3754
|
+
});
|
|
3755
|
+
}
|
|
3756
|
+
if (application.role === 'frontend' || application.role === 'fullstack') {
|
|
3757
|
+
checks.push({
|
|
3758
|
+
name: 'React provider',
|
|
3759
|
+
status: 'skip',
|
|
3760
|
+
detail: provider ? 'EdgeStoreProvider JSX was found; verify it wraps the upload UI.' : 'No directly named EdgeStoreProvider JSX found; inspect custom wrappers and provider aliases.'
|
|
3761
|
+
});
|
|
3762
|
+
}
|
|
3763
|
+
if (application.framework === 'hono') checks.push({
|
|
3764
|
+
name: 'Environment loading',
|
|
3765
|
+
status: 'skip',
|
|
3766
|
+
detail: 'Hono does not itself establish the Node env-file loader. Check the actual start script loads the selected backend env file.'
|
|
3767
|
+
});
|
|
3768
|
+
checks.push({
|
|
3769
|
+
name: 'Remote bucket mapping',
|
|
3770
|
+
status: 'skip',
|
|
3771
|
+
detail: 'Router/provider mapping was not executed or inferred. Compare actual bucket names during the application upload test.'
|
|
3772
|
+
});
|
|
3773
|
+
return checks;
|
|
3774
|
+
}
|
|
3775
|
+
function inspectSource(source, frontend) {
|
|
3776
|
+
let ast;
|
|
3777
|
+
try {
|
|
3778
|
+
ast = parse$1(source, {
|
|
3779
|
+
sourceType: 'unambiguous',
|
|
3780
|
+
plugins: [
|
|
3781
|
+
'typescript',
|
|
3782
|
+
'jsx'
|
|
3783
|
+
],
|
|
3784
|
+
attachComment: false
|
|
3785
|
+
});
|
|
3786
|
+
} catch {
|
|
3787
|
+
return undefined;
|
|
3788
|
+
}
|
|
3789
|
+
const warnings = new Set();
|
|
3790
|
+
let adapter = false;
|
|
3791
|
+
let provider = false;
|
|
3792
|
+
const corsNames = new Set();
|
|
3793
|
+
const handlers = new Set();
|
|
3794
|
+
const clientFile = frontend || ast.program.directives.some((directive)=>directive.value.value === 'use client');
|
|
3795
|
+
for (const statement of ast.program.body){
|
|
3796
|
+
if (statement.type !== 'ImportDeclaration') continue;
|
|
3797
|
+
const from = statement.source.value;
|
|
3798
|
+
const typeOnly = statement.importKind === 'type' || statement.specifiers.length > 0 && statement.specifiers.every((specifier)=>specifier.type === 'ImportSpecifier' && specifier.importKind === 'type');
|
|
3799
|
+
if (from.startsWith('@edgestore/server/adapters/') && !typeOnly) {
|
|
3800
|
+
adapter = true;
|
|
3801
|
+
for (const specifier of statement.specifiers)handlers.add(specifier.local.name);
|
|
3802
|
+
}
|
|
3803
|
+
if (from === 'hono/cors') {
|
|
3804
|
+
for (const specifier of statement.specifiers)corsNames.add(specifier.local.name);
|
|
3805
|
+
}
|
|
3806
|
+
if (clientFile && !typeOnly && (from.startsWith('@edgestore/server') || statement.specifiers.some((specifier)=>specifier.type === 'ImportSpecifier' && isIdentifier(specifier.imported, {
|
|
3807
|
+
name: 'EdgeStoreRouter'
|
|
3808
|
+
}) && specifier.importKind !== 'type'))) {
|
|
3809
|
+
warnings.add('Possible server runtime import in a client module. Use an import type for the backend router contract; do not bundle server modules.');
|
|
3810
|
+
}
|
|
3811
|
+
}
|
|
3812
|
+
traverseFast(ast, (node)=>{
|
|
3813
|
+
if (node.type === 'JSXOpeningElement' && node.name.type === 'JSXIdentifier' && node.name.name === 'EdgeStoreProvider') provider = true;
|
|
3814
|
+
if (node.type !== 'CallExpression' || !isIdentifier(node.callee) || node.arguments[0]?.type !== 'ObjectExpression') return;
|
|
3815
|
+
const argument = node.arguments[0];
|
|
3816
|
+
if (handlers.has(node.callee.name) && property(argument, 'router') && !property(argument, 'edgestore')) {
|
|
3817
|
+
warnings.add('Direct handler router option resembles 0.2 wiring. Check the installed version before maintaining or migrating it.');
|
|
3818
|
+
}
|
|
3819
|
+
if (corsNames.has(node.callee.name)) {
|
|
3820
|
+
const origin = property(argument, 'origin');
|
|
3821
|
+
const credentials = property(argument, 'credentials');
|
|
3822
|
+
const reflects = origin?.type === 'ArrowFunctionExpression' && origin.params.length === 1 && isIdentifier(origin.params[0]) && isIdentifier(origin.body, {
|
|
3823
|
+
name: origin.params[0].name
|
|
3824
|
+
});
|
|
3825
|
+
if (credentials?.type === 'BooleanLiteral' && credentials.value && (isStringLiteral(origin, {
|
|
3826
|
+
value: '*'
|
|
3827
|
+
}) || reflects)) warnings.add('Credentialed CORS uses a wildcard or reflected origin. Configure an explicit trusted frontend origin for the EdgeStore route.');
|
|
3828
|
+
}
|
|
3829
|
+
});
|
|
3830
|
+
return {
|
|
3831
|
+
adapter,
|
|
3832
|
+
provider,
|
|
3833
|
+
warnings: [
|
|
3834
|
+
...warnings
|
|
3835
|
+
]
|
|
3836
|
+
};
|
|
3837
|
+
}
|
|
3838
|
+
function property(object, name) {
|
|
3839
|
+
const member = object.properties.find((entry)=>isObjectProperty(entry) && !entry.computed && (isIdentifier(entry.key, {
|
|
3840
|
+
name
|
|
3841
|
+
}) || isStringLiteral(entry.key, {
|
|
3842
|
+
value: name
|
|
3843
|
+
})));
|
|
3844
|
+
return isObjectProperty(member) ? member.value : undefined;
|
|
3845
|
+
}
|
|
3846
|
+
async function readSources(directory) {
|
|
3847
|
+
const files = [];
|
|
3848
|
+
let bytes = 0;
|
|
3849
|
+
let skipped = false;
|
|
3850
|
+
let inspectedEntries = 0;
|
|
3851
|
+
const queue = [
|
|
3852
|
+
directory
|
|
3853
|
+
];
|
|
3854
|
+
const ignored = new Set([
|
|
3855
|
+
'node_modules',
|
|
3856
|
+
'dist',
|
|
3857
|
+
'build',
|
|
3858
|
+
'coverage',
|
|
3859
|
+
'public',
|
|
3860
|
+
'test',
|
|
3861
|
+
'tests',
|
|
3862
|
+
'__tests__'
|
|
3863
|
+
]);
|
|
3864
|
+
while(queue.length && files.length < 200 && bytes < 2_000_000 && inspectedEntries < 2_000){
|
|
3865
|
+
const current = queue.shift();
|
|
3866
|
+
const entries = await readdir(current, {
|
|
3867
|
+
withFileTypes: true
|
|
3868
|
+
}).catch(()=>[]);
|
|
3869
|
+
for (const entry of entries.sort((a, b)=>a.name.localeCompare(b.name))){
|
|
3870
|
+
if (++inspectedEntries > 2_000) {
|
|
3871
|
+
skipped = true;
|
|
3872
|
+
break;
|
|
3873
|
+
}
|
|
3874
|
+
if (entry.isSymbolicLink() || entry.name.startsWith('.') || ignored.has(entry.name)) continue;
|
|
3875
|
+
const file = path.join(current, entry.name);
|
|
3876
|
+
if (entry.isDirectory()) {
|
|
3877
|
+
// Do not inspect another workspace's source under a monorepo root.
|
|
3878
|
+
if (await stat(path.join(file, 'package.json')).then(()=>true, ()=>false)) continue;
|
|
3879
|
+
if (path.relative(directory, file).split(path.sep).length < 10) queue.push(file);
|
|
3880
|
+
continue;
|
|
3881
|
+
}
|
|
3882
|
+
if (!/\.[cm]?[jt]sx?$/.test(entry.name) || /\.(test|spec)\./.test(entry.name) || entry.name.endsWith('.d.ts')) continue;
|
|
3883
|
+
if (files.length >= 200) {
|
|
3884
|
+
skipped = true;
|
|
3885
|
+
break;
|
|
3886
|
+
}
|
|
3887
|
+
try {
|
|
3888
|
+
const size = (await stat(file)).size;
|
|
3889
|
+
if (size > 128_000 || bytes + size > 2_000_000) {
|
|
3890
|
+
skipped = true;
|
|
3891
|
+
continue;
|
|
3892
|
+
}
|
|
3893
|
+
await validatePath(file, directory);
|
|
3894
|
+
files.push({
|
|
3895
|
+
name: path.relative(directory, file),
|
|
3896
|
+
text: await readFile(file, 'utf8')
|
|
3897
|
+
});
|
|
3898
|
+
bytes += size;
|
|
3899
|
+
} catch {
|
|
3900
|
+
skipped = true;
|
|
3901
|
+
}
|
|
3902
|
+
}
|
|
3903
|
+
}
|
|
3904
|
+
return {
|
|
3905
|
+
files,
|
|
3906
|
+
skipped: skipped || queue.length > 0
|
|
3907
|
+
};
|
|
3908
|
+
}
|
|
3909
|
+
|
|
3910
|
+
async function doctorCommand(runtime, flags, options) {
|
|
2474
3911
|
await selectWorkspaceContext(runtime, flags, 'read');
|
|
2475
3912
|
const checks = [
|
|
2476
3913
|
{
|
|
2477
3914
|
name: 'CLI',
|
|
2478
3915
|
status: 'pass',
|
|
2479
|
-
detail: version
|
|
3916
|
+
detail: options.version
|
|
2480
3917
|
}
|
|
2481
3918
|
];
|
|
3919
|
+
checks.push(...await localApplicationChecks(runtime.workspaceCwd));
|
|
2482
3920
|
const apiUrl = apiUrlFor(runtime, flags);
|
|
2483
3921
|
const globalConfig = await checkGlobalConfig(runtime, checks);
|
|
2484
3922
|
const activeAccount = activeAccountFor(globalConfig, apiUrl.displayUrl);
|
|
2485
3923
|
const localConfig = await checkLocalConfig(runtime, checks);
|
|
2486
|
-
const linkedApiOrigin = localConfig ? apiOriginForRepoConfig(localConfig.config) : undefined;
|
|
3924
|
+
const linkedApiOrigin = localConfig ? resolveApiUrl(apiOriginForRepoConfig(localConfig.config), undefined).displayUrl : undefined;
|
|
2487
3925
|
const envKeys = await checkEnvFile(runtime, localConfig, checks);
|
|
3926
|
+
if (options.offline) {
|
|
3927
|
+
checks.push({
|
|
3928
|
+
name: 'Remote checks',
|
|
3929
|
+
status: 'skip',
|
|
3930
|
+
detail: 'Offline: no credential-store access, OAuth, or API requests.'
|
|
3931
|
+
});
|
|
3932
|
+
report(runtime, flags, checks);
|
|
3933
|
+
return;
|
|
3934
|
+
}
|
|
2488
3935
|
const keychainAvailable = await runtime.credentials.available();
|
|
2489
3936
|
checks.push({
|
|
2490
3937
|
name: 'Credential store',
|
|
@@ -2494,25 +3941,32 @@ async function doctorCommand(runtime, flags, version) {
|
|
|
2494
3941
|
let credential;
|
|
2495
3942
|
try {
|
|
2496
3943
|
credential = await resolveCredential(runtime.env.EDGESTORE_TOKEN, runtime.credentials, {
|
|
2497
|
-
apiOrigin: apiUrl.displayUrl
|
|
2498
|
-
oauth: runtime.oauth,
|
|
2499
|
-
signal: runtime.signal
|
|
3944
|
+
apiOrigin: apiUrl.displayUrl
|
|
2500
3945
|
});
|
|
2501
3946
|
checks.push({
|
|
2502
3947
|
name: 'Credential',
|
|
2503
3948
|
status: credential ? 'pass' : 'warn',
|
|
2504
3949
|
detail: credential ? credential.source : 'Not configured'
|
|
2505
3950
|
});
|
|
2506
|
-
} catch
|
|
3951
|
+
} catch {
|
|
2507
3952
|
rethrowIfAborted(runtime.signal);
|
|
2508
3953
|
checks.push({
|
|
2509
3954
|
name: 'Credential',
|
|
2510
|
-
status: '
|
|
2511
|
-
detail:
|
|
3955
|
+
status: 'warn',
|
|
3956
|
+
detail: 'No usable credential. Login or refresh explicitly; doctor does not change credentials.'
|
|
3957
|
+
});
|
|
3958
|
+
}
|
|
3959
|
+
if (!credential) {
|
|
3960
|
+
checks.push({
|
|
3961
|
+
name: 'Remote checks',
|
|
3962
|
+
status: 'skip',
|
|
3963
|
+
detail: 'No usable existing credential; no network requests were made.'
|
|
2512
3964
|
});
|
|
3965
|
+
report(runtime, flags, checks);
|
|
3966
|
+
return;
|
|
2513
3967
|
}
|
|
2514
3968
|
const sdk = runtime.sdkFactory({
|
|
2515
|
-
token: credential
|
|
3969
|
+
token: credential.token,
|
|
2516
3970
|
baseUrl: apiUrl.sdkBaseUrl
|
|
2517
3971
|
});
|
|
2518
3972
|
try {
|
|
@@ -2524,84 +3978,85 @@ async function doctorCommand(runtime, flags, version) {
|
|
|
2524
3978
|
status: 'pass',
|
|
2525
3979
|
detail: apiUrl.displayUrl
|
|
2526
3980
|
});
|
|
2527
|
-
} catch
|
|
3981
|
+
} catch {
|
|
2528
3982
|
rethrowIfAborted(runtime.signal);
|
|
2529
3983
|
checks.push({
|
|
2530
3984
|
name: 'API',
|
|
2531
3985
|
status: 'fail',
|
|
2532
|
-
detail:
|
|
3986
|
+
detail: 'Health check failed; verify the API endpoint and network.'
|
|
3987
|
+
});
|
|
3988
|
+
}
|
|
3989
|
+
let authenticated = false;
|
|
3990
|
+
try {
|
|
3991
|
+
const identity = await sdk.management.whoami({
|
|
3992
|
+
signal: runtime.signal
|
|
3993
|
+
});
|
|
3994
|
+
authenticated = true;
|
|
3995
|
+
const scopeDetail = 'scopes' in identity.actor ? ` (${identity.actor.scopes.join(', ')})` : '';
|
|
3996
|
+
checks.push({
|
|
3997
|
+
name: 'Authentication',
|
|
3998
|
+
status: 'pass',
|
|
3999
|
+
detail: `${identity.actor.kind}${scopeDetail}`
|
|
4000
|
+
});
|
|
4001
|
+
} catch {
|
|
4002
|
+
rethrowIfAborted(runtime.signal);
|
|
4003
|
+
checks.push({
|
|
4004
|
+
name: 'Authentication',
|
|
4005
|
+
status: 'fail',
|
|
4006
|
+
detail: 'Credential validation failed; inspect the login and granted scopes.'
|
|
2533
4007
|
});
|
|
2534
4008
|
}
|
|
2535
|
-
if (
|
|
2536
|
-
let authenticated = false;
|
|
4009
|
+
if (authenticated && activeAccount) {
|
|
2537
4010
|
try {
|
|
2538
|
-
const
|
|
4011
|
+
const account = await sdk.management.accounts.get({
|
|
4012
|
+
account: activeAccount,
|
|
2539
4013
|
signal: runtime.signal
|
|
2540
4014
|
});
|
|
2541
|
-
authenticated = true;
|
|
2542
|
-
const scopeDetail = 'scopes' in identity.actor ? ` (${identity.actor.scopes.join(', ')})` : '';
|
|
2543
4015
|
checks.push({
|
|
2544
|
-
name: '
|
|
4016
|
+
name: 'Active account',
|
|
2545
4017
|
status: 'pass',
|
|
2546
|
-
detail: `${
|
|
4018
|
+
detail: `${account.account.displayName} (${account.account.id})`
|
|
2547
4019
|
});
|
|
2548
|
-
} catch
|
|
4020
|
+
} catch {
|
|
2549
4021
|
rethrowIfAborted(runtime.signal);
|
|
2550
4022
|
checks.push({
|
|
2551
|
-
name: '
|
|
4023
|
+
name: 'Active account',
|
|
2552
4024
|
status: 'fail',
|
|
2553
|
-
detail:
|
|
4025
|
+
detail: 'Account is inaccessible with the current credential.'
|
|
2554
4026
|
});
|
|
2555
4027
|
}
|
|
2556
|
-
|
|
2557
|
-
|
|
2558
|
-
|
|
2559
|
-
|
|
2560
|
-
|
|
2561
|
-
|
|
2562
|
-
|
|
2563
|
-
|
|
2564
|
-
|
|
2565
|
-
|
|
2566
|
-
|
|
2567
|
-
}
|
|
2568
|
-
|
|
2569
|
-
|
|
2570
|
-
|
|
2571
|
-
|
|
2572
|
-
|
|
2573
|
-
|
|
2574
|
-
|
|
2575
|
-
} else if (authenticated) {
|
|
2576
|
-
checks.push({
|
|
2577
|
-
name: 'Active account',
|
|
2578
|
-
status: 'warn',
|
|
2579
|
-
detail: 'Not selected'
|
|
4028
|
+
} else if (authenticated) {
|
|
4029
|
+
checks.push({
|
|
4030
|
+
name: 'Active account',
|
|
4031
|
+
status: 'warn',
|
|
4032
|
+
detail: 'Not selected'
|
|
4033
|
+
});
|
|
4034
|
+
}
|
|
4035
|
+
if (authenticated && localConfig && linkedApiOrigin !== apiUrl.displayUrl) {
|
|
4036
|
+
checks.push({
|
|
4037
|
+
name: 'Linked project',
|
|
4038
|
+
status: 'fail',
|
|
4039
|
+
detail: `Linked to ${linkedApiOrigin}, current API is ${apiUrl.displayUrl}`
|
|
4040
|
+
});
|
|
4041
|
+
} else if (authenticated && localConfig) {
|
|
4042
|
+
try {
|
|
4043
|
+
await checkLinkedProject(runtime, sdk, {
|
|
4044
|
+
local: localConfig,
|
|
4045
|
+
envKeys,
|
|
4046
|
+
checks
|
|
2580
4047
|
});
|
|
2581
|
-
}
|
|
2582
|
-
|
|
4048
|
+
} catch {
|
|
4049
|
+
rethrowIfAborted(runtime.signal);
|
|
2583
4050
|
checks.push({
|
|
2584
4051
|
name: 'Linked project',
|
|
2585
4052
|
status: 'fail',
|
|
2586
|
-
detail:
|
|
4053
|
+
detail: 'Project is inaccessible with the current credential.'
|
|
2587
4054
|
});
|
|
2588
|
-
} else if (authenticated && localConfig) {
|
|
2589
|
-
try {
|
|
2590
|
-
await checkLinkedProject(runtime, sdk, {
|
|
2591
|
-
local: localConfig,
|
|
2592
|
-
envKeys,
|
|
2593
|
-
checks
|
|
2594
|
-
});
|
|
2595
|
-
} catch (error) {
|
|
2596
|
-
rethrowIfAborted(runtime.signal);
|
|
2597
|
-
checks.push({
|
|
2598
|
-
name: 'Linked project',
|
|
2599
|
-
status: 'fail',
|
|
2600
|
-
detail: error instanceof Error ? error.message : 'Project is inaccessible'
|
|
2601
|
-
});
|
|
2602
|
-
}
|
|
2603
4055
|
}
|
|
2604
4056
|
}
|
|
4057
|
+
report(runtime, flags, checks);
|
|
4058
|
+
}
|
|
4059
|
+
function report(runtime, flags, checks) {
|
|
2605
4060
|
if (checks.some((check)=>check.status === 'fail')) {
|
|
2606
4061
|
runtime.exitCode = 1;
|
|
2607
4062
|
}
|
|
@@ -2626,12 +4081,12 @@ async function checkGlobalConfig(runtime, checks) {
|
|
|
2626
4081
|
detail: runtime.globalConfig.path
|
|
2627
4082
|
});
|
|
2628
4083
|
return config;
|
|
2629
|
-
} catch
|
|
4084
|
+
} catch {
|
|
2630
4085
|
rethrowIfAborted(runtime.signal);
|
|
2631
4086
|
checks.push({
|
|
2632
4087
|
name: 'Global config',
|
|
2633
4088
|
status: 'fail',
|
|
2634
|
-
detail:
|
|
4089
|
+
detail: 'Could not read global config; inspect its format locally.'
|
|
2635
4090
|
});
|
|
2636
4091
|
return {
|
|
2637
4092
|
version: 2,
|
|
@@ -2648,22 +4103,32 @@ async function checkLocalConfig(runtime, checks) {
|
|
|
2648
4103
|
detail: located?.path ?? 'No linked project'
|
|
2649
4104
|
});
|
|
2650
4105
|
return located;
|
|
2651
|
-
} catch
|
|
4106
|
+
} catch {
|
|
2652
4107
|
rethrowIfAborted(runtime.signal);
|
|
2653
4108
|
checks.push({
|
|
2654
4109
|
name: 'Local config',
|
|
2655
4110
|
status: 'fail',
|
|
2656
|
-
detail:
|
|
4111
|
+
detail: 'Could not read local config; inspect its format locally.'
|
|
2657
4112
|
});
|
|
2658
4113
|
return undefined;
|
|
2659
4114
|
}
|
|
2660
4115
|
}
|
|
2661
4116
|
async function checkEnvFile(runtime, localConfig, checks) {
|
|
2662
|
-
const
|
|
4117
|
+
const existing = (await readdir(runtime.workspaceCwd).catch(()=>[])).filter((file)=>/^\.env(?:\.(?:local|development)(?:\.local)?)?$/.test(file));
|
|
4118
|
+
const label = localConfig?.config.envFile ?? (existing.length === 1 ? existing[0] : '.env.local');
|
|
4119
|
+
if (!localConfig?.config.envFile && existing.length > 1) {
|
|
4120
|
+
checks.push({
|
|
4121
|
+
name: 'Environment selection',
|
|
4122
|
+
status: 'skip',
|
|
4123
|
+
detail: 'Multiple env files exist. Inspect the backend loader; only .env.local key presence is checked here.'
|
|
4124
|
+
});
|
|
4125
|
+
}
|
|
2663
4126
|
const configRoot = localConfig?.config.envFile ? path.dirname(path.dirname(localConfig.path)) : runtime.workspaceCwd;
|
|
2664
4127
|
const envPath = path.resolve(configRoot, label);
|
|
2665
4128
|
let contents = '';
|
|
2666
4129
|
try {
|
|
4130
|
+
await validatePath(envPath, configRoot);
|
|
4131
|
+
if ((await stat(envPath)).size > 128_000) throw new Error();
|
|
2667
4132
|
contents = await readFile(envPath, 'utf8');
|
|
2668
4133
|
} catch (error) {
|
|
2669
4134
|
rethrowIfAborted(runtime.signal);
|
|
@@ -2675,10 +4140,11 @@ async function checkEnvFile(runtime, localConfig, checks) {
|
|
|
2675
4140
|
detail: 'Configured env file not found'
|
|
2676
4141
|
});
|
|
2677
4142
|
} else if (code !== 'ENOENT') {
|
|
4143
|
+
const unsafePath = error instanceof CliError && error.code === 'unsafe_agent_path';
|
|
2678
4144
|
checks.push({
|
|
2679
4145
|
name: label,
|
|
2680
|
-
status: 'fail',
|
|
2681
|
-
detail: 'Could not read file'
|
|
4146
|
+
status: unsafePath ? 'skip' : 'fail',
|
|
4147
|
+
detail: unsafePath ? 'Symlinked or out-of-scope env file was not inspected.' : 'Could not read file'
|
|
2682
4148
|
});
|
|
2683
4149
|
}
|
|
2684
4150
|
return {
|
|
@@ -2892,313 +4358,58 @@ async function fileDeleteCommand(runtime, flags, input) {
|
|
|
2892
4358
|
notAttemptedReferences,
|
|
2893
4359
|
cause: {
|
|
2894
4360
|
code: cause.code,
|
|
2895
|
-
message: cause.message,
|
|
2896
|
-
...cause.options.details === undefined ? {} : {
|
|
2897
|
-
details: cause.options.details
|
|
2898
|
-
}
|
|
2899
|
-
}
|
|
2900
|
-
},
|
|
2901
|
-
requestId: cause.options.requestId,
|
|
2902
|
-
exitCode: cause.exitCode
|
|
2903
|
-
});
|
|
2904
|
-
}
|
|
2905
|
-
}
|
|
2906
|
-
const successCount = results.filter((result)=>result.success).length;
|
|
2907
|
-
const failureCount = results.length - successCount;
|
|
2908
|
-
const human = [
|
|
2909
|
-
`Deleted ${successCount} file(s); ${failureCount} failed.`,
|
|
2910
|
-
...results.filter((result)=>!result.success).map((result)=>` ${fileReferenceLabel(result.fileRef)}: ${result.error.message}`)
|
|
2911
|
-
].join('\n');
|
|
2912
|
-
outputFor(runtime, flags).result({
|
|
2913
|
-
results,
|
|
2914
|
-
successCount,
|
|
2915
|
-
failureCount
|
|
2916
|
-
}, human);
|
|
2917
|
-
if (failureCount) runtime.exitCode = 1;
|
|
2918
|
-
}
|
|
2919
|
-
function renderIncompleteDeletion(input) {
|
|
2920
|
-
return [
|
|
2921
|
-
`File deletion stopped: ${input.cause.message}`,
|
|
2922
|
-
`Completed: ${input.successCount} succeeded, ${input.failureCount} failed.`,
|
|
2923
|
-
...input.results.map((result)=>` ${JSON.stringify(result.fileRef)}: ${result.success ? 'deleted' : `failed: ${result.error?.message ?? 'unknown failure'}`}`),
|
|
2924
|
-
`Uncertain (${input.uncertainReferences.length}):`,
|
|
2925
|
-
...input.uncertainReferences.map((reference)=>` ${reference}`),
|
|
2926
|
-
`Not attempted (${input.notAttemptedReferences.length}):`,
|
|
2927
|
-
...input.notAttemptedReferences.map((reference)=>` ${reference}`)
|
|
2928
|
-
].join('\n');
|
|
2929
|
-
}
|
|
2930
|
-
function fileReference(value, bucket) {
|
|
2931
|
-
if (bucket) return {
|
|
2932
|
-
bucketName: bucket,
|
|
2933
|
-
path: value
|
|
2934
|
-
};
|
|
2935
|
-
if (/^https?:\/\//i.test(value)) return {
|
|
2936
|
-
url: value
|
|
2937
|
-
};
|
|
2938
|
-
return {
|
|
2939
|
-
id: value
|
|
2940
|
-
};
|
|
2941
|
-
}
|
|
2942
|
-
function fileReferenceLabel(reference) {
|
|
2943
|
-
if ('id' in reference) return reference.id;
|
|
2944
|
-
if ('key' in reference) return reference.key;
|
|
2945
|
-
if ('url' in reference) return reference.url;
|
|
2946
|
-
return `${reference.bucketName}/${reference.path}`;
|
|
2947
|
-
}
|
|
2948
|
-
|
|
2949
|
-
async function detectPackages(cwd, options = {}) {
|
|
2950
|
-
const resolvedCwd = path.resolve(cwd);
|
|
2951
|
-
const workspaceRoot = await findWorkspaceRoot(resolvedCwd);
|
|
2952
|
-
const packagePath = path.join(cwd, 'package.json');
|
|
2953
|
-
let manifest;
|
|
2954
|
-
let contents;
|
|
2955
|
-
try {
|
|
2956
|
-
contents = await readFile(packagePath, 'utf8');
|
|
2957
|
-
} catch (error) {
|
|
2958
|
-
if (!isMissingPathError(error)) {
|
|
2959
|
-
throw new CliError('package_manifest_unreadable', `Could not read package manifest at ${packagePath}.`, {
|
|
2960
|
-
details: {
|
|
2961
|
-
path: packagePath,
|
|
2962
|
-
cause: error instanceof Error ? error.message : String(error)
|
|
2963
|
-
}
|
|
2964
|
-
});
|
|
2965
|
-
}
|
|
2966
|
-
return {
|
|
2967
|
-
framework: 'unknown',
|
|
2968
|
-
manager: await detectPackageManager(cwd),
|
|
2969
|
-
missing: []
|
|
2970
|
-
};
|
|
2971
|
-
}
|
|
2972
|
-
try {
|
|
2973
|
-
manifest = JSON.parse(contents);
|
|
2974
|
-
} catch (error) {
|
|
2975
|
-
throw new CliError('invalid_package_manifest', `Invalid package manifest at ${packagePath}.`, {
|
|
2976
|
-
details: {
|
|
2977
|
-
path: packagePath,
|
|
2978
|
-
cause: error instanceof Error ? error.message : String(error)
|
|
2979
|
-
},
|
|
2980
|
-
exitCode: 2
|
|
2981
|
-
});
|
|
2982
|
-
}
|
|
2983
|
-
const dependencies = {
|
|
2984
|
-
...manifest.dependencies,
|
|
2985
|
-
...manifest.devDependencies
|
|
2986
|
-
};
|
|
2987
|
-
const framework = dependencies.next ? 'next' : dependencies.react ? 'react' : 'node';
|
|
2988
|
-
const wanted = framework === 'next' || framework === 'react' ? [
|
|
2989
|
-
'@edgestore/server',
|
|
2990
|
-
'@edgestore/react'
|
|
2991
|
-
] : [
|
|
2992
|
-
'@edgestore/server'
|
|
2993
|
-
];
|
|
2994
|
-
return {
|
|
2995
|
-
framework,
|
|
2996
|
-
manager: await detectPackageManager(cwd),
|
|
2997
|
-
missing: wanted.filter((name)=>!dependencies[name]),
|
|
2998
|
-
installAtWorkspaceRoot: options.installAtWorkspaceRoot,
|
|
2999
|
-
...workspaceRoot && workspaceRoot !== resolvedCwd ? {
|
|
3000
|
-
workspace: {
|
|
3001
|
-
root: workspaceRoot,
|
|
3002
|
-
...manifest.name ? {
|
|
3003
|
-
packageName: manifest.name
|
|
3004
|
-
} : {}
|
|
3005
|
-
}
|
|
3006
|
-
} : {}
|
|
3007
|
-
};
|
|
3008
|
-
}
|
|
3009
|
-
async function installPackages(runtime, plan, options) {
|
|
3010
|
-
if (!plan.manager || !plan.missing.length) return {
|
|
3011
|
-
ran: false
|
|
3012
|
-
};
|
|
3013
|
-
const args = installArgs(plan.manager, plan.missing, plan.installAtWorkspaceRoot);
|
|
3014
|
-
const command = renderInstallCommand(plan.manager, args, {
|
|
3015
|
-
plan,
|
|
3016
|
-
packageCwd: options.cwd,
|
|
3017
|
-
invocationCwd: options.invocationCwd
|
|
3018
|
-
});
|
|
3019
|
-
const shouldInstall = options.requested ?? (options.interactive ? await runtime.prompts.confirm(`Install EdgeStore packages with ${plan.manager}?`, true) : false);
|
|
3020
|
-
if (!shouldInstall) return {
|
|
3021
|
-
command,
|
|
3022
|
-
cwd: options.cwd,
|
|
3023
|
-
ran: false
|
|
3024
|
-
};
|
|
3025
|
-
const captured = options.structured ? createOutputCapture() : undefined;
|
|
3026
|
-
try {
|
|
3027
|
-
await runtime.runCommand(plan.manager, args, {
|
|
3028
|
-
cwd: options.cwd,
|
|
3029
|
-
...captured ? {
|
|
3030
|
-
stdout: captured.stream,
|
|
3031
|
-
stderr: captured.stream
|
|
3032
|
-
} : {}
|
|
3033
|
-
});
|
|
3034
|
-
} catch (error) {
|
|
3035
|
-
const diagnostics = captured?.read().trim();
|
|
3036
|
-
if (diagnostics && !options.json) {
|
|
3037
|
-
runtime.io.stderr.write(`${diagnostics}\n`);
|
|
3038
|
-
}
|
|
3039
|
-
throw new CliError('package_install_failed', error instanceof Error ? error.message : 'Package installation failed.', {
|
|
3040
|
-
...diagnostics ? {
|
|
3041
|
-
details: {
|
|
3042
|
-
diagnostics
|
|
3043
|
-
}
|
|
3044
|
-
} : {},
|
|
3045
|
-
suggestions: [
|
|
3046
|
-
command
|
|
3047
|
-
]
|
|
3048
|
-
});
|
|
3049
|
-
}
|
|
3050
|
-
const diagnostics = captured?.read();
|
|
3051
|
-
if (diagnostics) {
|
|
3052
|
-
runtime.io.stderr.write(diagnostics.endsWith('\n') ? diagnostics : `${diagnostics}\n`);
|
|
3053
|
-
}
|
|
3054
|
-
return {
|
|
3055
|
-
command,
|
|
3056
|
-
cwd: options.cwd,
|
|
3057
|
-
ran: true
|
|
3058
|
-
};
|
|
3059
|
-
}
|
|
3060
|
-
function renderInstallCommand(manager, args, context) {
|
|
3061
|
-
const { plan, packageCwd, invocationCwd } = context;
|
|
3062
|
-
const workspace = plan.workspace;
|
|
3063
|
-
if (invocationCwd === workspace?.root) {
|
|
3064
|
-
const workspacePath = path.relative(workspace.root, packageCwd).replaceAll(path.sep, '/');
|
|
3065
|
-
if (manager === 'pnpm') {
|
|
3066
|
-
return renderShellCommand([
|
|
3067
|
-
manager,
|
|
3068
|
-
'--filter',
|
|
3069
|
-
`./${workspacePath}`,
|
|
3070
|
-
...args
|
|
3071
|
-
]);
|
|
3072
|
-
}
|
|
3073
|
-
if (manager === 'npm') {
|
|
3074
|
-
return renderShellCommand([
|
|
3075
|
-
manager,
|
|
3076
|
-
...args,
|
|
3077
|
-
'--workspace',
|
|
3078
|
-
workspacePath
|
|
3079
|
-
]);
|
|
3080
|
-
}
|
|
3081
|
-
if (manager === 'yarn' && workspace.packageName) {
|
|
3082
|
-
return renderShellCommand([
|
|
3083
|
-
manager,
|
|
3084
|
-
'workspace',
|
|
3085
|
-
workspace.packageName,
|
|
3086
|
-
...args
|
|
3087
|
-
]);
|
|
4361
|
+
message: cause.message,
|
|
4362
|
+
...cause.options.details === undefined ? {} : {
|
|
4363
|
+
details: cause.options.details
|
|
4364
|
+
}
|
|
4365
|
+
}
|
|
4366
|
+
},
|
|
4367
|
+
requestId: cause.options.requestId,
|
|
4368
|
+
exitCode: cause.exitCode
|
|
4369
|
+
});
|
|
3088
4370
|
}
|
|
3089
4371
|
}
|
|
3090
|
-
const
|
|
3091
|
-
|
|
3092
|
-
|
|
3093
|
-
|
|
3094
|
-
|
|
3095
|
-
|
|
3096
|
-
|
|
3097
|
-
|
|
3098
|
-
|
|
3099
|
-
|
|
3100
|
-
|
|
3101
|
-
|
|
3102
|
-
}
|
|
3103
|
-
if (manager === 'pnpm') {
|
|
3104
|
-
return renderShellCommand([
|
|
3105
|
-
manager,
|
|
3106
|
-
'--dir',
|
|
3107
|
-
relativeCwd,
|
|
3108
|
-
...args
|
|
3109
|
-
]);
|
|
3110
|
-
}
|
|
3111
|
-
return renderShellCommand([
|
|
3112
|
-
manager,
|
|
3113
|
-
'--cwd',
|
|
3114
|
-
relativeCwd,
|
|
3115
|
-
...args
|
|
3116
|
-
]);
|
|
4372
|
+
const successCount = results.filter((result)=>result.success).length;
|
|
4373
|
+
const failureCount = results.length - successCount;
|
|
4374
|
+
const human = [
|
|
4375
|
+
`Deleted ${successCount} file(s); ${failureCount} failed.`,
|
|
4376
|
+
...results.filter((result)=>!result.success).map((result)=>` ${fileReferenceLabel(result.fileRef)}: ${result.error.message}`)
|
|
4377
|
+
].join('\n');
|
|
4378
|
+
outputFor(runtime, flags).result({
|
|
4379
|
+
results,
|
|
4380
|
+
successCount,
|
|
4381
|
+
failureCount
|
|
4382
|
+
}, human);
|
|
4383
|
+
if (failureCount) runtime.exitCode = 1;
|
|
3117
4384
|
}
|
|
3118
|
-
function
|
|
3119
|
-
if (framework === 'next') {
|
|
3120
|
-
return [
|
|
3121
|
-
'Next steps:',
|
|
3122
|
-
' Configure an EdgeStore router in your Next.js app.',
|
|
3123
|
-
' Add the EdgeStore provider to your client layout.'
|
|
3124
|
-
];
|
|
3125
|
-
}
|
|
3126
|
-
if (framework === 'react') {
|
|
3127
|
-
return [
|
|
3128
|
-
'Next steps:',
|
|
3129
|
-
' Configure an EdgeStore server endpoint.',
|
|
3130
|
-
' Add the EdgeStore provider to your React app.'
|
|
3131
|
-
];
|
|
3132
|
-
}
|
|
4385
|
+
function renderIncompleteDeletion(input) {
|
|
3133
4386
|
return [
|
|
3134
|
-
|
|
3135
|
-
|
|
3136
|
-
|
|
4387
|
+
`File deletion stopped: ${input.cause.message}`,
|
|
4388
|
+
`Completed: ${input.successCount} succeeded, ${input.failureCount} failed.`,
|
|
4389
|
+
...input.results.map((result)=>` ${JSON.stringify(result.fileRef)}: ${result.success ? 'deleted' : `failed: ${result.error?.message ?? 'unknown failure'}`}`),
|
|
4390
|
+
`Uncertain (${input.uncertainReferences.length}):`,
|
|
4391
|
+
...input.uncertainReferences.map((reference)=>` ${reference}`),
|
|
4392
|
+
`Not attempted (${input.notAttemptedReferences.length}):`,
|
|
4393
|
+
...input.notAttemptedReferences.map((reference)=>` ${reference}`)
|
|
4394
|
+
].join('\n');
|
|
3137
4395
|
}
|
|
3138
|
-
function
|
|
3139
|
-
|
|
4396
|
+
function fileReference(value, bucket) {
|
|
4397
|
+
if (bucket) return {
|
|
4398
|
+
bucketName: bucket,
|
|
4399
|
+
path: value
|
|
4400
|
+
};
|
|
4401
|
+
if (/^https?:\/\//i.test(value)) return {
|
|
4402
|
+
url: value
|
|
4403
|
+
};
|
|
3140
4404
|
return {
|
|
3141
|
-
|
|
3142
|
-
write (chunk, _encoding, callback) {
|
|
3143
|
-
output = `${output}${Buffer.from(chunk).toString('utf8')}`.slice(-limit);
|
|
3144
|
-
callback();
|
|
3145
|
-
}
|
|
3146
|
-
}),
|
|
3147
|
-
read: ()=>output
|
|
4405
|
+
id: value
|
|
3148
4406
|
};
|
|
3149
4407
|
}
|
|
3150
|
-
function
|
|
3151
|
-
|
|
3152
|
-
|
|
3153
|
-
|
|
3154
|
-
|
|
3155
|
-
const boundary = await findWorkspaceRoot(start) ?? await findGitRoot(start) ?? start;
|
|
3156
|
-
let directory = start;
|
|
3157
|
-
while(true){
|
|
3158
|
-
for (const strategy of [
|
|
3159
|
-
'packageManager-field',
|
|
3160
|
-
'lockfile',
|
|
3161
|
-
'devEngines-field'
|
|
3162
|
-
]){
|
|
3163
|
-
const result = await detect({
|
|
3164
|
-
cwd: directory,
|
|
3165
|
-
stopDir: directory,
|
|
3166
|
-
strategies: [
|
|
3167
|
-
strategy
|
|
3168
|
-
],
|
|
3169
|
-
packageJsonParser: (contents)=>packageJsonForStrategy(contents, strategy)
|
|
3170
|
-
});
|
|
3171
|
-
if (result?.name === 'pnpm' || result?.name === 'npm' || result?.name === 'yarn' || result?.name === 'bun') {
|
|
3172
|
-
return result.name;
|
|
3173
|
-
}
|
|
3174
|
-
}
|
|
3175
|
-
if (directory === boundary) break;
|
|
3176
|
-
directory = path.dirname(directory);
|
|
3177
|
-
}
|
|
3178
|
-
return 'npm';
|
|
3179
|
-
}
|
|
3180
|
-
function packageJsonForStrategy(contents, strategy) {
|
|
3181
|
-
const manifest = JSON.parse(contents);
|
|
3182
|
-
if (strategy !== 'packageManager-field') delete manifest.packageManager;
|
|
3183
|
-
if (strategy !== 'devEngines-field') delete manifest.devEngines;
|
|
3184
|
-
return manifest;
|
|
3185
|
-
}
|
|
3186
|
-
function installArgs(manager, packages, installAtWorkspaceRoot = false) {
|
|
3187
|
-
if (manager === 'npm') return [
|
|
3188
|
-
'install',
|
|
3189
|
-
...packages
|
|
3190
|
-
];
|
|
3191
|
-
if (manager === 'pnpm' && installAtWorkspaceRoot) {
|
|
3192
|
-
return [
|
|
3193
|
-
'add',
|
|
3194
|
-
'-w',
|
|
3195
|
-
...packages
|
|
3196
|
-
];
|
|
3197
|
-
}
|
|
3198
|
-
return [
|
|
3199
|
-
'add',
|
|
3200
|
-
...packages
|
|
3201
|
-
];
|
|
4408
|
+
function fileReferenceLabel(reference) {
|
|
4409
|
+
if ('id' in reference) return reference.id;
|
|
4410
|
+
if ('key' in reference) return reference.key;
|
|
4411
|
+
if ('url' in reference) return reference.url;
|
|
4412
|
+
return `${reference.bucketName}/${reference.path}`;
|
|
3202
4413
|
}
|
|
3203
4414
|
|
|
3204
4415
|
async function deliverEnvSecretWithRollback(input) {
|
|
@@ -3447,13 +4658,25 @@ function errorDetails$1(error) {
|
|
|
3447
4658
|
const execFileAsync = promisify(execFile);
|
|
3448
4659
|
async function resolveSecretOutput(runtime, explicitOutput, interactive) {
|
|
3449
4660
|
if (explicitOutput) return explicitOutput;
|
|
3450
|
-
|
|
4661
|
+
const linked = await runtime.repoConfig.read();
|
|
4662
|
+
if (linked?.config.envFile) return linked.config.envFile;
|
|
3451
4663
|
const discovered = (await readdir(runtime.workspaceCwd, {
|
|
3452
4664
|
withFileTypes: true
|
|
3453
4665
|
})).filter((entry)=>entry.isFile() && entry.name.startsWith('.env') && !/\.(?:example|sample|template)$/i.test(entry.name)).map((entry)=>entry.name).sort();
|
|
3454
4666
|
if (!discovered.length || discovered.every((name)=>name === '.env.local')) {
|
|
3455
4667
|
return '.env.local';
|
|
3456
4668
|
}
|
|
4669
|
+
if (!interactive) {
|
|
4670
|
+
if (discovered.length === 1 && [
|
|
4671
|
+
'.env',
|
|
4672
|
+
'.env.local',
|
|
4673
|
+
'.env.development',
|
|
4674
|
+
'.env.development.local'
|
|
4675
|
+
].includes(discovered[0])) return discovered[0];
|
|
4676
|
+
throw usageError('secret_output_required', 'Existing environment files require an explicit --output choice.', [
|
|
4677
|
+
'Inspect the backend environment loader and pass --output <file>.'
|
|
4678
|
+
]);
|
|
4679
|
+
}
|
|
3457
4680
|
const choices = Array.from(new Set([
|
|
3458
4681
|
...discovered,
|
|
3459
4682
|
'.env.local'
|
|
@@ -3474,6 +4697,13 @@ async function protectSecretFile(cwd, output) {
|
|
|
3474
4697
|
const boundary = gitRoot ?? packageRoot;
|
|
3475
4698
|
const repositoryRelative = path.relative(boundary, absolute);
|
|
3476
4699
|
const packageRelative = path.relative(packageRoot, absolute);
|
|
4700
|
+
const realPackageRoot = await realpath(packageRoot);
|
|
4701
|
+
const realParent = await realpath(path.dirname(absolute));
|
|
4702
|
+
const realRelative = path.relative(realPackageRoot, realParent);
|
|
4703
|
+
if (realRelative === '..' || realRelative.startsWith(`..${path.sep}`) || path.isAbsolute(realRelative)) {
|
|
4704
|
+
throw usageError('secret_output_unprotected', 'Secret output must not escape the selected package through a symlink.');
|
|
4705
|
+
}
|
|
4706
|
+
await rejectSymlink(absolute);
|
|
3477
4707
|
if (!packageRelative || packageRelative.startsWith('..') || path.isAbsolute(packageRelative)) {
|
|
3478
4708
|
throw usageError('secret_output_unprotected', `Secret output ${absolute} must be inside ${packageRoot}.`, [
|
|
3479
4709
|
'Choose an env file inside the selected package.'
|
|
@@ -3490,6 +4720,7 @@ async function protectSecretFile(cwd, output) {
|
|
|
3490
4720
|
if (await isIgnoredFile(gitRoot, gitPath)) return normalized;
|
|
3491
4721
|
}
|
|
3492
4722
|
const gitignorePath = path.join(packageRoot, '.gitignore');
|
|
4723
|
+
await rejectSymlink(gitignorePath);
|
|
3493
4724
|
let contents = '';
|
|
3494
4725
|
try {
|
|
3495
4726
|
contents = await readFile(gitignorePath, 'utf8');
|
|
@@ -3513,6 +4744,13 @@ async function protectSecretFile(cwd, output) {
|
|
|
3513
4744
|
}
|
|
3514
4745
|
return normalized;
|
|
3515
4746
|
}
|
|
4747
|
+
async function rejectSymlink(file) {
|
|
4748
|
+
try {
|
|
4749
|
+
if ((await lstat(file)).isSymbolicLink()) throw usageError('secret_output_unprotected', `Refusing to overwrite a symlink at ${file}.`);
|
|
4750
|
+
} catch (error) {
|
|
4751
|
+
if (error.code !== 'ENOENT') throw error;
|
|
4752
|
+
}
|
|
4753
|
+
}
|
|
3516
4754
|
async function isIgnoredFile(root, relative) {
|
|
3517
4755
|
try {
|
|
3518
4756
|
await execFileAsync('git', [
|
|
@@ -4043,6 +5281,42 @@ function requireInteractive(interactive, message) {
|
|
|
4043
5281
|
}
|
|
4044
5282
|
}
|
|
4045
5283
|
|
|
5284
|
+
async function mcpCommand(runtime, flags, input) {
|
|
5285
|
+
const { action } = input;
|
|
5286
|
+
const options = await agentOptions(runtime, flags, input);
|
|
5287
|
+
const { client } = options;
|
|
5288
|
+
const plan = await planMcp(options, action);
|
|
5289
|
+
if (plan.changes.length && !input.dryRun) {
|
|
5290
|
+
if (!input.yes) {
|
|
5291
|
+
if (!isInteractive(runtime, flags)) throw usageError('confirmation_required', 'Use --yes to apply changes or --dry-run to preview them.');
|
|
5292
|
+
if (!await runtime.prompts.confirm(`${action === 'remove' ? 'Remove EdgeStore MCP from' : 'Configure EdgeStore MCP in'} ${plan.result.configPath}?`)) return;
|
|
5293
|
+
}
|
|
5294
|
+
await applyChanges(plan.changes);
|
|
5295
|
+
}
|
|
5296
|
+
const current = plan.changes.length && !input.dryRun ? (await planMcp(options, 'status')).result : plan.result;
|
|
5297
|
+
const result = {
|
|
5298
|
+
...current,
|
|
5299
|
+
action,
|
|
5300
|
+
dryRun: Boolean(input.dryRun),
|
|
5301
|
+
changedFiles: input.dryRun ? [] : plan.changes.map(({ file })=>file),
|
|
5302
|
+
plannedFiles: plan.result.plannedFiles
|
|
5303
|
+
};
|
|
5304
|
+
outputFor(runtime, flags).result(result, [
|
|
5305
|
+
`${client}: ${result.status}`,
|
|
5306
|
+
result.configPath,
|
|
5307
|
+
...result.warnings,
|
|
5308
|
+
...input.dryRun ? [
|
|
5309
|
+
'Dry run; no files changed.'
|
|
5310
|
+
] : [],
|
|
5311
|
+
...action === 'status' ? [
|
|
5312
|
+
'Connection and sign-in were not checked.'
|
|
5313
|
+
] : [],
|
|
5314
|
+
...!input.dryRun && action === 'setup' ? [
|
|
5315
|
+
'Open your client to connect EdgeStore.'
|
|
5316
|
+
] : []
|
|
5317
|
+
].join('\n'));
|
|
5318
|
+
}
|
|
5319
|
+
|
|
4046
5320
|
async function memberListCommand(runtime, flags, options) {
|
|
4047
5321
|
const account = await selectedAccount(runtime, flags);
|
|
4048
5322
|
if (account.type === 'PERSONAL') {
|
|
@@ -4327,7 +5601,7 @@ async function projectKeyListCommand(runtime, flags, project) {
|
|
|
4327
5601
|
], rows) : 'No project keys found.');
|
|
4328
5602
|
}
|
|
4329
5603
|
async function projectKeyCreateCommand(runtime, flags, input) {
|
|
4330
|
-
|
|
5604
|
+
requireSecretDelivery(runtime, flags, input);
|
|
4331
5605
|
await preflightKeyDelivery(runtime, input);
|
|
4332
5606
|
const sdk = await sdkFor(runtime, flags);
|
|
4333
5607
|
const { result, delivered } = await createAndDeliverKey({
|
|
@@ -4336,7 +5610,10 @@ async function projectKeyCreateCommand(runtime, flags, input) {
|
|
|
4336
5610
|
sdk
|
|
4337
5611
|
}, input);
|
|
4338
5612
|
const values = keyValues(result.key.accessKey, result.secretKey);
|
|
4339
|
-
outputFor(runtime, flags).result(
|
|
5613
|
+
outputFor(runtime, flags).result({
|
|
5614
|
+
key: result.key,
|
|
5615
|
+
delivery: delivered
|
|
5616
|
+
}, [
|
|
4340
5617
|
`Created project key "${result.key.name}".`,
|
|
4341
5618
|
...delivered.length ? [
|
|
4342
5619
|
'',
|
|
@@ -4409,7 +5686,7 @@ async function projectKeyRotateCommand(runtime, flags, input) {
|
|
|
4409
5686
|
} else if (!input.copy && !input.output) {
|
|
4410
5687
|
throw usageError('secret_delivery_required', 'Non-interactive rotation requires --copy or --output.');
|
|
4411
5688
|
}
|
|
4412
|
-
|
|
5689
|
+
requireSecretDelivery(runtime, flags, input);
|
|
4413
5690
|
const sdk = await sdkFor(runtime, flags);
|
|
4414
5691
|
const listed = await sdk.management.projectKeys.list({
|
|
4415
5692
|
project: input.project,
|
|
@@ -4541,7 +5818,10 @@ async function projectKeyRotateCommand(runtime, flags, input) {
|
|
|
4541
5818
|
});
|
|
4542
5819
|
}
|
|
4543
5820
|
output.result({
|
|
4544
|
-
replacement:
|
|
5821
|
+
replacement: {
|
|
5822
|
+
key: result.key,
|
|
5823
|
+
delivery: delivered
|
|
5824
|
+
},
|
|
4545
5825
|
revokedKeyId: input.keyId
|
|
4546
5826
|
}, input.yes ? [
|
|
4547
5827
|
secretMessage,
|
|
@@ -4590,10 +5870,11 @@ async function preflightKeyDelivery(runtime, options) {
|
|
|
4590
5870
|
'EDGE_STORE_ACCESS_KEY',
|
|
4591
5871
|
'EDGE_STORE_SECRET_KEY'
|
|
4592
5872
|
], options);
|
|
5873
|
+
if (options.output) await protectSecretFile(runtime.cwd, options.output);
|
|
4593
5874
|
}
|
|
4594
|
-
function
|
|
4595
|
-
if (flags
|
|
4596
|
-
throw usageError('secret_delivery_required', '
|
|
5875
|
+
function requireSecretDelivery(runtime, flags, options) {
|
|
5876
|
+
if (!isInteractive(runtime, flags) && !options.output) {
|
|
5877
|
+
throw usageError('secret_delivery_required', 'Automated project-key commands require --output to deliver the one-time key to a protected env file.');
|
|
4597
5878
|
}
|
|
4598
5879
|
}
|
|
4599
5880
|
function keyValues(accessKey, secretKey) {
|
|
@@ -4666,12 +5947,13 @@ async function tokenCreateCommand(runtime, flags, options) {
|
|
|
4666
5947
|
if (!options.preset && !options.scope?.length) {
|
|
4667
5948
|
throw usageError('token_permissions_required', 'Token creation requires --preset or at least one --scope.');
|
|
4668
5949
|
}
|
|
4669
|
-
if (flags
|
|
4670
|
-
throw usageError('secret_delivery_required', '
|
|
5950
|
+
if (!isInteractive(runtime, flags) && !options.output) {
|
|
5951
|
+
throw usageError('secret_delivery_required', 'Automated token creation requires --output to deliver the one-time token to a protected env file.');
|
|
4671
5952
|
}
|
|
4672
5953
|
await preflightEnvSecret(runtime.cwd, [
|
|
4673
5954
|
'EDGESTORE_TOKEN'
|
|
4674
5955
|
], options);
|
|
5956
|
+
if (options.output) await protectSecretFile(runtime.cwd, options.output);
|
|
4675
5957
|
const sdk = await sdkFor(runtime, flags);
|
|
4676
5958
|
const permissions = options.preset ? {
|
|
4677
5959
|
preset: options.preset
|
|
@@ -4716,7 +5998,10 @@ async function tokenCreateCommand(runtime, flags, options) {
|
|
|
4716
5998
|
'Use a credential with token:revoke access or revoke the token in the dashboard.'
|
|
4717
5999
|
]
|
|
4718
6000
|
});
|
|
4719
|
-
outputFor(runtime, flags).result(
|
|
6001
|
+
outputFor(runtime, flags).result({
|
|
6002
|
+
token: result.token,
|
|
6003
|
+
delivery: delivered
|
|
6004
|
+
}, [
|
|
4720
6005
|
`Created ${result.token.kind.toLowerCase()} token "${result.token.name}".`,
|
|
4721
6006
|
...delivered.length ? [
|
|
4722
6007
|
'',
|
|
@@ -4751,6 +6036,141 @@ async function tokenRevokeCommand(runtime, flags, input) {
|
|
|
4751
6036
|
outputFor(runtime, flags).result(result, `Revoked management token ${input.tokenId}.`, input.tokenId);
|
|
4752
6037
|
}
|
|
4753
6038
|
|
|
6039
|
+
const SPINNER_FRAMES = [
|
|
6040
|
+
'◒',
|
|
6041
|
+
'◐',
|
|
6042
|
+
'◓',
|
|
6043
|
+
'◑'
|
|
6044
|
+
];
|
|
6045
|
+
const BAR_WIDTH = 18;
|
|
6046
|
+
class UploadProgressDisplay {
|
|
6047
|
+
live;
|
|
6048
|
+
entries = new Map();
|
|
6049
|
+
colors;
|
|
6050
|
+
timer;
|
|
6051
|
+
frame = 0;
|
|
6052
|
+
constructor(live, color){
|
|
6053
|
+
this.live = live;
|
|
6054
|
+
this.colors = createColors(color);
|
|
6055
|
+
}
|
|
6056
|
+
start(id, fileName, totalBytes) {
|
|
6057
|
+
this.entries.set(id, {
|
|
6058
|
+
fileName,
|
|
6059
|
+
totalBytes,
|
|
6060
|
+
progress: {
|
|
6061
|
+
transferredBytes: 0,
|
|
6062
|
+
totalBytes,
|
|
6063
|
+
percentage: 0,
|
|
6064
|
+
phase: 'preparing'
|
|
6065
|
+
},
|
|
6066
|
+
status: 'active'
|
|
6067
|
+
});
|
|
6068
|
+
this.startTimer();
|
|
6069
|
+
this.render();
|
|
6070
|
+
}
|
|
6071
|
+
update(id, progress) {
|
|
6072
|
+
const entry = this.entries.get(id);
|
|
6073
|
+
if (entry?.status !== 'active' || sameProgress(entry.progress, progress)) return;
|
|
6074
|
+
entry.progress = progress;
|
|
6075
|
+
}
|
|
6076
|
+
succeed(id) {
|
|
6077
|
+
const entry = this.entries.get(id);
|
|
6078
|
+
if (!entry) return;
|
|
6079
|
+
entry.status = 'succeeded';
|
|
6080
|
+
this.afterEntryFinished();
|
|
6081
|
+
}
|
|
6082
|
+
fail(id, canceled) {
|
|
6083
|
+
const entry = this.entries.get(id);
|
|
6084
|
+
if (!entry) return;
|
|
6085
|
+
entry.status = canceled ? 'canceled' : 'failed';
|
|
6086
|
+
this.afterEntryFinished();
|
|
6087
|
+
}
|
|
6088
|
+
close() {
|
|
6089
|
+
this.stopTimer();
|
|
6090
|
+
this.render();
|
|
6091
|
+
this.live.done();
|
|
6092
|
+
this.entries.clear();
|
|
6093
|
+
}
|
|
6094
|
+
startTimer() {
|
|
6095
|
+
if (this.timer) return;
|
|
6096
|
+
this.timer = setInterval(()=>{
|
|
6097
|
+
this.frame = (this.frame + 1) % SPINNER_FRAMES.length;
|
|
6098
|
+
this.render();
|
|
6099
|
+
}, 80);
|
|
6100
|
+
this.timer.unref();
|
|
6101
|
+
}
|
|
6102
|
+
stopTimer() {
|
|
6103
|
+
if (!this.timer) return;
|
|
6104
|
+
clearInterval(this.timer);
|
|
6105
|
+
this.timer = undefined;
|
|
6106
|
+
}
|
|
6107
|
+
afterEntryFinished() {
|
|
6108
|
+
if ([
|
|
6109
|
+
...this.entries.values()
|
|
6110
|
+
].every((entry)=>entry.status !== 'active')) {
|
|
6111
|
+
this.stopTimer();
|
|
6112
|
+
}
|
|
6113
|
+
this.render();
|
|
6114
|
+
}
|
|
6115
|
+
render() {
|
|
6116
|
+
if (!this.entries.size) {
|
|
6117
|
+
this.live.clear();
|
|
6118
|
+
return;
|
|
6119
|
+
}
|
|
6120
|
+
const spinner = this.colors.magenta(SPINNER_FRAMES[this.frame] ?? '◒');
|
|
6121
|
+
this.live([
|
|
6122
|
+
...this.entries.entries()
|
|
6123
|
+
].sort(([left], [right])=>left - right).map(([, entry])=>renderEntry(entry, spinner, this.colors)).join('\n'));
|
|
6124
|
+
}
|
|
6125
|
+
}
|
|
6126
|
+
function createUploadProgressDisplay(runtime, flags) {
|
|
6127
|
+
const enabled = flags.progress && !flags.json && !flags.plain && runtime.io.stderrIsTty;
|
|
6128
|
+
if (!enabled) return undefined;
|
|
6129
|
+
return new UploadProgressDisplay(createLogUpdate(runtime.io.stderr, {
|
|
6130
|
+
showCursor: true,
|
|
6131
|
+
defaultWidth: 80
|
|
6132
|
+
}), flags.color && runtime.env.NO_COLOR === undefined);
|
|
6133
|
+
}
|
|
6134
|
+
function renderEntry(entry, spinner, colors) {
|
|
6135
|
+
if (entry.status === 'succeeded') {
|
|
6136
|
+
return `${colors.green('◆')} ${entry.fileName} ${colors.green('uploaded')} · ${formatBytes(entry.totalBytes)}`;
|
|
6137
|
+
}
|
|
6138
|
+
if (entry.status !== 'active') {
|
|
6139
|
+
return `${colors.red('■')} ${entry.fileName} ${colors.red(entry.status)}`;
|
|
6140
|
+
}
|
|
6141
|
+
const { progress } = entry;
|
|
6142
|
+
const percentage = Math.max(0, Math.min(100, progress.percentage));
|
|
6143
|
+
const completed = Math.round(percentage / 100 * BAR_WIDTH);
|
|
6144
|
+
const bar = `${colors.magenta('━'.repeat(completed))}${colors.dim('─'.repeat(BAR_WIDTH - completed))}`;
|
|
6145
|
+
const transfer = `${formatBytes(progress.transferredBytes)} / ${formatBytes(progress.totalBytes)}`;
|
|
6146
|
+
return `${spinner} ${entry.fileName} ${bar} ${formatPercentage(percentage)} · ${transfer} · ${progress.phase}`;
|
|
6147
|
+
}
|
|
6148
|
+
function sameProgress(left, right) {
|
|
6149
|
+
return left.transferredBytes === right.transferredBytes && left.totalBytes === right.totalBytes && left.percentage === right.percentage && left.phase === right.phase;
|
|
6150
|
+
}
|
|
6151
|
+
function formatPercentage(percentage) {
|
|
6152
|
+
return `${Number(percentage.toFixed(2))}%`;
|
|
6153
|
+
}
|
|
6154
|
+
function formatBytes(bytes) {
|
|
6155
|
+
if (bytes < 1024) return `${bytes} B`;
|
|
6156
|
+
const units = [
|
|
6157
|
+
'KB',
|
|
6158
|
+
'MB',
|
|
6159
|
+
'GB',
|
|
6160
|
+
'TB'
|
|
6161
|
+
];
|
|
6162
|
+
let value = bytes / 1024;
|
|
6163
|
+
let unit = units[0] ?? 'KB';
|
|
6164
|
+
for (const candidate of units.slice(1)){
|
|
6165
|
+
if (value < 1024) break;
|
|
6166
|
+
value /= 1024;
|
|
6167
|
+
unit = candidate;
|
|
6168
|
+
}
|
|
6169
|
+
const precision = value >= 10 ? 0 : 1;
|
|
6170
|
+
return `${Number(value.toFixed(precision))} ${unit}`;
|
|
6171
|
+
}
|
|
6172
|
+
|
|
6173
|
+
const FILE_UPLOAD_CONCURRENCY = 3;
|
|
4754
6174
|
async function fileUploadCommand(runtime, flags, input) {
|
|
4755
6175
|
if (flags.plain) {
|
|
4756
6176
|
throw usageError('plain_output_unavailable', 'File upload does not have a single plain-text result.', [
|
|
@@ -4772,73 +6192,64 @@ async function fileUploadCommand(runtime, flags, input) {
|
|
|
4772
6192
|
if (localFiles.length > 1 && input.path && !input.path.endsWith('/')) {
|
|
4773
6193
|
throw usageError('upload_path_not_folder', '--path must end in / when uploading multiple files.');
|
|
4774
6194
|
}
|
|
6195
|
+
validateUniqueUploadDestinations(localFiles, {
|
|
6196
|
+
destinationPath: input.path,
|
|
6197
|
+
keepName: input.keepName
|
|
6198
|
+
});
|
|
4775
6199
|
const project = await resolvedProjectRef(runtime, flags, input.project);
|
|
4776
6200
|
const sdk = await sdkFor(runtime, flags);
|
|
4777
|
-
const
|
|
4778
|
-
|
|
4779
|
-
|
|
4780
|
-
|
|
4781
|
-
const mimeType = mimeTypeFor(fileName);
|
|
4782
|
-
const destination = resolveUploadDestination(input.path, {
|
|
4783
|
-
fileName,
|
|
4784
|
-
keepName: input.keepName
|
|
4785
|
-
});
|
|
4786
|
-
const source = await openAsBlob(localFile, mimeType ? {
|
|
4787
|
-
type: mimeType
|
|
4788
|
-
} : undefined);
|
|
6201
|
+
const progressDisplay = createUploadProgressDisplay(runtime, flags);
|
|
6202
|
+
let outcomes;
|
|
6203
|
+
try {
|
|
6204
|
+
const upload = async (localFile, index)=>{
|
|
4789
6205
|
try {
|
|
4790
|
-
|
|
4791
|
-
|
|
4792
|
-
|
|
4793
|
-
|
|
4794
|
-
|
|
4795
|
-
|
|
4796
|
-
|
|
4797
|
-
|
|
4798
|
-
|
|
4799
|
-
|
|
4800
|
-
|
|
4801
|
-
|
|
4802
|
-
|
|
4803
|
-
|
|
4804
|
-
|
|
4805
|
-
percentage: progress.percentage
|
|
4806
|
-
})
|
|
4807
|
-
});
|
|
4808
|
-
results.push({
|
|
4809
|
-
localPath: localFile,
|
|
4810
|
-
...completed
|
|
4811
|
-
});
|
|
6206
|
+
return {
|
|
6207
|
+
status: 'completed',
|
|
6208
|
+
result: await uploadLocalFile({
|
|
6209
|
+
runtime,
|
|
6210
|
+
flags,
|
|
6211
|
+
sdk,
|
|
6212
|
+
progressDisplay,
|
|
6213
|
+
index,
|
|
6214
|
+
localFile,
|
|
6215
|
+
project,
|
|
6216
|
+
bucket: input.bucket,
|
|
6217
|
+
destinationPath: input.path,
|
|
6218
|
+
keepName: input.keepName
|
|
6219
|
+
})
|
|
6220
|
+
};
|
|
4812
6221
|
} catch (error) {
|
|
4813
|
-
|
|
4814
|
-
|
|
4815
|
-
|
|
4816
|
-
|
|
4817
|
-
}
|
|
6222
|
+
return {
|
|
6223
|
+
status: 'failed',
|
|
6224
|
+
localPath: localFile,
|
|
6225
|
+
cause: normalizeError(error)
|
|
6226
|
+
};
|
|
4818
6227
|
}
|
|
4819
|
-
}
|
|
4820
|
-
|
|
4821
|
-
|
|
4822
|
-
|
|
4823
|
-
|
|
4824
|
-
|
|
4825
|
-
|
|
4826
|
-
|
|
4827
|
-
|
|
4828
|
-
|
|
4829
|
-
|
|
4830
|
-
|
|
4831
|
-
|
|
4832
|
-
|
|
4833
|
-
|
|
4834
|
-
|
|
4835
|
-
|
|
4836
|
-
|
|
4837
|
-
|
|
4838
|
-
|
|
4839
|
-
|
|
4840
|
-
|
|
4841
|
-
|
|
6228
|
+
};
|
|
6229
|
+
outcomes = localFiles.length === 1 ? [
|
|
6230
|
+
await upload(localFiles[0], 0)
|
|
6231
|
+
] : await mapConcurrent(localFiles, upload, {
|
|
6232
|
+
concurrency: FILE_UPLOAD_CONCURRENCY,
|
|
6233
|
+
signal: runtime.signal
|
|
6234
|
+
});
|
|
6235
|
+
} finally{
|
|
6236
|
+
progressDisplay?.close();
|
|
6237
|
+
}
|
|
6238
|
+
const results = outcomes.flatMap((outcome)=>outcome?.status === 'completed' ? [
|
|
6239
|
+
outcome.result
|
|
6240
|
+
] : []);
|
|
6241
|
+
const failures = outcomes.flatMap((outcome)=>outcome?.status === 'failed' ? [
|
|
6242
|
+
outcome
|
|
6243
|
+
] : []);
|
|
6244
|
+
const notAttemptedPaths = localFiles.filter((_, index)=>!outcomes[index]);
|
|
6245
|
+
if (failures.length || notAttemptedPaths.length) {
|
|
6246
|
+
if (localFiles.length === 1 && failures[0]) throw failures[0].cause;
|
|
6247
|
+
if (runtime.signal.aborted && !failures.length) throw interruptedError();
|
|
6248
|
+
throw batchUploadError({
|
|
6249
|
+
results,
|
|
6250
|
+
failures,
|
|
6251
|
+
notAttemptedPaths
|
|
6252
|
+
});
|
|
4842
6253
|
}
|
|
4843
6254
|
const human = results.map((result)=>{
|
|
4844
6255
|
const readUrl = result.signedReadUrl?.signedUrl ?? result.file.url;
|
|
@@ -4848,6 +6259,123 @@ async function fileUploadCommand(runtime, flags, input) {
|
|
|
4848
6259
|
uploads: results
|
|
4849
6260
|
}, human);
|
|
4850
6261
|
}
|
|
6262
|
+
async function uploadLocalFile(input) {
|
|
6263
|
+
const { progressDisplay } = input;
|
|
6264
|
+
const fileName = path.basename(input.localFile);
|
|
6265
|
+
const mimeType = mimeTypeFor(fileName);
|
|
6266
|
+
const destination = resolveUploadDestination(input.destinationPath, {
|
|
6267
|
+
fileName,
|
|
6268
|
+
keepName: input.keepName
|
|
6269
|
+
});
|
|
6270
|
+
const source = await openAsBlob(input.localFile, mimeType ? {
|
|
6271
|
+
type: mimeType
|
|
6272
|
+
} : undefined);
|
|
6273
|
+
progressDisplay?.start(input.index, fileName, source.size);
|
|
6274
|
+
try {
|
|
6275
|
+
const completed = await input.sdk.management.uploads.upload({
|
|
6276
|
+
project: input.project,
|
|
6277
|
+
bucket: input.bucket,
|
|
6278
|
+
source,
|
|
6279
|
+
signedReadUrl: {},
|
|
6280
|
+
...destination.fileName ? {
|
|
6281
|
+
fileName: destination.fileName
|
|
6282
|
+
} : {},
|
|
6283
|
+
...destination.path ? {
|
|
6284
|
+
path: destination.path
|
|
6285
|
+
} : {},
|
|
6286
|
+
mimeType,
|
|
6287
|
+
signal: input.runtime.signal,
|
|
6288
|
+
...progressDisplay ? {
|
|
6289
|
+
onProgress: (progress)=>progressDisplay.update(input.index, progress)
|
|
6290
|
+
} : {}
|
|
6291
|
+
});
|
|
6292
|
+
progressDisplay?.succeed(input.index);
|
|
6293
|
+
return {
|
|
6294
|
+
localPath: input.localFile,
|
|
6295
|
+
...completed
|
|
6296
|
+
};
|
|
6297
|
+
} catch (error) {
|
|
6298
|
+
progressDisplay?.fail(input.index, input.runtime.signal.aborted);
|
|
6299
|
+
throw normalizeUploadError(error, {
|
|
6300
|
+
runtime: input.runtime,
|
|
6301
|
+
flags: input.flags,
|
|
6302
|
+
project: input.project
|
|
6303
|
+
});
|
|
6304
|
+
}
|
|
6305
|
+
}
|
|
6306
|
+
async function mapConcurrent(values, mapper, options) {
|
|
6307
|
+
const results = Array(values.length);
|
|
6308
|
+
let nextIndex = 0;
|
|
6309
|
+
const workers = Array.from({
|
|
6310
|
+
length: Math.min(options.concurrency, values.length)
|
|
6311
|
+
}, async ()=>{
|
|
6312
|
+
while(!options.signal.aborted){
|
|
6313
|
+
const index = nextIndex++;
|
|
6314
|
+
if (index >= values.length) return;
|
|
6315
|
+
results[index] = await mapper(values[index], index);
|
|
6316
|
+
}
|
|
6317
|
+
});
|
|
6318
|
+
await Promise.all(workers);
|
|
6319
|
+
return results;
|
|
6320
|
+
}
|
|
6321
|
+
function batchUploadError(input) {
|
|
6322
|
+
const firstFailure = input.failures[0]?.cause;
|
|
6323
|
+
const suggestions = Array.from(new Set(input.failures.flatMap((failure)=>failure.cause.options.suggestions ?? [])));
|
|
6324
|
+
const lines = [
|
|
6325
|
+
`Upload batch finished with ${input.failures.length} failure${input.failures.length === 1 ? '' : 's'}.`,
|
|
6326
|
+
`Completed (${input.results.length}):`,
|
|
6327
|
+
...input.results.map((result)=>` ${result.localPath} -> ${result.signedReadUrl?.signedUrl ?? result.file.url} (${result.file.id})`),
|
|
6328
|
+
`Failed (${input.failures.length}):`,
|
|
6329
|
+
...input.failures.map((failure)=>` ${failure.localPath}: ${failure.cause.message}`),
|
|
6330
|
+
...input.notAttemptedPaths.length ? [
|
|
6331
|
+
`Not attempted (${input.notAttemptedPaths.length}):`,
|
|
6332
|
+
...input.notAttemptedPaths.map((file)=>` ${file}`)
|
|
6333
|
+
] : []
|
|
6334
|
+
];
|
|
6335
|
+
return new CliError('file_upload_incomplete', lines.join('\n'), {
|
|
6336
|
+
details: {
|
|
6337
|
+
completed: input.results,
|
|
6338
|
+
failures: input.failures.map((failure)=>({
|
|
6339
|
+
localPath: failure.localPath,
|
|
6340
|
+
cause: errorDetails(failure.cause)
|
|
6341
|
+
})),
|
|
6342
|
+
notAttemptedPaths: input.notAttemptedPaths
|
|
6343
|
+
},
|
|
6344
|
+
requestId: firstFailure?.options.requestId,
|
|
6345
|
+
...suggestions.length ? {
|
|
6346
|
+
suggestions
|
|
6347
|
+
} : {},
|
|
6348
|
+
exitCode: firstFailure?.exitCode
|
|
6349
|
+
});
|
|
6350
|
+
}
|
|
6351
|
+
function validateUniqueUploadDestinations(localFiles, options) {
|
|
6352
|
+
const filesByDestination = new Map();
|
|
6353
|
+
for (const localFile of localFiles){
|
|
6354
|
+
const destination = resolveUploadDestination(options.destinationPath, {
|
|
6355
|
+
fileName: path.basename(localFile),
|
|
6356
|
+
keepName: options.keepName
|
|
6357
|
+
});
|
|
6358
|
+
if (!destination.fileName) continue;
|
|
6359
|
+
const remotePath = path.posix.join(destination.path ?? '', destination.fileName);
|
|
6360
|
+
const files = filesByDestination.get(remotePath) ?? [];
|
|
6361
|
+
files.push(localFile);
|
|
6362
|
+
filesByDestination.set(remotePath, files);
|
|
6363
|
+
}
|
|
6364
|
+
const conflicts = [
|
|
6365
|
+
...filesByDestination.entries()
|
|
6366
|
+
].filter(([, files])=>files.length > 1);
|
|
6367
|
+
if (!conflicts.length) return;
|
|
6368
|
+
throw usageError('upload_destination_conflict', [
|
|
6369
|
+
'Multiple files resolve to the same upload destination:',
|
|
6370
|
+
...conflicts.flatMap(([remotePath, files])=>[
|
|
6371
|
+
` ${remotePath}`,
|
|
6372
|
+
...files.map((file)=>` ${file}`)
|
|
6373
|
+
])
|
|
6374
|
+
].join('\n'), [
|
|
6375
|
+
'Rename the local files so each destination is unique.',
|
|
6376
|
+
'Omit --keep-name to let EdgeStore generate unique file names.'
|
|
6377
|
+
]);
|
|
6378
|
+
}
|
|
4851
6379
|
function resolveUploadDestination(destinationPath, input) {
|
|
4852
6380
|
if (!destinationPath) {
|
|
4853
6381
|
return input.keepName ? {
|
|
@@ -4913,11 +6441,6 @@ function interruptedError() {
|
|
|
4913
6441
|
function isFileBlobMutation(error) {
|
|
4914
6442
|
return error instanceof DOMException && error.name === 'NotReadableError';
|
|
4915
6443
|
}
|
|
4916
|
-
function reportProgress(runtime, flags, progress) {
|
|
4917
|
-
if (flags.progress && !flags.json && runtime.io.outputIsTty) {
|
|
4918
|
-
runtime.io.stderr.write(`${progress.fileName}: uploading ${progress.percentage}%\n`);
|
|
4919
|
-
}
|
|
4920
|
-
}
|
|
4921
6444
|
async function fileUploadStatusCommand(runtime, flags, input) {
|
|
4922
6445
|
const sdk = await sdkFor(runtime, flags);
|
|
4923
6446
|
const result = await sdk.management.uploads.get({
|
|
@@ -5068,7 +6591,7 @@ Common workflows:
|
|
|
5068
6591
|
}
|
|
5069
6592
|
outputFor(runtime, globalFlags(program));
|
|
5070
6593
|
});
|
|
5071
|
-
program.command('login').description('Log in to EdgeStore').option('--token', 'read and securely store a management token').action(async (options)=>{
|
|
6594
|
+
program.command('login').description('Log in to EdgeStore').option('--device', 'log in with a device code').option('--token', 'read and securely store a management token').action(async (options)=>{
|
|
5072
6595
|
await loginCommand(runtime, globalFlags(program), options);
|
|
5073
6596
|
});
|
|
5074
6597
|
program.command('logout').description('Remove the stored login').action(async ()=>{
|
|
@@ -5077,9 +6600,52 @@ Common workflows:
|
|
|
5077
6600
|
program.command('whoami').description('Show the current identity and context').action(async ()=>{
|
|
5078
6601
|
await whoamiCommand(runtime, globalFlags(program));
|
|
5079
6602
|
});
|
|
5080
|
-
program.command('doctor').description('Check local configuration and API connectivity').action(async ()=>{
|
|
5081
|
-
await doctorCommand(runtime, globalFlags(program),
|
|
6603
|
+
program.command('doctor').description('Check local configuration and API connectivity').option('--offline', 'inspect locally without credentials, OAuth, or API requests').action(async (options)=>{
|
|
6604
|
+
await doctorCommand(runtime, globalFlags(program), {
|
|
6605
|
+
version,
|
|
6606
|
+
offline: options.offline
|
|
6607
|
+
});
|
|
6608
|
+
});
|
|
6609
|
+
const agent = program.command('agent').description('Manage coding-agent integrations');
|
|
6610
|
+
const assetFile = fileURLToPath(new URL('../agent-assets/skills.json', import.meta.url));
|
|
6611
|
+
agent.command('context').description('Show application versions and local documentation paths').action(async ()=>{
|
|
6612
|
+
await agentContextCommand(runtime, globalFlags(program), {
|
|
6613
|
+
cliVersion: version,
|
|
6614
|
+
assetFile
|
|
6615
|
+
});
|
|
5082
6616
|
});
|
|
6617
|
+
const agentDescriptions = {
|
|
6618
|
+
setup: 'Install the EdgeStore skill and configure MCP',
|
|
6619
|
+
update: 'Update the skill from this CLI and configure MCP',
|
|
6620
|
+
status: 'Show installed skill and MCP configuration'
|
|
6621
|
+
};
|
|
6622
|
+
for (const action of [
|
|
6623
|
+
'setup',
|
|
6624
|
+
'update',
|
|
6625
|
+
'status'
|
|
6626
|
+
]){
|
|
6627
|
+
agent.command(action).description(agentDescriptions[action]).option('--client <client>', 'codex, claude, or cursor').option('--global', 'use user-level skill and MCP configuration').option('--skills-only', 'manage only the skill').option('--dry-run', 'preview changes without writing').option('--yes', 'skip confirmation prompts').action(async (options)=>agentAssetsCommand(runtime, globalFlags(program), {
|
|
6628
|
+
...options,
|
|
6629
|
+
action,
|
|
6630
|
+
assetFile
|
|
6631
|
+
}));
|
|
6632
|
+
}
|
|
6633
|
+
const mcp = program.command('mcp').description('Manage the EdgeStore MCP configuration');
|
|
6634
|
+
const mcpDescriptions = {
|
|
6635
|
+
setup: 'Configure the EdgeStore MCP connection',
|
|
6636
|
+
status: 'Show the saved MCP configuration, not connection status',
|
|
6637
|
+
remove: 'Remove the EdgeStore MCP connection'
|
|
6638
|
+
};
|
|
6639
|
+
for (const action of [
|
|
6640
|
+
'setup',
|
|
6641
|
+
'status',
|
|
6642
|
+
'remove'
|
|
6643
|
+
]){
|
|
6644
|
+
mcp.command(action).description(mcpDescriptions[action]).option('--client <client>', 'codex, claude, or cursor').option('--global', 'use the user-level client configuration').option('--dry-run', 'preview changes without writing').option('--yes', 'skip confirmation prompts').action(async (options)=>mcpCommand(runtime, globalFlags(program), {
|
|
6645
|
+
...options,
|
|
6646
|
+
action
|
|
6647
|
+
}));
|
|
6648
|
+
}
|
|
5083
6649
|
program.command('init').description('Configure EdgeStore for the current project').option('--new', 'create a new project').option('--link <project>', 'link an existing project').option('--name <name>', 'new project name').option('--account <account-id>', 'override the active account').option('--create-key', 'create a new local project key').option('--without-key', 'do not create a project key').option('--output <file>', 'write project keys to an env file').option('--update', 'replace existing values in the output file').option('--bucket <bucket>', 'create a bucket').option('--bucket-type <type>', 'bucket type: file or image').option('--public', 'make the new bucket public').option('--protected', 'make the new bucket protected').option('--install', 'install detected EdgeStore packages').option('--allow-overage', 'allow billable project overage').addHelpText('after', `
|
|
5084
6650
|
Examples:
|
|
5085
6651
|
edgestore init --new --name "Marketing Site" --output .env.local
|