@edgestore/cli 1.0.0-next.3 → 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.');
@@ -1181,7 +1188,8 @@ function createDefaultRuntime(signal) {
1181
1188
  stdout: process.stdout,
1182
1189
  stderr: process.stderr,
1183
1190
  inputIsTty: Boolean(process.stdin.isTTY),
1184
- outputIsTty: Boolean(process.stdout.isTTY)
1191
+ outputIsTty: Boolean(process.stdout.isTTY),
1192
+ stderrIsTty: Boolean(process.stderr.isTTY)
1185
1193
  },
1186
1194
  signal,
1187
1195
  setCwd (cwd) {
@@ -1465,6 +1473,545 @@ function missingAccountError() {
1465
1473
  ]);
1466
1474
  }
1467
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
+
1468
2015
  async function resolveWorkingDirectory(current, requested) {
1469
2016
  const resolved = path.resolve(current, requested);
1470
2017
  try {
@@ -1580,104 +2127,616 @@ async function exists(candidate) {
1580
2127
  }
1581
2128
  }
1582
2129
 
1583
- async function loginCommand(runtime, flags, options) {
1584
- if (options.device && options.token) {
1585
- throw usageError('conflicting_login_modes', '--device and --token cannot be used together.');
1586
- }
1587
- const machineOutputFlag = flags.json ? '--json' : flags.plain ? '--plain' : undefined;
1588
- if (!options.token && machineOutputFlag) {
1589
- throw usageError('interactive_input_disabled', `OAuth login is interactive and cannot be used with ${machineOutputFlag}.`, [
1590
- 'edgestore login',
1591
- 'edgestore login --device'
1592
- ]);
1593
- }
1594
- if (options.device) return await oauthLogin(runtime, flags, 'device');
1595
- if (!options.token) return await oauthLogin(runtime, flags, 'browser');
1596
- if (machineOutputFlag && runtime.io.inputIsTty) {
1597
- throw usageError('interactive_input_disabled', `Interactive token input is disabled with ${machineOutputFlag}.`, [
1598
- `printf %s "$EDGESTORE_TOKEN" | edgestore login --token ${machineOutputFlag}`
1599
- ]);
1600
- }
1601
- const token = await runtime.prompts.readToken(runtime.io.stdin, runtime.io.inputIsTty);
1602
- const apiUrl = apiUrlFor(runtime, flags);
1603
- const sdk = runtime.sdkFactory({
1604
- token,
1605
- baseUrl: apiUrl.sdkBaseUrl
1606
- });
1607
- const identity = await sdk.management.whoami({
1608
- signal: runtime.signal
1609
- });
1610
- const environmentTokenActive = hasEnvironmentToken(runtime);
1611
- await runtime.credentials.set(apiUrl.displayUrl, token);
1612
- if (!environmentTokenActive) {
1613
- await updateActiveAccountAfterLogin(runtime, flags, identity.actor);
1614
- }
1615
- const output = outputFor(runtime, flags);
1616
- output.result(loginResult(identity.actor, environmentTokenActive), `Logged in as ${actorLabel(identity.actor)}.`, actorLabel(identity.actor));
1617
- warnWhenStoredLoginIsInactive(output, environmentTokenActive);
1618
- }
1619
- async function logoutCommand(runtime, flags) {
1620
- const apiOrigin = apiUrlFor(runtime, flags).displayUrl;
1621
- const output = outputFor(runtime, flags);
1622
- const stored = await runtime.credentials.get(apiOrigin);
1623
- let oauthCredential;
1624
- try {
1625
- oauthCredential = parseStoredOAuthCredential(stored);
1626
- } catch {
1627
- output.warning('The stored OAuth login is invalid. Skipping remote revocation.');
1628
- }
1629
- let oauthRevoked;
1630
- let revocationFailure;
1631
- if (oauthCredential) {
1632
- try {
1633
- await runtime.oauth.revoke(oauthCredential, runtime.signal);
1634
- oauthRevoked = true;
1635
- } catch (error) {
1636
- if (runtime.signal.aborted) throw error;
1637
- oauthRevoked = false;
1638
- revocationFailure = normalizeError(error);
1639
- output.warning('The OAuth grant could not be revoked remotely. The login will still be removed locally.');
1640
- }
1641
- }
1642
- let deleted;
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;
1643
2136
  try {
1644
- deleted = await runtime.credentials.delete(apiOrigin);
2137
+ contents = await readFile(packagePath, 'utf8');
1645
2138
  } catch (error) {
1646
- const deletionFailure = normalizeError(error);
1647
- if (oauthRevoked) {
1648
- throw new CliError('logout_partial_failure', 'The OAuth grant was revoked, but the stored login could not be removed.', {
2139
+ if (!isMissingPathError(error)) {
2140
+ throw new CliError('package_manifest_unreadable', `Could not read package manifest at ${packagePath}.`, {
1649
2141
  details: {
1650
- status: 'partial',
1651
- oauthGrant: {
1652
- status: 'revoked'
1653
- },
1654
- localCredential: {
1655
- status: 'delete_failed',
1656
- cause: errorDetails$2(deletionFailure)
1657
- }
1658
- },
1659
- requestId: deletionFailure.options.requestId,
1660
- suggestions: [
1661
- renderCliCommand(flags, [
1662
- 'logout'
1663
- ])
1664
- ],
1665
- exitCode: deletionFailure.exitCode
2142
+ path: packagePath,
2143
+ cause: error instanceof Error ? error.message : String(error)
2144
+ }
1666
2145
  });
1667
2146
  }
1668
- if (revocationFailure) {
1669
- throw new CliError('logout_failed', 'The OAuth grant could not be revoked and the stored login could not be removed.', {
1670
- details: {
1671
- status: 'failed',
1672
- oauthGrant: {
1673
- status: 'revocation_failed',
1674
- cause: errorDetails$2(revocationFailure)
1675
- },
1676
- localCredential: {
1677
- status: 'delete_failed',
1678
- cause: errorDetails$2(deletionFailure)
1679
- }
1680
- },
2147
+ return {
2148
+ framework: 'unknown',
2149
+ manager: await detectPackageManager(cwd),
2150
+ missing: []
2151
+ };
2152
+ }
2153
+ try {
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);
2685
+ } catch {
2686
+ output.warning('The stored OAuth login is invalid. Skipping remote revocation.');
2687
+ }
2688
+ let oauthRevoked;
2689
+ let revocationFailure;
2690
+ if (oauthCredential) {
2691
+ try {
2692
+ await runtime.oauth.revoke(oauthCredential, runtime.signal);
2693
+ oauthRevoked = true;
2694
+ } catch (error) {
2695
+ if (runtime.signal.aborted) throw error;
2696
+ oauthRevoked = false;
2697
+ revocationFailure = normalizeError(error);
2698
+ output.warning('The OAuth grant could not be revoked remotely. The login will still be removed locally.');
2699
+ }
2700
+ }
2701
+ let deleted;
2702
+ try {
2703
+ deleted = await runtime.credentials.delete(apiOrigin);
2704
+ } catch (error) {
2705
+ const deletionFailure = normalizeError(error);
2706
+ if (oauthRevoked) {
2707
+ throw new CliError('logout_partial_failure', 'The OAuth grant was revoked, but the stored login could not be removed.', {
2708
+ details: {
2709
+ status: 'partial',
2710
+ oauthGrant: {
2711
+ status: 'revoked'
2712
+ },
2713
+ localCredential: {
2714
+ status: 'delete_failed',
2715
+ cause: errorDetails$2(deletionFailure)
2716
+ }
2717
+ },
2718
+ requestId: deletionFailure.options.requestId,
2719
+ suggestions: [
2720
+ renderCliCommand(flags, [
2721
+ 'logout'
2722
+ ])
2723
+ ],
2724
+ exitCode: deletionFailure.exitCode
2725
+ });
2726
+ }
2727
+ if (revocationFailure) {
2728
+ throw new CliError('logout_failed', 'The OAuth grant could not be revoked and the stored login could not be removed.', {
2729
+ details: {
2730
+ status: 'failed',
2731
+ oauthGrant: {
2732
+ status: 'revocation_failed',
2733
+ cause: errorDetails$2(revocationFailure)
2734
+ },
2735
+ localCredential: {
2736
+ status: 'delete_failed',
2737
+ cause: errorDetails$2(deletionFailure)
2738
+ }
2739
+ },
1681
2740
  requestId: deletionFailure.options.requestId ?? revocationFailure.options.requestId,
1682
2741
  suggestions: [
1683
2742
  renderCliCommand(flags, [
@@ -1947,10 +3006,9 @@ async function projectShowCommand(runtime, flags, projectRef) {
1947
3006
  ].join('\n'), project.basePath);
1948
3007
  }
1949
3008
  async function projectCreateCommand(runtime, flags, options) {
1950
- if (flags.plain && !options.withoutKey) {
1951
- throw usageError('one_time_secret_delivery_required', 'Plain output cannot deliver the initial project key.', [
1952
- 'Use --without-key with --plain.',
1953
- '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.'
1954
3012
  ]);
1955
3013
  }
1956
3014
  const account = await activeAccount(runtime, flags, options.account);
@@ -1962,14 +3020,16 @@ async function projectCreateCommand(runtime, flags, options) {
1962
3020
  allowOverage: Boolean(options.allowOverage),
1963
3021
  signal: runtime.signal
1964
3022
  });
1965
- const keyLines = result.projectKey ? [
3023
+ const keyLines = isInteractive(runtime, flags) && result.projectKey ? [
1966
3024
  '',
1967
3025
  `EDGE_STORE_ACCESS_KEY=${result.projectKey.key.accessKey}`,
1968
3026
  `EDGE_STORE_SECRET_KEY=${result.projectKey.secretKey}`,
1969
3027
  '',
1970
3028
  'Save this secret now. You will not be able to view it again.'
1971
3029
  ] : [];
1972
- outputFor(runtime, flags).result(result, [
3030
+ outputFor(runtime, flags).result({
3031
+ project: result.project
3032
+ }, [
1973
3033
  `Created project "${result.project.name}" (${result.project.basePath}).`,
1974
3034
  ...keyLines,
1975
3035
  '',
@@ -2602,21 +3662,276 @@ function findClosingQuote(value, quote) {
2602
3662
  return -1;
2603
3663
  }
2604
3664
 
2605
- 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) {
2606
3911
  await selectWorkspaceContext(runtime, flags, 'read');
2607
3912
  const checks = [
2608
3913
  {
2609
3914
  name: 'CLI',
2610
3915
  status: 'pass',
2611
- detail: version
3916
+ detail: options.version
2612
3917
  }
2613
3918
  ];
3919
+ checks.push(...await localApplicationChecks(runtime.workspaceCwd));
2614
3920
  const apiUrl = apiUrlFor(runtime, flags);
2615
3921
  const globalConfig = await checkGlobalConfig(runtime, checks);
2616
3922
  const activeAccount = activeAccountFor(globalConfig, apiUrl.displayUrl);
2617
3923
  const localConfig = await checkLocalConfig(runtime, checks);
2618
- const linkedApiOrigin = localConfig ? apiOriginForRepoConfig(localConfig.config) : undefined;
3924
+ const linkedApiOrigin = localConfig ? resolveApiUrl(apiOriginForRepoConfig(localConfig.config), undefined).displayUrl : undefined;
2619
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
+ }
2620
3935
  const keychainAvailable = await runtime.credentials.available();
2621
3936
  checks.push({
2622
3937
  name: 'Credential store',
@@ -2626,25 +3941,32 @@ async function doctorCommand(runtime, flags, version) {
2626
3941
  let credential;
2627
3942
  try {
2628
3943
  credential = await resolveCredential(runtime.env.EDGESTORE_TOKEN, runtime.credentials, {
2629
- apiOrigin: apiUrl.displayUrl,
2630
- oauth: runtime.oauth,
2631
- signal: runtime.signal
3944
+ apiOrigin: apiUrl.displayUrl
2632
3945
  });
2633
3946
  checks.push({
2634
3947
  name: 'Credential',
2635
3948
  status: credential ? 'pass' : 'warn',
2636
3949
  detail: credential ? credential.source : 'Not configured'
2637
3950
  });
2638
- } catch (error) {
3951
+ } catch {
2639
3952
  rethrowIfAborted(runtime.signal);
2640
3953
  checks.push({
2641
3954
  name: 'Credential',
2642
- status: 'fail',
2643
- 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.'
2644
3964
  });
3965
+ report(runtime, flags, checks);
3966
+ return;
2645
3967
  }
2646
3968
  const sdk = runtime.sdkFactory({
2647
- token: credential?.token ?? 'edgestore-doctor',
3969
+ token: credential.token,
2648
3970
  baseUrl: apiUrl.sdkBaseUrl
2649
3971
  });
2650
3972
  try {
@@ -2656,84 +3978,85 @@ async function doctorCommand(runtime, flags, version) {
2656
3978
  status: 'pass',
2657
3979
  detail: apiUrl.displayUrl
2658
3980
  });
2659
- } catch (error) {
3981
+ } catch {
2660
3982
  rethrowIfAborted(runtime.signal);
2661
3983
  checks.push({
2662
3984
  name: 'API',
2663
3985
  status: 'fail',
2664
- 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.'
2665
4007
  });
2666
4008
  }
2667
- if (credential) {
2668
- let authenticated = false;
4009
+ if (authenticated && activeAccount) {
2669
4010
  try {
2670
- const identity = await sdk.management.whoami({
4011
+ const account = await sdk.management.accounts.get({
4012
+ account: activeAccount,
2671
4013
  signal: runtime.signal
2672
4014
  });
2673
- authenticated = true;
2674
- const scopeDetail = 'scopes' in identity.actor ? ` (${identity.actor.scopes.join(', ')})` : '';
2675
4015
  checks.push({
2676
- name: 'Authentication',
4016
+ name: 'Active account',
2677
4017
  status: 'pass',
2678
- detail: `${identity.actor.kind}${scopeDetail}`
4018
+ detail: `${account.account.displayName} (${account.account.id})`
2679
4019
  });
2680
- } catch (error) {
4020
+ } catch {
2681
4021
  rethrowIfAborted(runtime.signal);
2682
4022
  checks.push({
2683
- name: 'Authentication',
4023
+ name: 'Active account',
2684
4024
  status: 'fail',
2685
- detail: error instanceof Error ? error.message : 'Validation failed'
4025
+ detail: 'Account is inaccessible with the current credential.'
2686
4026
  });
2687
4027
  }
2688
- if (authenticated && activeAccount) {
2689
- try {
2690
- const account = await sdk.management.accounts.get({
2691
- account: activeAccount,
2692
- signal: runtime.signal
2693
- });
2694
- checks.push({
2695
- name: 'Active account',
2696
- status: 'pass',
2697
- detail: `${account.account.displayName} (${account.account.id})`
2698
- });
2699
- } catch (error) {
2700
- rethrowIfAborted(runtime.signal);
2701
- checks.push({
2702
- name: 'Active account',
2703
- status: 'fail',
2704
- detail: error instanceof Error ? error.message : 'Account is inaccessible'
2705
- });
2706
- }
2707
- } else if (authenticated) {
2708
- checks.push({
2709
- name: 'Active account',
2710
- status: 'warn',
2711
- 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
2712
4047
  });
2713
- }
2714
- if (authenticated && localConfig && linkedApiOrigin !== apiUrl.displayUrl) {
4048
+ } catch {
4049
+ rethrowIfAborted(runtime.signal);
2715
4050
  checks.push({
2716
4051
  name: 'Linked project',
2717
4052
  status: 'fail',
2718
- detail: `Linked to ${linkedApiOrigin}, current API is ${apiUrl.displayUrl}`
4053
+ detail: 'Project is inaccessible with the current credential.'
2719
4054
  });
2720
- } else if (authenticated && localConfig) {
2721
- try {
2722
- await checkLinkedProject(runtime, sdk, {
2723
- local: localConfig,
2724
- envKeys,
2725
- checks
2726
- });
2727
- } catch (error) {
2728
- rethrowIfAborted(runtime.signal);
2729
- checks.push({
2730
- name: 'Linked project',
2731
- status: 'fail',
2732
- detail: error instanceof Error ? error.message : 'Project is inaccessible'
2733
- });
2734
- }
2735
4055
  }
2736
4056
  }
4057
+ report(runtime, flags, checks);
4058
+ }
4059
+ function report(runtime, flags, checks) {
2737
4060
  if (checks.some((check)=>check.status === 'fail')) {
2738
4061
  runtime.exitCode = 1;
2739
4062
  }
@@ -2758,12 +4081,12 @@ async function checkGlobalConfig(runtime, checks) {
2758
4081
  detail: runtime.globalConfig.path
2759
4082
  });
2760
4083
  return config;
2761
- } catch (error) {
4084
+ } catch {
2762
4085
  rethrowIfAborted(runtime.signal);
2763
4086
  checks.push({
2764
4087
  name: 'Global config',
2765
4088
  status: 'fail',
2766
- detail: error instanceof Error ? error.message : 'Could not read config'
4089
+ detail: 'Could not read global config; inspect its format locally.'
2767
4090
  });
2768
4091
  return {
2769
4092
  version: 2,
@@ -2780,22 +4103,32 @@ async function checkLocalConfig(runtime, checks) {
2780
4103
  detail: located?.path ?? 'No linked project'
2781
4104
  });
2782
4105
  return located;
2783
- } catch (error) {
4106
+ } catch {
2784
4107
  rethrowIfAborted(runtime.signal);
2785
4108
  checks.push({
2786
4109
  name: 'Local config',
2787
4110
  status: 'fail',
2788
- detail: error instanceof Error ? error.message : 'Could not read local config'
4111
+ detail: 'Could not read local config; inspect its format locally.'
2789
4112
  });
2790
4113
  return undefined;
2791
4114
  }
2792
4115
  }
2793
4116
  async function checkEnvFile(runtime, localConfig, checks) {
2794
- 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
+ }
2795
4126
  const configRoot = localConfig?.config.envFile ? path.dirname(path.dirname(localConfig.path)) : runtime.workspaceCwd;
2796
4127
  const envPath = path.resolve(configRoot, label);
2797
4128
  let contents = '';
2798
4129
  try {
4130
+ await validatePath(envPath, configRoot);
4131
+ if ((await stat(envPath)).size > 128_000) throw new Error();
2799
4132
  contents = await readFile(envPath, 'utf8');
2800
4133
  } catch (error) {
2801
4134
  rethrowIfAborted(runtime.signal);
@@ -2807,10 +4140,11 @@ async function checkEnvFile(runtime, localConfig, checks) {
2807
4140
  detail: 'Configured env file not found'
2808
4141
  });
2809
4142
  } else if (code !== 'ENOENT') {
4143
+ const unsafePath = error instanceof CliError && error.code === 'unsafe_agent_path';
2810
4144
  checks.push({
2811
4145
  name: label,
2812
- status: 'fail',
2813
- 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'
2814
4148
  });
2815
4149
  }
2816
4150
  return {
@@ -3024,313 +4358,58 @@ async function fileDeleteCommand(runtime, flags, input) {
3024
4358
  notAttemptedReferences,
3025
4359
  cause: {
3026
4360
  code: cause.code,
3027
- message: cause.message,
3028
- ...cause.options.details === undefined ? {} : {
3029
- details: cause.options.details
3030
- }
3031
- }
3032
- },
3033
- requestId: cause.options.requestId,
3034
- exitCode: cause.exitCode
3035
- });
3036
- }
3037
- }
3038
- const successCount = results.filter((result)=>result.success).length;
3039
- const failureCount = results.length - successCount;
3040
- const human = [
3041
- `Deleted ${successCount} file(s); ${failureCount} failed.`,
3042
- ...results.filter((result)=>!result.success).map((result)=>` ${fileReferenceLabel(result.fileRef)}: ${result.error.message}`)
3043
- ].join('\n');
3044
- outputFor(runtime, flags).result({
3045
- results,
3046
- successCount,
3047
- failureCount
3048
- }, human);
3049
- if (failureCount) runtime.exitCode = 1;
3050
- }
3051
- function renderIncompleteDeletion(input) {
3052
- return [
3053
- `File deletion stopped: ${input.cause.message}`,
3054
- `Completed: ${input.successCount} succeeded, ${input.failureCount} failed.`,
3055
- ...input.results.map((result)=>` ${JSON.stringify(result.fileRef)}: ${result.success ? 'deleted' : `failed: ${result.error?.message ?? 'unknown failure'}`}`),
3056
- `Uncertain (${input.uncertainReferences.length}):`,
3057
- ...input.uncertainReferences.map((reference)=>` ${reference}`),
3058
- `Not attempted (${input.notAttemptedReferences.length}):`,
3059
- ...input.notAttemptedReferences.map((reference)=>` ${reference}`)
3060
- ].join('\n');
3061
- }
3062
- function fileReference(value, bucket) {
3063
- if (bucket) return {
3064
- bucketName: bucket,
3065
- path: value
3066
- };
3067
- if (/^https?:\/\//i.test(value)) return {
3068
- url: value
3069
- };
3070
- return {
3071
- id: value
3072
- };
3073
- }
3074
- function fileReferenceLabel(reference) {
3075
- if ('id' in reference) return reference.id;
3076
- if ('key' in reference) return reference.key;
3077
- if ('url' in reference) return reference.url;
3078
- return `${reference.bucketName}/${reference.path}`;
3079
- }
3080
-
3081
- async function detectPackages(cwd, options = {}) {
3082
- const resolvedCwd = path.resolve(cwd);
3083
- const workspaceRoot = await findWorkspaceRoot(resolvedCwd);
3084
- const packagePath = path.join(cwd, 'package.json');
3085
- let manifest;
3086
- let contents;
3087
- try {
3088
- contents = await readFile(packagePath, 'utf8');
3089
- } catch (error) {
3090
- if (!isMissingPathError(error)) {
3091
- throw new CliError('package_manifest_unreadable', `Could not read package manifest at ${packagePath}.`, {
3092
- details: {
3093
- path: packagePath,
3094
- cause: error instanceof Error ? error.message : String(error)
3095
- }
3096
- });
3097
- }
3098
- return {
3099
- framework: 'unknown',
3100
- manager: await detectPackageManager(cwd),
3101
- missing: []
3102
- };
3103
- }
3104
- try {
3105
- manifest = JSON.parse(contents);
3106
- } catch (error) {
3107
- throw new CliError('invalid_package_manifest', `Invalid package manifest at ${packagePath}.`, {
3108
- details: {
3109
- path: packagePath,
3110
- cause: error instanceof Error ? error.message : String(error)
3111
- },
3112
- exitCode: 2
3113
- });
3114
- }
3115
- const dependencies = {
3116
- ...manifest.dependencies,
3117
- ...manifest.devDependencies
3118
- };
3119
- const framework = dependencies.next ? 'next' : dependencies.react ? 'react' : 'node';
3120
- const wanted = framework === 'next' || framework === 'react' ? [
3121
- '@edgestore/server',
3122
- '@edgestore/react'
3123
- ] : [
3124
- '@edgestore/server'
3125
- ];
3126
- return {
3127
- framework,
3128
- manager: await detectPackageManager(cwd),
3129
- missing: wanted.filter((name)=>!dependencies[name]),
3130
- installAtWorkspaceRoot: options.installAtWorkspaceRoot,
3131
- ...workspaceRoot && workspaceRoot !== resolvedCwd ? {
3132
- workspace: {
3133
- root: workspaceRoot,
3134
- ...manifest.name ? {
3135
- packageName: manifest.name
3136
- } : {}
3137
- }
3138
- } : {}
3139
- };
3140
- }
3141
- async function installPackages(runtime, plan, options) {
3142
- if (!plan.manager || !plan.missing.length) return {
3143
- ran: false
3144
- };
3145
- const args = installArgs(plan.manager, plan.missing, plan.installAtWorkspaceRoot);
3146
- const command = renderInstallCommand(plan.manager, args, {
3147
- plan,
3148
- packageCwd: options.cwd,
3149
- invocationCwd: options.invocationCwd
3150
- });
3151
- const shouldInstall = options.requested ?? (options.interactive ? await runtime.prompts.confirm(`Install EdgeStore packages with ${plan.manager}?`, true) : false);
3152
- if (!shouldInstall) return {
3153
- command,
3154
- cwd: options.cwd,
3155
- ran: false
3156
- };
3157
- const captured = options.structured ? createOutputCapture() : undefined;
3158
- try {
3159
- await runtime.runCommand(plan.manager, args, {
3160
- cwd: options.cwd,
3161
- ...captured ? {
3162
- stdout: captured.stream,
3163
- stderr: captured.stream
3164
- } : {}
3165
- });
3166
- } catch (error) {
3167
- const diagnostics = captured?.read().trim();
3168
- if (diagnostics && !options.json) {
3169
- runtime.io.stderr.write(`${diagnostics}\n`);
3170
- }
3171
- throw new CliError('package_install_failed', error instanceof Error ? error.message : 'Package installation failed.', {
3172
- ...diagnostics ? {
3173
- details: {
3174
- diagnostics
3175
- }
3176
- } : {},
3177
- suggestions: [
3178
- command
3179
- ]
3180
- });
3181
- }
3182
- const diagnostics = captured?.read();
3183
- if (diagnostics) {
3184
- runtime.io.stderr.write(diagnostics.endsWith('\n') ? diagnostics : `${diagnostics}\n`);
3185
- }
3186
- return {
3187
- command,
3188
- cwd: options.cwd,
3189
- ran: true
3190
- };
3191
- }
3192
- function renderInstallCommand(manager, args, context) {
3193
- const { plan, packageCwd, invocationCwd } = context;
3194
- const workspace = plan.workspace;
3195
- if (invocationCwd === workspace?.root) {
3196
- const workspacePath = path.relative(workspace.root, packageCwd).replaceAll(path.sep, '/');
3197
- if (manager === 'pnpm') {
3198
- return renderShellCommand([
3199
- manager,
3200
- '--filter',
3201
- `./${workspacePath}`,
3202
- ...args
3203
- ]);
3204
- }
3205
- if (manager === 'npm') {
3206
- return renderShellCommand([
3207
- manager,
3208
- ...args,
3209
- '--workspace',
3210
- workspacePath
3211
- ]);
3212
- }
3213
- if (manager === 'yarn' && workspace.packageName) {
3214
- return renderShellCommand([
3215
- manager,
3216
- 'workspace',
3217
- workspace.packageName,
3218
- ...args
3219
- ]);
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
+ });
3220
4370
  }
3221
4371
  }
3222
- const relativeCwd = path.relative(invocationCwd, packageCwd) || '.';
3223
- if (relativeCwd === '.') return renderShellCommand([
3224
- manager,
3225
- ...args
3226
- ]);
3227
- if (manager === 'npm') {
3228
- return renderShellCommand([
3229
- manager,
3230
- '--prefix',
3231
- relativeCwd,
3232
- ...args
3233
- ]);
3234
- }
3235
- if (manager === 'pnpm') {
3236
- return renderShellCommand([
3237
- manager,
3238
- '--dir',
3239
- relativeCwd,
3240
- ...args
3241
- ]);
3242
- }
3243
- return renderShellCommand([
3244
- manager,
3245
- '--cwd',
3246
- relativeCwd,
3247
- ...args
3248
- ]);
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;
3249
4384
  }
3250
- function packageNextSteps(framework) {
3251
- if (framework === 'next') {
3252
- return [
3253
- 'Next steps:',
3254
- ' Configure an EdgeStore router in your Next.js app.',
3255
- ' Add the EdgeStore provider to your client layout.'
3256
- ];
3257
- }
3258
- if (framework === 'react') {
3259
- return [
3260
- 'Next steps:',
3261
- ' Configure an EdgeStore server endpoint.',
3262
- ' Add the EdgeStore provider to your React app.'
3263
- ];
3264
- }
4385
+ function renderIncompleteDeletion(input) {
3265
4386
  return [
3266
- 'Next steps:',
3267
- ' Configure an EdgeStore router and server endpoint.'
3268
- ];
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');
3269
4395
  }
3270
- function createOutputCapture(limit = 32_768) {
3271
- 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
+ };
3272
4404
  return {
3273
- stream: new Writable({
3274
- write (chunk, _encoding, callback) {
3275
- output = `${output}${Buffer.from(chunk).toString('utf8')}`.slice(-limit);
3276
- callback();
3277
- }
3278
- }),
3279
- read: ()=>output
4405
+ id: value
3280
4406
  };
3281
4407
  }
3282
- function isMissingPathError(error) {
3283
- return error instanceof Error && 'code' in error && (error.code === 'ENOENT' || error.code === 'ENOTDIR');
3284
- }
3285
- async function detectPackageManager(cwd) {
3286
- const start = path.resolve(cwd);
3287
- const boundary = await findWorkspaceRoot(start) ?? await findGitRoot(start) ?? start;
3288
- let directory = start;
3289
- while(true){
3290
- for (const strategy of [
3291
- 'packageManager-field',
3292
- 'lockfile',
3293
- 'devEngines-field'
3294
- ]){
3295
- const result = await detect({
3296
- cwd: directory,
3297
- stopDir: directory,
3298
- strategies: [
3299
- strategy
3300
- ],
3301
- packageJsonParser: (contents)=>packageJsonForStrategy(contents, strategy)
3302
- });
3303
- if (result?.name === 'pnpm' || result?.name === 'npm' || result?.name === 'yarn' || result?.name === 'bun') {
3304
- return result.name;
3305
- }
3306
- }
3307
- if (directory === boundary) break;
3308
- directory = path.dirname(directory);
3309
- }
3310
- return 'npm';
3311
- }
3312
- function packageJsonForStrategy(contents, strategy) {
3313
- const manifest = JSON.parse(contents);
3314
- if (strategy !== 'packageManager-field') delete manifest.packageManager;
3315
- if (strategy !== 'devEngines-field') delete manifest.devEngines;
3316
- return manifest;
3317
- }
3318
- function installArgs(manager, packages, installAtWorkspaceRoot = false) {
3319
- if (manager === 'npm') return [
3320
- 'install',
3321
- ...packages
3322
- ];
3323
- if (manager === 'pnpm' && installAtWorkspaceRoot) {
3324
- return [
3325
- 'add',
3326
- '-w',
3327
- ...packages
3328
- ];
3329
- }
3330
- return [
3331
- 'add',
3332
- ...packages
3333
- ];
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}`;
3334
4413
  }
3335
4414
 
3336
4415
  async function deliverEnvSecretWithRollback(input) {
@@ -3579,13 +4658,25 @@ function errorDetails$1(error) {
3579
4658
  const execFileAsync = promisify(execFile);
3580
4659
  async function resolveSecretOutput(runtime, explicitOutput, interactive) {
3581
4660
  if (explicitOutput) return explicitOutput;
3582
- if (!interactive) return '.env.local';
4661
+ const linked = await runtime.repoConfig.read();
4662
+ if (linked?.config.envFile) return linked.config.envFile;
3583
4663
  const discovered = (await readdir(runtime.workspaceCwd, {
3584
4664
  withFileTypes: true
3585
4665
  })).filter((entry)=>entry.isFile() && entry.name.startsWith('.env') && !/\.(?:example|sample|template)$/i.test(entry.name)).map((entry)=>entry.name).sort();
3586
4666
  if (!discovered.length || discovered.every((name)=>name === '.env.local')) {
3587
4667
  return '.env.local';
3588
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
+ }
3589
4680
  const choices = Array.from(new Set([
3590
4681
  ...discovered,
3591
4682
  '.env.local'
@@ -3606,6 +4697,13 @@ async function protectSecretFile(cwd, output) {
3606
4697
  const boundary = gitRoot ?? packageRoot;
3607
4698
  const repositoryRelative = path.relative(boundary, absolute);
3608
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);
3609
4707
  if (!packageRelative || packageRelative.startsWith('..') || path.isAbsolute(packageRelative)) {
3610
4708
  throw usageError('secret_output_unprotected', `Secret output ${absolute} must be inside ${packageRoot}.`, [
3611
4709
  'Choose an env file inside the selected package.'
@@ -3622,6 +4720,7 @@ async function protectSecretFile(cwd, output) {
3622
4720
  if (await isIgnoredFile(gitRoot, gitPath)) return normalized;
3623
4721
  }
3624
4722
  const gitignorePath = path.join(packageRoot, '.gitignore');
4723
+ await rejectSymlink(gitignorePath);
3625
4724
  let contents = '';
3626
4725
  try {
3627
4726
  contents = await readFile(gitignorePath, 'utf8');
@@ -3645,6 +4744,13 @@ async function protectSecretFile(cwd, output) {
3645
4744
  }
3646
4745
  return normalized;
3647
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
+ }
3648
4754
  async function isIgnoredFile(root, relative) {
3649
4755
  try {
3650
4756
  await execFileAsync('git', [
@@ -4175,6 +5281,42 @@ function requireInteractive(interactive, message) {
4175
5281
  }
4176
5282
  }
4177
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
+
4178
5320
  async function memberListCommand(runtime, flags, options) {
4179
5321
  const account = await selectedAccount(runtime, flags);
4180
5322
  if (account.type === 'PERSONAL') {
@@ -4459,7 +5601,7 @@ async function projectKeyListCommand(runtime, flags, project) {
4459
5601
  ], rows) : 'No project keys found.');
4460
5602
  }
4461
5603
  async function projectKeyCreateCommand(runtime, flags, input) {
4462
- requirePlainSecretDelivery(flags, input);
5604
+ requireSecretDelivery(runtime, flags, input);
4463
5605
  await preflightKeyDelivery(runtime, input);
4464
5606
  const sdk = await sdkFor(runtime, flags);
4465
5607
  const { result, delivered } = await createAndDeliverKey({
@@ -4468,7 +5610,10 @@ async function projectKeyCreateCommand(runtime, flags, input) {
4468
5610
  sdk
4469
5611
  }, input);
4470
5612
  const values = keyValues(result.key.accessKey, result.secretKey);
4471
- outputFor(runtime, flags).result(result, [
5613
+ outputFor(runtime, flags).result({
5614
+ key: result.key,
5615
+ delivery: delivered
5616
+ }, [
4472
5617
  `Created project key "${result.key.name}".`,
4473
5618
  ...delivered.length ? [
4474
5619
  '',
@@ -4541,7 +5686,7 @@ async function projectKeyRotateCommand(runtime, flags, input) {
4541
5686
  } else if (!input.copy && !input.output) {
4542
5687
  throw usageError('secret_delivery_required', 'Non-interactive rotation requires --copy or --output.');
4543
5688
  }
4544
- requirePlainSecretDelivery(flags, input);
5689
+ requireSecretDelivery(runtime, flags, input);
4545
5690
  const sdk = await sdkFor(runtime, flags);
4546
5691
  const listed = await sdk.management.projectKeys.list({
4547
5692
  project: input.project,
@@ -4673,7 +5818,10 @@ async function projectKeyRotateCommand(runtime, flags, input) {
4673
5818
  });
4674
5819
  }
4675
5820
  output.result({
4676
- replacement: result,
5821
+ replacement: {
5822
+ key: result.key,
5823
+ delivery: delivered
5824
+ },
4677
5825
  revokedKeyId: input.keyId
4678
5826
  }, input.yes ? [
4679
5827
  secretMessage,
@@ -4722,10 +5870,11 @@ async function preflightKeyDelivery(runtime, options) {
4722
5870
  'EDGE_STORE_ACCESS_KEY',
4723
5871
  'EDGE_STORE_SECRET_KEY'
4724
5872
  ], options);
5873
+ if (options.output) await protectSecretFile(runtime.cwd, options.output);
4725
5874
  }
4726
- function requirePlainSecretDelivery(flags, options) {
4727
- if (flags.plain && !options.copy && !options.output) {
4728
- 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.');
4729
5878
  }
4730
5879
  }
4731
5880
  function keyValues(accessKey, secretKey) {
@@ -4798,12 +5947,13 @@ async function tokenCreateCommand(runtime, flags, options) {
4798
5947
  if (!options.preset && !options.scope?.length) {
4799
5948
  throw usageError('token_permissions_required', 'Token creation requires --preset or at least one --scope.');
4800
5949
  }
4801
- if (flags.plain && !options.copy && !options.output) {
4802
- 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.');
4803
5952
  }
4804
5953
  await preflightEnvSecret(runtime.cwd, [
4805
5954
  'EDGESTORE_TOKEN'
4806
5955
  ], options);
5956
+ if (options.output) await protectSecretFile(runtime.cwd, options.output);
4807
5957
  const sdk = await sdkFor(runtime, flags);
4808
5958
  const permissions = options.preset ? {
4809
5959
  preset: options.preset
@@ -4848,7 +5998,10 @@ async function tokenCreateCommand(runtime, flags, options) {
4848
5998
  'Use a credential with token:revoke access or revoke the token in the dashboard.'
4849
5999
  ]
4850
6000
  });
4851
- outputFor(runtime, flags).result(result, [
6001
+ outputFor(runtime, flags).result({
6002
+ token: result.token,
6003
+ delivery: delivered
6004
+ }, [
4852
6005
  `Created ${result.token.kind.toLowerCase()} token "${result.token.name}".`,
4853
6006
  ...delivered.length ? [
4854
6007
  '',
@@ -4883,6 +6036,141 @@ async function tokenRevokeCommand(runtime, flags, input) {
4883
6036
  outputFor(runtime, flags).result(result, `Revoked management token ${input.tokenId}.`, input.tokenId);
4884
6037
  }
4885
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;
4886
6174
  async function fileUploadCommand(runtime, flags, input) {
4887
6175
  if (flags.plain) {
4888
6176
  throw usageError('plain_output_unavailable', 'File upload does not have a single plain-text result.', [
@@ -4904,73 +6192,64 @@ async function fileUploadCommand(runtime, flags, input) {
4904
6192
  if (localFiles.length > 1 && input.path && !input.path.endsWith('/')) {
4905
6193
  throw usageError('upload_path_not_folder', '--path must end in / when uploading multiple files.');
4906
6194
  }
6195
+ validateUniqueUploadDestinations(localFiles, {
6196
+ destinationPath: input.path,
6197
+ keepName: input.keepName
6198
+ });
4907
6199
  const project = await resolvedProjectRef(runtime, flags, input.project);
4908
6200
  const sdk = await sdkFor(runtime, flags);
4909
- const results = [];
4910
- for (const [index, localFile] of localFiles.entries()){
4911
- try {
4912
- const fileName = path.basename(localFile);
4913
- const mimeType = mimeTypeFor(fileName);
4914
- const destination = resolveUploadDestination(input.path, {
4915
- fileName,
4916
- keepName: input.keepName
4917
- });
4918
- const source = await openAsBlob(localFile, mimeType ? {
4919
- type: mimeType
4920
- } : undefined);
6201
+ const progressDisplay = createUploadProgressDisplay(runtime, flags);
6202
+ let outcomes;
6203
+ try {
6204
+ const upload = async (localFile, index)=>{
4921
6205
  try {
4922
- const completed = await sdk.management.uploads.upload({
4923
- project,
4924
- bucket: input.bucket,
4925
- source,
4926
- signedReadUrl: {},
4927
- ...destination.fileName ? {
4928
- fileName: destination.fileName
4929
- } : {},
4930
- ...destination.path ? {
4931
- path: destination.path
4932
- } : {},
4933
- mimeType,
4934
- signal: runtime.signal,
4935
- onProgress: (progress)=>reportProgress(runtime, flags, {
4936
- fileName,
4937
- percentage: progress.percentage
4938
- })
4939
- });
4940
- results.push({
4941
- localPath: localFile,
4942
- ...completed
4943
- });
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
+ };
4944
6221
  } catch (error) {
4945
- throw normalizeUploadError(error, {
4946
- runtime,
4947
- flags,
4948
- project
4949
- });
6222
+ return {
6223
+ status: 'failed',
6224
+ localPath: localFile,
6225
+ cause: normalizeError(error)
6226
+ };
4950
6227
  }
4951
- } catch (error) {
4952
- if (localFiles.length === 1) throw error;
4953
- const cause = normalizeError(error);
4954
- const notAttemptedPaths = localFiles.slice(index + 1);
4955
- throw new CliError('file_upload_incomplete', [
4956
- `Upload batch stopped while processing ${localFile}: ${cause.message}`,
4957
- `Completed (${results.length}):`,
4958
- ...results.map((result)=>` ${result.localPath} -> ${result.signedReadUrl?.signedUrl ?? result.file.url} (${result.file.id})`),
4959
- `Interrupted: ${localFile}`,
4960
- `Not attempted (${notAttemptedPaths.length}):`,
4961
- ...notAttemptedPaths.map((file)=>` ${file}`)
4962
- ].join('\n'), {
4963
- details: {
4964
- completed: results,
4965
- interruptedPath: localFile,
4966
- notAttemptedPaths,
4967
- cause: errorDetails(cause)
4968
- },
4969
- requestId: cause.options.requestId,
4970
- suggestions: cause.options.suggestions,
4971
- exitCode: cause.exitCode
4972
- });
4973
- }
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
+ });
4974
6253
  }
4975
6254
  const human = results.map((result)=>{
4976
6255
  const readUrl = result.signedReadUrl?.signedUrl ?? result.file.url;
@@ -4980,6 +6259,123 @@ async function fileUploadCommand(runtime, flags, input) {
4980
6259
  uploads: results
4981
6260
  }, human);
4982
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
+ }
4983
6379
  function resolveUploadDestination(destinationPath, input) {
4984
6380
  if (!destinationPath) {
4985
6381
  return input.keepName ? {
@@ -5045,11 +6441,6 @@ function interruptedError() {
5045
6441
  function isFileBlobMutation(error) {
5046
6442
  return error instanceof DOMException && error.name === 'NotReadableError';
5047
6443
  }
5048
- function reportProgress(runtime, flags, progress) {
5049
- if (flags.progress && !flags.json && runtime.io.outputIsTty) {
5050
- runtime.io.stderr.write(`${progress.fileName}: uploading ${progress.percentage}%\n`);
5051
- }
5052
- }
5053
6444
  async function fileUploadStatusCommand(runtime, flags, input) {
5054
6445
  const sdk = await sdkFor(runtime, flags);
5055
6446
  const result = await sdk.management.uploads.get({
@@ -5209,9 +6600,52 @@ Common workflows:
5209
6600
  program.command('whoami').description('Show the current identity and context').action(async ()=>{
5210
6601
  await whoamiCommand(runtime, globalFlags(program));
5211
6602
  });
5212
- program.command('doctor').description('Check local configuration and API connectivity').action(async ()=>{
5213
- 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
+ });
5214
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
+ }
5215
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', `
5216
6650
  Examples:
5217
6651
  edgestore init --new --name "Marketing Site" --output .env.local