@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/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, access, chmod, readdir, glob } from 'node:fs/promises';
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 { findRoot, NoPkgJsonFound } from '@manypkg/find-root';
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', `Invalid EdgeStore API URL: ${rawValue}`);
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.object({
418
- version: z.literal(1),
419
- clientId: z.string().min(1),
420
- issuer: z.string().url(),
421
- redirectUri: z.string().url()
422
- }).strict();
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 browser login could not be refreshed.', {
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: 1,
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 browser login (${response.status}).`, {
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
- return client && normalizeUrl(client.issuer) === normalizeUrl(issuer.toString()) && isReusableOAuthRedirectUri(client.redirectUri) ? client : undefined;
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 'Browser login failed.';
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 loginCommand(runtime, flags, options) {
1473
- if (!options.token) return await browserLogin(runtime, flags);
1474
- if (flags.json && runtime.io.inputIsTty) {
1475
- throw usageError('interactive_input_disabled', 'Interactive token input is disabled with --json.', [
1476
- 'printf %s "$EDGESTORE_TOKEN" | edgestore login --token --json'
1477
- ]);
1478
- }
1479
- const token = await runtime.prompts.readToken(runtime.io.stdin, runtime.io.inputIsTty);
1480
- const apiUrl = apiUrlFor(runtime, flags);
1481
- const sdk = runtime.sdkFactory({
1482
- token,
1483
- baseUrl: apiUrl.sdkBaseUrl
1484
- });
1485
- const identity = await sdk.management.whoami({
1486
- signal: runtime.signal
1487
- });
1488
- const environmentTokenActive = hasEnvironmentToken(runtime);
1489
- await runtime.credentials.set(apiUrl.displayUrl, token);
1490
- if (!environmentTokenActive) {
1491
- await updateActiveAccountAfterLogin(runtime, flags, identity.actor);
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
- oauthCredential = parseStoredOAuthCredential(stored);
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 browserLogin(runtime, flags) {
2817
+ async function oauthLogin(runtime, flags, mode) {
1637
2818
  const apiUrl = apiUrlFor(runtime, flags);
1638
2819
  const output = outputFor(runtime, flags);
1639
- const result = await runtime.oauth.login({
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.plain && !options.withoutKey) {
1819
- throw usageError('one_time_secret_delivery_required', 'Plain output cannot deliver the initial project key.', [
1820
- 'Use --without-key with --plain.',
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(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 doctorCommand(runtime, flags, version) {
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 (error) {
3951
+ } catch {
2507
3952
  rethrowIfAborted(runtime.signal);
2508
3953
  checks.push({
2509
3954
  name: 'Credential',
2510
- status: 'fail',
2511
- detail: error instanceof Error ? error.message : 'Could not read credential'
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?.token ?? 'edgestore-doctor',
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 (error) {
3981
+ } catch {
2528
3982
  rethrowIfAborted(runtime.signal);
2529
3983
  checks.push({
2530
3984
  name: 'API',
2531
3985
  status: 'fail',
2532
- detail: error instanceof Error ? error.message : 'Health check failed'
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 (credential) {
2536
- let authenticated = false;
4009
+ if (authenticated && activeAccount) {
2537
4010
  try {
2538
- const identity = await sdk.management.whoami({
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: 'Authentication',
4016
+ name: 'Active account',
2545
4017
  status: 'pass',
2546
- detail: `${identity.actor.kind}${scopeDetail}`
4018
+ detail: `${account.account.displayName} (${account.account.id})`
2547
4019
  });
2548
- } catch (error) {
4020
+ } catch {
2549
4021
  rethrowIfAborted(runtime.signal);
2550
4022
  checks.push({
2551
- name: 'Authentication',
4023
+ name: 'Active account',
2552
4024
  status: 'fail',
2553
- detail: error instanceof Error ? error.message : 'Validation failed'
4025
+ detail: 'Account is inaccessible with the current credential.'
2554
4026
  });
2555
4027
  }
2556
- if (authenticated && activeAccount) {
2557
- try {
2558
- const account = await sdk.management.accounts.get({
2559
- account: activeAccount,
2560
- signal: runtime.signal
2561
- });
2562
- checks.push({
2563
- name: 'Active account',
2564
- status: 'pass',
2565
- detail: `${account.account.displayName} (${account.account.id})`
2566
- });
2567
- } catch (error) {
2568
- rethrowIfAborted(runtime.signal);
2569
- checks.push({
2570
- name: 'Active account',
2571
- status: 'fail',
2572
- detail: error instanceof Error ? error.message : 'Account is inaccessible'
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
- if (authenticated && localConfig && linkedApiOrigin !== apiUrl.displayUrl) {
4048
+ } catch {
4049
+ rethrowIfAborted(runtime.signal);
2583
4050
  checks.push({
2584
4051
  name: 'Linked project',
2585
4052
  status: 'fail',
2586
- detail: `Linked to ${linkedApiOrigin}, current API is ${apiUrl.displayUrl}`
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 (error) {
4084
+ } catch {
2630
4085
  rethrowIfAborted(runtime.signal);
2631
4086
  checks.push({
2632
4087
  name: 'Global config',
2633
4088
  status: 'fail',
2634
- detail: error instanceof Error ? error.message : 'Could not read config'
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 (error) {
4106
+ } catch {
2652
4107
  rethrowIfAborted(runtime.signal);
2653
4108
  checks.push({
2654
4109
  name: 'Local config',
2655
4110
  status: 'fail',
2656
- detail: error instanceof Error ? error.message : 'Could not read local config'
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 label = localConfig?.config.envFile ?? '.env.local';
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 relativeCwd = path.relative(invocationCwd, packageCwd) || '.';
3091
- if (relativeCwd === '.') return renderShellCommand([
3092
- manager,
3093
- ...args
3094
- ]);
3095
- if (manager === 'npm') {
3096
- return renderShellCommand([
3097
- manager,
3098
- '--prefix',
3099
- relativeCwd,
3100
- ...args
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 packageNextSteps(framework) {
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
- 'Next steps:',
3135
- ' Configure an EdgeStore router and server endpoint.'
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 createOutputCapture(limit = 32_768) {
3139
- let output = '';
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
- stream: new Writable({
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 isMissingPathError(error) {
3151
- return error instanceof Error && 'code' in error && (error.code === 'ENOENT' || error.code === 'ENOTDIR');
3152
- }
3153
- async function detectPackageManager(cwd) {
3154
- const start = path.resolve(cwd);
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
- if (!interactive) return '.env.local';
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
- requirePlainSecretDelivery(flags, input);
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(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
- requirePlainSecretDelivery(flags, input);
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: result,
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 requirePlainSecretDelivery(flags, options) {
4595
- if (flags.plain && !options.copy && !options.output) {
4596
- throw usageError('secret_delivery_required', 'Plain output requires --copy or --output for the one-time key.');
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.plain && !options.copy && !options.output) {
4670
- throw usageError('secret_delivery_required', 'Plain output requires --copy or --output for the one-time token.');
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(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 results = [];
4778
- for (const [index, localFile] of localFiles.entries()){
4779
- try {
4780
- const fileName = path.basename(localFile);
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
- const completed = await sdk.management.uploads.upload({
4791
- project,
4792
- bucket: input.bucket,
4793
- source,
4794
- signedReadUrl: {},
4795
- ...destination.fileName ? {
4796
- fileName: destination.fileName
4797
- } : {},
4798
- ...destination.path ? {
4799
- path: destination.path
4800
- } : {},
4801
- mimeType,
4802
- signal: runtime.signal,
4803
- onProgress: (progress)=>reportProgress(runtime, flags, {
4804
- fileName,
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
- throw normalizeUploadError(error, {
4814
- runtime,
4815
- flags,
4816
- project
4817
- });
6222
+ return {
6223
+ status: 'failed',
6224
+ localPath: localFile,
6225
+ cause: normalizeError(error)
6226
+ };
4818
6227
  }
4819
- } catch (error) {
4820
- if (localFiles.length === 1) throw error;
4821
- const cause = normalizeError(error);
4822
- const notAttemptedPaths = localFiles.slice(index + 1);
4823
- throw new CliError('file_upload_incomplete', [
4824
- `Upload batch stopped while processing ${localFile}: ${cause.message}`,
4825
- `Completed (${results.length}):`,
4826
- ...results.map((result)=>` ${result.localPath} -> ${result.signedReadUrl?.signedUrl ?? result.file.url} (${result.file.id})`),
4827
- `Interrupted: ${localFile}`,
4828
- `Not attempted (${notAttemptedPaths.length}):`,
4829
- ...notAttemptedPaths.map((file)=>` ${file}`)
4830
- ].join('\n'), {
4831
- details: {
4832
- completed: results,
4833
- interruptedPath: localFile,
4834
- notAttemptedPaths,
4835
- cause: errorDetails(cause)
4836
- },
4837
- requestId: cause.options.requestId,
4838
- suggestions: cause.options.suggestions,
4839
- exitCode: cause.exitCode
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), version);
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