@edgestore/cli 1.0.0-next.3 → 1.0.0-next.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +133 -4
- package/agent-assets/skills.json +9 -0
- package/dist/bin.mjs +1990 -556
- package/package.json +11 -5
package/dist/bin.mjs
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { createRequire } from 'node:module';
|
|
3
|
+
import { fileURLToPath } from 'node:url';
|
|
3
4
|
import { CommanderError, Command } from 'commander';
|
|
4
|
-
import { randomUUID } from 'node:crypto';
|
|
5
|
-
import { rm, rmdir, readFile, mkdir, writeFile, rename, stat,
|
|
5
|
+
import { randomUUID, createHash } from 'node:crypto';
|
|
6
|
+
import { rm, rmdir, readFile, mkdir, writeFile, rename, stat, unlink, lstat, readdir, realpath, access, chmod, glob } from 'node:fs/promises';
|
|
6
7
|
import path from 'node:path';
|
|
7
8
|
import { z } from 'zod';
|
|
8
9
|
import { EdgeStoreApiError, EdgeStoreNetworkError, EdgeStoreTimeoutError, EdgeStoreAbortError, createEdgeStoreSdk, EdgeStoreUploadCleanupError } from '@edgestore/sdk';
|
|
@@ -13,13 +14,19 @@ import open from 'open';
|
|
|
13
14
|
import * as oauth from 'openid-client';
|
|
14
15
|
import { createServer } from 'node:http';
|
|
15
16
|
import { password, isCancel, text, confirm, select } from '@clack/prompts';
|
|
16
|
-
import {
|
|
17
|
+
import { parseTree, findNodeAtLocation, applyEdits, modify } from 'jsonc-parser';
|
|
18
|
+
import { parse } from 'smol-toml';
|
|
19
|
+
import { homedir } from 'node:os';
|
|
17
20
|
import { getPackages } from '@manypkg/get-packages';
|
|
21
|
+
import { Writable, Readable } from 'node:stream';
|
|
22
|
+
import { detect } from 'package-manager-detector/detect';
|
|
23
|
+
import { findRoot, NoPkgJsonFound } from '@manypkg/find-root';
|
|
24
|
+
import { parse as parse$1 } from '@babel/parser';
|
|
25
|
+
import { isIdentifier, traverseFast, isStringLiteral, isObjectProperty } from '@babel/types';
|
|
18
26
|
import { createWriteStream, constants, openAsBlob } from 'node:fs';
|
|
19
|
-
import { Readable, Writable } from 'node:stream';
|
|
20
27
|
import { pipeline } from 'node:stream/promises';
|
|
21
|
-
import { detect } from 'package-manager-detector/detect';
|
|
22
28
|
import { promisify } from 'node:util';
|
|
29
|
+
import { createLogUpdate } from 'log-update';
|
|
23
30
|
|
|
24
31
|
function renderCliCommand(flags, command, options = {}) {
|
|
25
32
|
const args = [
|
|
@@ -133,7 +140,7 @@ function resolveApiUrl(flagValue, envValue) {
|
|
|
133
140
|
try {
|
|
134
141
|
url = new URL(rawValue);
|
|
135
142
|
} catch {
|
|
136
|
-
throw usageError('invalid_api_url',
|
|
143
|
+
throw usageError('invalid_api_url', 'Invalid EdgeStore API URL. Supply an http or https origin.');
|
|
137
144
|
}
|
|
138
145
|
if (url.protocol !== 'http:' && url.protocol !== 'https:') {
|
|
139
146
|
throw usageError('invalid_api_url', 'The EdgeStore API URL must use http or https.');
|
|
@@ -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
|
|
1584
|
-
|
|
1585
|
-
|
|
1586
|
-
|
|
1587
|
-
|
|
1588
|
-
|
|
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
|
-
|
|
2137
|
+
contents = await readFile(packagePath, 'utf8');
|
|
1645
2138
|
} catch (error) {
|
|
1646
|
-
|
|
1647
|
-
|
|
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
|
-
|
|
1651
|
-
|
|
1652
|
-
|
|
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
|
-
|
|
1669
|
-
|
|
1670
|
-
|
|
1671
|
-
|
|
1672
|
-
|
|
1673
|
-
|
|
1674
|
-
|
|
1675
|
-
|
|
1676
|
-
|
|
1677
|
-
|
|
1678
|
-
|
|
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
|
|
1951
|
-
throw usageError('one_time_secret_delivery_required', '
|
|
1952
|
-
'
|
|
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(
|
|
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
|
|
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
|
|
3951
|
+
} catch {
|
|
2639
3952
|
rethrowIfAborted(runtime.signal);
|
|
2640
3953
|
checks.push({
|
|
2641
3954
|
name: 'Credential',
|
|
2642
|
-
status: '
|
|
2643
|
-
detail:
|
|
3955
|
+
status: 'warn',
|
|
3956
|
+
detail: 'No usable credential. Login or refresh explicitly; doctor does not change credentials.'
|
|
3957
|
+
});
|
|
3958
|
+
}
|
|
3959
|
+
if (!credential) {
|
|
3960
|
+
checks.push({
|
|
3961
|
+
name: 'Remote checks',
|
|
3962
|
+
status: 'skip',
|
|
3963
|
+
detail: 'No usable existing credential; no network requests were made.'
|
|
2644
3964
|
});
|
|
3965
|
+
report(runtime, flags, checks);
|
|
3966
|
+
return;
|
|
2645
3967
|
}
|
|
2646
3968
|
const sdk = runtime.sdkFactory({
|
|
2647
|
-
token: credential
|
|
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
|
|
3981
|
+
} catch {
|
|
2660
3982
|
rethrowIfAborted(runtime.signal);
|
|
2661
3983
|
checks.push({
|
|
2662
3984
|
name: 'API',
|
|
2663
3985
|
status: 'fail',
|
|
2664
|
-
detail:
|
|
3986
|
+
detail: 'Health check failed; verify the API endpoint and network.'
|
|
3987
|
+
});
|
|
3988
|
+
}
|
|
3989
|
+
let authenticated = false;
|
|
3990
|
+
try {
|
|
3991
|
+
const identity = await sdk.management.whoami({
|
|
3992
|
+
signal: runtime.signal
|
|
3993
|
+
});
|
|
3994
|
+
authenticated = true;
|
|
3995
|
+
const scopeDetail = 'scopes' in identity.actor ? ` (${identity.actor.scopes.join(', ')})` : '';
|
|
3996
|
+
checks.push({
|
|
3997
|
+
name: 'Authentication',
|
|
3998
|
+
status: 'pass',
|
|
3999
|
+
detail: `${identity.actor.kind}${scopeDetail}`
|
|
4000
|
+
});
|
|
4001
|
+
} catch {
|
|
4002
|
+
rethrowIfAborted(runtime.signal);
|
|
4003
|
+
checks.push({
|
|
4004
|
+
name: 'Authentication',
|
|
4005
|
+
status: 'fail',
|
|
4006
|
+
detail: 'Credential validation failed; inspect the login and granted scopes.'
|
|
2665
4007
|
});
|
|
2666
4008
|
}
|
|
2667
|
-
if (
|
|
2668
|
-
let authenticated = false;
|
|
4009
|
+
if (authenticated && activeAccount) {
|
|
2669
4010
|
try {
|
|
2670
|
-
const
|
|
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: '
|
|
4016
|
+
name: 'Active account',
|
|
2677
4017
|
status: 'pass',
|
|
2678
|
-
detail: `${
|
|
4018
|
+
detail: `${account.account.displayName} (${account.account.id})`
|
|
2679
4019
|
});
|
|
2680
|
-
} catch
|
|
4020
|
+
} catch {
|
|
2681
4021
|
rethrowIfAborted(runtime.signal);
|
|
2682
4022
|
checks.push({
|
|
2683
|
-
name: '
|
|
4023
|
+
name: 'Active account',
|
|
2684
4024
|
status: 'fail',
|
|
2685
|
-
detail:
|
|
4025
|
+
detail: 'Account is inaccessible with the current credential.'
|
|
2686
4026
|
});
|
|
2687
4027
|
}
|
|
2688
|
-
|
|
2689
|
-
|
|
2690
|
-
|
|
2691
|
-
|
|
2692
|
-
|
|
2693
|
-
|
|
2694
|
-
|
|
2695
|
-
|
|
2696
|
-
|
|
2697
|
-
|
|
2698
|
-
|
|
2699
|
-
}
|
|
2700
|
-
|
|
2701
|
-
|
|
2702
|
-
|
|
2703
|
-
|
|
2704
|
-
|
|
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
|
-
|
|
4048
|
+
} catch {
|
|
4049
|
+
rethrowIfAborted(runtime.signal);
|
|
2715
4050
|
checks.push({
|
|
2716
4051
|
name: 'Linked project',
|
|
2717
4052
|
status: 'fail',
|
|
2718
|
-
detail:
|
|
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
|
|
4084
|
+
} catch {
|
|
2762
4085
|
rethrowIfAborted(runtime.signal);
|
|
2763
4086
|
checks.push({
|
|
2764
4087
|
name: 'Global config',
|
|
2765
4088
|
status: 'fail',
|
|
2766
|
-
detail:
|
|
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
|
|
4106
|
+
} catch {
|
|
2784
4107
|
rethrowIfAborted(runtime.signal);
|
|
2785
4108
|
checks.push({
|
|
2786
4109
|
name: 'Local config',
|
|
2787
4110
|
status: 'fail',
|
|
2788
|
-
detail:
|
|
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
|
|
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
|
|
3223
|
-
|
|
3224
|
-
|
|
3225
|
-
|
|
3226
|
-
|
|
3227
|
-
|
|
3228
|
-
|
|
3229
|
-
|
|
3230
|
-
|
|
3231
|
-
|
|
3232
|
-
|
|
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
|
|
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
|
-
|
|
3267
|
-
|
|
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
|
|
3271
|
-
|
|
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
|
-
|
|
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
|
|
3283
|
-
|
|
3284
|
-
|
|
3285
|
-
|
|
3286
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
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:
|
|
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
|
|
4727
|
-
if (flags
|
|
4728
|
-
throw usageError('secret_delivery_required', '
|
|
5875
|
+
function requireSecretDelivery(runtime, flags, options) {
|
|
5876
|
+
if (!isInteractive(runtime, flags) && !options.output) {
|
|
5877
|
+
throw usageError('secret_delivery_required', 'Automated project-key commands require --output to deliver the one-time key to a protected env file.');
|
|
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
|
|
4802
|
-
throw usageError('secret_delivery_required', '
|
|
5950
|
+
if (!isInteractive(runtime, flags) && !options.output) {
|
|
5951
|
+
throw usageError('secret_delivery_required', 'Automated token creation requires --output to deliver the one-time token to a protected env file.');
|
|
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(
|
|
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
|
|
4910
|
-
|
|
4911
|
-
|
|
4912
|
-
|
|
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
|
-
|
|
4923
|
-
|
|
4924
|
-
|
|
4925
|
-
|
|
4926
|
-
|
|
4927
|
-
|
|
4928
|
-
|
|
4929
|
-
|
|
4930
|
-
|
|
4931
|
-
|
|
4932
|
-
|
|
4933
|
-
|
|
4934
|
-
|
|
4935
|
-
|
|
4936
|
-
|
|
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
|
-
|
|
4946
|
-
|
|
4947
|
-
|
|
4948
|
-
|
|
4949
|
-
}
|
|
6222
|
+
return {
|
|
6223
|
+
status: 'failed',
|
|
6224
|
+
localPath: localFile,
|
|
6225
|
+
cause: normalizeError(error)
|
|
6226
|
+
};
|
|
4950
6227
|
}
|
|
4951
|
-
}
|
|
4952
|
-
|
|
4953
|
-
|
|
4954
|
-
|
|
4955
|
-
|
|
4956
|
-
|
|
4957
|
-
|
|
4958
|
-
|
|
4959
|
-
|
|
4960
|
-
|
|
4961
|
-
|
|
4962
|
-
|
|
4963
|
-
|
|
4964
|
-
|
|
4965
|
-
|
|
4966
|
-
|
|
4967
|
-
|
|
4968
|
-
|
|
4969
|
-
|
|
4970
|
-
|
|
4971
|
-
|
|
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),
|
|
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
|