@trycadence/cli 0.1.1 → 0.1.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cadence +736 -54
- package/package.json +1 -1
package/dist/cadence
CHANGED
|
@@ -1447,7 +1447,16 @@ function createCadenceClient(options = {}) {
|
|
|
1447
1447
|
rpc,
|
|
1448
1448
|
health: () => getCadenceHealth(healthOptions),
|
|
1449
1449
|
auth: {
|
|
1450
|
-
login: (input) => rpc.auth.login.mutate(input)
|
|
1450
|
+
login: (input) => rpc.auth.login.mutate(input),
|
|
1451
|
+
cli: {
|
|
1452
|
+
start: (input) => rpc.auth.cli.start.mutate(input),
|
|
1453
|
+
poll: (input) => rpc.auth.cli.poll.query(input),
|
|
1454
|
+
complete: (input) => rpc.auth.cli.complete.mutate(input),
|
|
1455
|
+
refresh: (input) => rpc.auth.cli.refresh.mutate(input)
|
|
1456
|
+
}
|
|
1457
|
+
},
|
|
1458
|
+
actors: {
|
|
1459
|
+
ensureWorkspaceAgent: (input) => rpc.actors.ensureWorkspaceAgent.mutate(input)
|
|
1451
1460
|
},
|
|
1452
1461
|
events: {
|
|
1453
1462
|
list: (input) => rpc.events.list.query(input)
|
|
@@ -1462,7 +1471,8 @@ function createCadenceClient(options = {}) {
|
|
|
1462
1471
|
attach: (input) => rpc.tickets.attach.mutate(input),
|
|
1463
1472
|
update: (input) => rpc.tickets.update.mutate(input),
|
|
1464
1473
|
changeStatus: (input) => rpc.tickets.changeStatus.mutate(input),
|
|
1465
|
-
complete: (input) => rpc.tickets.complete.mutate(input)
|
|
1474
|
+
complete: (input) => rpc.tickets.complete.mutate(input),
|
|
1475
|
+
log: (input) => rpc.tickets.log.mutate(input)
|
|
1466
1476
|
},
|
|
1467
1477
|
intake: {
|
|
1468
1478
|
create: (input) => rpc.intake.create.mutate(input),
|
|
@@ -1471,6 +1481,7 @@ function createCadenceClient(options = {}) {
|
|
|
1471
1481
|
sessions: {
|
|
1472
1482
|
start: (input) => rpc.sessions.start.mutate(input),
|
|
1473
1483
|
end: (input) => rpc.sessions.end.mutate(input),
|
|
1484
|
+
files: (input) => rpc.sessions.files.mutate(input),
|
|
1474
1485
|
current: (input) => rpc.sessions.current.query(input),
|
|
1475
1486
|
leases: {
|
|
1476
1487
|
create: (input) => rpc.sessions.leases.create.mutate(input),
|
|
@@ -1480,20 +1491,41 @@ function createCadenceClient(options = {}) {
|
|
|
1480
1491
|
changesets: {
|
|
1481
1492
|
create: (input) => rpc.changesets.create.mutate(input),
|
|
1482
1493
|
get: (input) => rpc.changesets.get.query(input),
|
|
1483
|
-
|
|
1494
|
+
context: (input) => rpc.changesets.context.query(input),
|
|
1495
|
+
list: (input) => rpc.changesets.list.query(input),
|
|
1496
|
+
notes: {
|
|
1497
|
+
get: (input) => rpc.changesets.notes.get.query(input),
|
|
1498
|
+
put: (input) => rpc.changesets.notes.put.mutate(input),
|
|
1499
|
+
apply: (input) => rpc.changesets.notes.markApplied.mutate(input)
|
|
1500
|
+
}
|
|
1501
|
+
},
|
|
1502
|
+
projects: {
|
|
1503
|
+
default: () => rpc.projects.default.query(),
|
|
1504
|
+
list: () => rpc.projects.list.query(),
|
|
1505
|
+
resolve: (input) => rpc.projects.resolve.query(input)
|
|
1484
1506
|
}
|
|
1485
1507
|
};
|
|
1486
1508
|
}
|
|
1487
1509
|
|
|
1488
1510
|
// src/index.ts
|
|
1489
1511
|
import { spawnSync } from "child_process";
|
|
1490
|
-
import {
|
|
1491
|
-
import {
|
|
1512
|
+
import { createHash, randomUUID } from "crypto";
|
|
1513
|
+
import { mkdir, readFile, rm, stat, writeFile } from "fs/promises";
|
|
1514
|
+
import { basename, dirname, isAbsolute, join, parse } from "path";
|
|
1492
1515
|
import { createInterface } from "readline/promises";
|
|
1493
1516
|
var ticketPriorities = ["low", "normal", "high", "urgent"];
|
|
1494
1517
|
var ticketStatuses = ["backlog", "ready", "in_progress", "blocked", "review", "done", "abandoned"];
|
|
1518
|
+
var sessionFileChangeKinds = ["added", "modified", "deleted", "renamed", "unknown"];
|
|
1519
|
+
var workLogEntryKinds = ["intent", "decision", "rationale", "action", "verification", "blocker", "correction", "note"];
|
|
1520
|
+
var workLogParentSelectors = ["last", "ticket-last", "session-last", "last-decision", "last-correction", "last-action"];
|
|
1521
|
+
var changesetPrNoteSources = ["agent", "human", "system"];
|
|
1495
1522
|
var defaultLeaseTtlSeconds = 5 * 60 * 60;
|
|
1496
1523
|
var defaultCliApiBaseUrl = "https://cadenceapi.deploy.lvl8studios.com";
|
|
1524
|
+
var defaultCliWebBaseUrl = "https://cadence.deploy.lvl8studios.com";
|
|
1525
|
+
var credentialRefreshSkewMs = 60 * 1000;
|
|
1526
|
+
var credentialRefreshLockTimeoutMs = 10 * 1000;
|
|
1527
|
+
var credentialRefreshLockStaleMs = 60 * 1000;
|
|
1528
|
+
var credentialRefreshLockPollMs = 100;
|
|
1497
1529
|
|
|
1498
1530
|
class CliError extends Error {
|
|
1499
1531
|
code;
|
|
@@ -1513,14 +1545,20 @@ function readFlagValue(argv, index, flag) {
|
|
|
1513
1545
|
return value;
|
|
1514
1546
|
}
|
|
1515
1547
|
var knownCommandPaths = [
|
|
1548
|
+
["actors", "ensure-workspace-agent"],
|
|
1516
1549
|
["auth", "login"],
|
|
1517
1550
|
["auth", "status"],
|
|
1518
1551
|
["auth", "logout"],
|
|
1519
1552
|
["changesets", "create"],
|
|
1520
1553
|
["changesets", "list"],
|
|
1521
1554
|
["changesets", "get"],
|
|
1555
|
+
["changesets", "current"],
|
|
1556
|
+
["changesets", "notes", "get"],
|
|
1557
|
+
["changesets", "notes", "put"],
|
|
1558
|
+
["changesets", "notes", "apply"],
|
|
1522
1559
|
["sessions", "start"],
|
|
1523
1560
|
["sessions", "end"],
|
|
1561
|
+
["sessions", "files"],
|
|
1524
1562
|
["sessions", "current"],
|
|
1525
1563
|
["intake", "dismiss"],
|
|
1526
1564
|
["intake"],
|
|
@@ -1528,12 +1566,14 @@ var knownCommandPaths = [
|
|
|
1528
1566
|
["tickets", "complete"],
|
|
1529
1567
|
["tickets", "release"],
|
|
1530
1568
|
["tickets", "claim"],
|
|
1569
|
+
["tickets", "log"],
|
|
1531
1570
|
["tickets", "update"],
|
|
1532
1571
|
["tickets", "create"],
|
|
1533
1572
|
["tickets", "list"],
|
|
1534
1573
|
["tickets", "get"],
|
|
1535
1574
|
["events", "list"],
|
|
1536
1575
|
["work", "overview"],
|
|
1576
|
+
["projects", "list"],
|
|
1537
1577
|
["init"],
|
|
1538
1578
|
["status"],
|
|
1539
1579
|
["help"]
|
|
@@ -1591,7 +1631,10 @@ function parseCliArgs(argv) {
|
|
|
1591
1631
|
continue;
|
|
1592
1632
|
}
|
|
1593
1633
|
if (arg.startsWith("--")) {
|
|
1594
|
-
|
|
1634
|
+
const key = arg.slice(2);
|
|
1635
|
+
const value = readFlagValue(argv, index, arg);
|
|
1636
|
+
options[key] = key === "file" && options[key] ? `${options[key]}
|
|
1637
|
+
${value}` : value;
|
|
1595
1638
|
index += 1;
|
|
1596
1639
|
continue;
|
|
1597
1640
|
}
|
|
@@ -1620,12 +1663,40 @@ function safeJsonParse(source, filePath) {
|
|
|
1620
1663
|
}
|
|
1621
1664
|
const record = parsed;
|
|
1622
1665
|
const server = typeof record.server === "string" ? record.server : undefined;
|
|
1666
|
+
const webBaseUrl = typeof record.webBaseUrl === "string" ? record.webBaseUrl : undefined;
|
|
1667
|
+
const profile = typeof record.profile === "string" ? record.profile : undefined;
|
|
1623
1668
|
const projectId = typeof record.projectId === "string" ? record.projectId : typeof record.project === "string" ? record.project : undefined;
|
|
1669
|
+
const profiles = parseProfiles(record.profiles, filePath);
|
|
1624
1670
|
return {
|
|
1625
1671
|
...server ? { server } : {},
|
|
1626
|
-
...
|
|
1672
|
+
...webBaseUrl ? { webBaseUrl } : {},
|
|
1673
|
+
...projectId ? { projectId } : {},
|
|
1674
|
+
...profile ? { profile } : {},
|
|
1675
|
+
...profiles ? { profiles } : {}
|
|
1627
1676
|
};
|
|
1628
1677
|
}
|
|
1678
|
+
function parseProfiles(value, filePath) {
|
|
1679
|
+
if (value === undefined) {
|
|
1680
|
+
return;
|
|
1681
|
+
}
|
|
1682
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
1683
|
+
throw new CliError("CONFIG_ERROR", `${filePath} profiles must be a JSON object.`);
|
|
1684
|
+
}
|
|
1685
|
+
const profiles = {};
|
|
1686
|
+
for (const [name, rawProfile] of Object.entries(value)) {
|
|
1687
|
+
if (!rawProfile || typeof rawProfile !== "object" || Array.isArray(rawProfile)) {
|
|
1688
|
+
throw new CliError("CONFIG_ERROR", `${filePath} profile '${name}' must be a JSON object.`);
|
|
1689
|
+
}
|
|
1690
|
+
const profileRecord = rawProfile;
|
|
1691
|
+
const server = typeof profileRecord.server === "string" ? profileRecord.server : undefined;
|
|
1692
|
+
const webBaseUrl = typeof profileRecord.webBaseUrl === "string" ? profileRecord.webBaseUrl : undefined;
|
|
1693
|
+
profiles[name] = {
|
|
1694
|
+
...server ? { server } : {},
|
|
1695
|
+
...webBaseUrl ? { webBaseUrl } : {}
|
|
1696
|
+
};
|
|
1697
|
+
}
|
|
1698
|
+
return profiles;
|
|
1699
|
+
}
|
|
1629
1700
|
async function readOptionalConfig(filePath) {
|
|
1630
1701
|
const file = Bun.file(filePath);
|
|
1631
1702
|
if (!await file.exists()) {
|
|
@@ -1645,12 +1716,104 @@ async function mergeConfigFile(filePath, updates) {
|
|
|
1645
1716
|
...updates
|
|
1646
1717
|
});
|
|
1647
1718
|
}
|
|
1648
|
-
|
|
1719
|
+
function parseAgentIdentityCache(source, filePath) {
|
|
1720
|
+
const parsed = JSON.parse(source);
|
|
1721
|
+
if (!parsed || typeof parsed !== "object") {
|
|
1722
|
+
throw new CliError("CONFIG_ERROR", `${filePath} must contain a JSON object.`);
|
|
1723
|
+
}
|
|
1724
|
+
const record = parsed;
|
|
1725
|
+
const identities = record.identities;
|
|
1726
|
+
if (record.version !== 1 || typeof record.workspaceRef !== "string" || typeof record.workspaceName !== "string" || !identities || typeof identities !== "object" || Array.isArray(identities)) {
|
|
1727
|
+
throw new CliError("CONFIG_ERROR", `${filePath} is not a valid Cadence agent identity cache.`);
|
|
1728
|
+
}
|
|
1729
|
+
const normalizedIdentities = {};
|
|
1730
|
+
for (const [key, value] of Object.entries(identities)) {
|
|
1731
|
+
if (!value || typeof value !== "object") {
|
|
1732
|
+
throw new CliError("CONFIG_ERROR", `${filePath} contains an invalid identity entry.`);
|
|
1733
|
+
}
|
|
1734
|
+
const identity2 = value;
|
|
1735
|
+
if (typeof identity2.actorId !== "string" || typeof identity2.displayName !== "string" || identity2.kind !== "agent" || typeof identity2.agentKind !== "string" || typeof identity2.updatedAt !== "string") {
|
|
1736
|
+
throw new CliError("CONFIG_ERROR", `${filePath} contains an invalid identity entry.`);
|
|
1737
|
+
}
|
|
1738
|
+
normalizedIdentities[key] = {
|
|
1739
|
+
actorId: identity2.actorId,
|
|
1740
|
+
displayName: identity2.displayName,
|
|
1741
|
+
kind: "agent",
|
|
1742
|
+
agentKind: identity2.agentKind,
|
|
1743
|
+
updatedAt: identity2.updatedAt
|
|
1744
|
+
};
|
|
1745
|
+
}
|
|
1746
|
+
return {
|
|
1747
|
+
version: 1,
|
|
1748
|
+
workspaceRef: record.workspaceRef,
|
|
1749
|
+
workspaceName: record.workspaceName,
|
|
1750
|
+
identities: normalizedIdentities
|
|
1751
|
+
};
|
|
1752
|
+
}
|
|
1753
|
+
async function readOptionalAgentIdentityCache(filePath) {
|
|
1754
|
+
const file = Bun.file(filePath);
|
|
1755
|
+
if (!await file.exists()) {
|
|
1756
|
+
return null;
|
|
1757
|
+
}
|
|
1758
|
+
return parseAgentIdentityCache(await file.text(), filePath);
|
|
1759
|
+
}
|
|
1760
|
+
function agentIdentityCachePath(config, options) {
|
|
1761
|
+
const cwd = options.cwd ?? process.cwd();
|
|
1762
|
+
const cadenceDirectory = config.localConfigPath ? dirname(config.localConfigPath) : config.repoConfigPath ? dirname(config.repoConfigPath) : join(cwd, ".cadence");
|
|
1763
|
+
return join(cadenceDirectory, "agent-identities.json");
|
|
1764
|
+
}
|
|
1765
|
+
async function writeAgentIdentityCache(filePath, cache) {
|
|
1766
|
+
await mkdir(dirname(filePath), { recursive: true });
|
|
1767
|
+
await writeFile(filePath, `${JSON.stringify(cache, null, 2)}
|
|
1768
|
+
`);
|
|
1769
|
+
}
|
|
1770
|
+
async function ensureWorkspaceAgentIdentity(parsed, config, client, options) {
|
|
1771
|
+
const projectId = requireProjectId(config);
|
|
1772
|
+
const agentKind = requireOption(parsed, "agent-kind");
|
|
1773
|
+
const filePath = agentIdentityCachePath(config, options);
|
|
1774
|
+
const existing = await readOptionalAgentIdentityCache(filePath);
|
|
1775
|
+
const explicitWorkspaceRef = parsed.options["workspace-ref"];
|
|
1776
|
+
const workspaceRef = explicitWorkspaceRef ?? existing?.workspaceRef ?? `local:${randomUUID()}`;
|
|
1777
|
+
const workspaceName = parsed.options["workspace-name"] ?? existing?.workspaceName ?? basename(options.cwd ?? process.cwd());
|
|
1778
|
+
const identities = explicitWorkspaceRef && existing && explicitWorkspaceRef !== existing.workspaceRef ? {} : { ...existing?.identities ?? {} };
|
|
1779
|
+
const ensured = await client.actors.ensureWorkspaceAgent({
|
|
1780
|
+
projectId,
|
|
1781
|
+
workspaceRef,
|
|
1782
|
+
workspaceName,
|
|
1783
|
+
agentKind,
|
|
1784
|
+
...parsed.options["display-name"] ? { displayName: parsed.options["display-name"] } : {}
|
|
1785
|
+
});
|
|
1786
|
+
const updatedAt = new Date().toISOString();
|
|
1787
|
+
const cache = {
|
|
1788
|
+
version: 1,
|
|
1789
|
+
workspaceRef,
|
|
1790
|
+
workspaceName,
|
|
1791
|
+
identities: {
|
|
1792
|
+
...identities,
|
|
1793
|
+
[agentKind]: {
|
|
1794
|
+
actorId: ensured.actorId,
|
|
1795
|
+
displayName: ensured.displayName,
|
|
1796
|
+
kind: "agent",
|
|
1797
|
+
agentKind: ensured.agentKind,
|
|
1798
|
+
updatedAt
|
|
1799
|
+
}
|
|
1800
|
+
}
|
|
1801
|
+
};
|
|
1802
|
+
await writeAgentIdentityCache(filePath, cache);
|
|
1803
|
+
return {
|
|
1804
|
+
...cache.identities[agentKind],
|
|
1805
|
+
workspaceRef: cache.workspaceRef,
|
|
1806
|
+
workspaceName: cache.workspaceName
|
|
1807
|
+
};
|
|
1808
|
+
}
|
|
1809
|
+
async function findRepoCadenceDirectory(cwd) {
|
|
1649
1810
|
let current = cwd;
|
|
1650
1811
|
while (true) {
|
|
1651
|
-
const
|
|
1652
|
-
|
|
1653
|
-
|
|
1812
|
+
const cadenceDirectory = join(current, ".cadence");
|
|
1813
|
+
const repoConfig = join(cadenceDirectory, "config.json");
|
|
1814
|
+
const localConfig = join(cadenceDirectory, "config.local.json");
|
|
1815
|
+
if (await Bun.file(repoConfig).exists() || await Bun.file(localConfig).exists()) {
|
|
1816
|
+
return cadenceDirectory;
|
|
1654
1817
|
}
|
|
1655
1818
|
const parent = dirname(current);
|
|
1656
1819
|
if (parent === current) {
|
|
@@ -1666,18 +1829,34 @@ async function resolveCliConfig(flags, options = {}) {
|
|
|
1666
1829
|
const env = options.env ?? process.env;
|
|
1667
1830
|
const cwd = options.cwd ?? process.cwd();
|
|
1668
1831
|
const globalConfigPath = join(getConfigHome(env), "config.json");
|
|
1669
|
-
const
|
|
1832
|
+
const repoCadenceDirectory = await findRepoCadenceDirectory(cwd);
|
|
1833
|
+
const repoConfigPath = repoCadenceDirectory ? join(repoCadenceDirectory, "config.json") : null;
|
|
1834
|
+
const localConfigPath = repoCadenceDirectory ? join(repoCadenceDirectory, "config.local.json") : null;
|
|
1670
1835
|
const repoConfigPromise = repoConfigPath ? readOptionalConfig(repoConfigPath) : Promise.resolve({});
|
|
1671
|
-
const
|
|
1836
|
+
const localConfigPromise = localConfigPath ? readOptionalConfig(localConfigPath) : Promise.resolve({});
|
|
1837
|
+
const [globalConfig, repoConfig, localConfig] = await Promise.all([
|
|
1672
1838
|
readOptionalConfig(globalConfigPath),
|
|
1673
|
-
repoConfigPromise
|
|
1839
|
+
repoConfigPromise,
|
|
1840
|
+
localConfigPromise
|
|
1674
1841
|
]);
|
|
1675
|
-
const
|
|
1676
|
-
|
|
1842
|
+
const profiles = {
|
|
1843
|
+
...globalConfig.profiles ?? {},
|
|
1844
|
+
...repoConfig.profiles ?? {},
|
|
1845
|
+
...localConfig.profiles ?? {}
|
|
1846
|
+
};
|
|
1847
|
+
const envProfile = env.CADENCE_PROFILE;
|
|
1848
|
+
const profile = envProfile ?? localConfig.profile ?? repoConfig.profile ?? globalConfig.profile ?? null;
|
|
1849
|
+
const profileConfig = profile ? profiles[profile] : undefined;
|
|
1850
|
+
const server = flags.server ?? env.CADENCE_SERVER ?? localConfig.server ?? profileConfig?.server ?? repoConfig.server ?? globalConfig.server ?? defaultCliApiBaseUrl;
|
|
1851
|
+
const webBaseUrl = env.CADENCE_WEB_BASE_URL ?? localConfig.webBaseUrl ?? profileConfig?.webBaseUrl ?? repoConfig.webBaseUrl ?? globalConfig.webBaseUrl ?? deriveWebBaseUrl(server);
|
|
1852
|
+
const projectId = flags.project ?? localConfig.projectId ?? repoConfig.projectId ?? globalConfig.projectId ?? null;
|
|
1677
1853
|
return {
|
|
1678
1854
|
server,
|
|
1855
|
+
webBaseUrl,
|
|
1856
|
+
profile,
|
|
1679
1857
|
projectId,
|
|
1680
1858
|
repoConfigPath,
|
|
1859
|
+
localConfigPath,
|
|
1681
1860
|
globalConfigPath
|
|
1682
1861
|
};
|
|
1683
1862
|
}
|
|
@@ -1724,8 +1903,55 @@ function getCredentialStore(options) {
|
|
|
1724
1903
|
async function readPromptText(options, message) {
|
|
1725
1904
|
return options.readText ? options.readText(message) : promptText(message);
|
|
1726
1905
|
}
|
|
1727
|
-
|
|
1728
|
-
return options.
|
|
1906
|
+
function isInteractive(options) {
|
|
1907
|
+
return options.isInteractive ?? Boolean(process.stdin.isTTY && process.stderr.isTTY);
|
|
1908
|
+
}
|
|
1909
|
+
function getCliWebBaseUrl(config, parsed, options) {
|
|
1910
|
+
return parsed.options["web-base-url"] ?? options.env?.CADENCE_WEB_BASE_URL ?? process.env.CADENCE_WEB_BASE_URL ?? config.webBaseUrl;
|
|
1911
|
+
}
|
|
1912
|
+
function deriveWebBaseUrl(server) {
|
|
1913
|
+
try {
|
|
1914
|
+
const url = new URL(server);
|
|
1915
|
+
if ((url.hostname === "localhost" || url.hostname === "127.0.0.1") && url.port === "3000") {
|
|
1916
|
+
url.port = "3001";
|
|
1917
|
+
return url.toString().replace(/\/$/, "");
|
|
1918
|
+
}
|
|
1919
|
+
} catch {
|
|
1920
|
+
return defaultCliWebBaseUrl;
|
|
1921
|
+
}
|
|
1922
|
+
return defaultCliWebBaseUrl;
|
|
1923
|
+
}
|
|
1924
|
+
async function openBrowser(url, options) {
|
|
1925
|
+
if (options.openBrowser) {
|
|
1926
|
+
await options.openBrowser(url);
|
|
1927
|
+
return;
|
|
1928
|
+
}
|
|
1929
|
+
if (process.platform === "darwin") {
|
|
1930
|
+
const result = spawnSync("open", [url]);
|
|
1931
|
+
if (result.status !== 0) {
|
|
1932
|
+
throw new CliError("BROWSER_OPEN_FAILED", "Could not open the browser for Cadence login.", {
|
|
1933
|
+
loginUrl: url
|
|
1934
|
+
});
|
|
1935
|
+
}
|
|
1936
|
+
}
|
|
1937
|
+
}
|
|
1938
|
+
async function sleep2(milliseconds, options) {
|
|
1939
|
+
if (options.sleep) {
|
|
1940
|
+
await options.sleep(milliseconds);
|
|
1941
|
+
return;
|
|
1942
|
+
}
|
|
1943
|
+
await new Promise((resolve) => setTimeout(resolve, milliseconds));
|
|
1944
|
+
}
|
|
1945
|
+
async function writeInteractiveStatus(message, options) {
|
|
1946
|
+
if (!isInteractive(options)) {
|
|
1947
|
+
return;
|
|
1948
|
+
}
|
|
1949
|
+
if (options.writeStatus) {
|
|
1950
|
+
await options.writeStatus(message);
|
|
1951
|
+
return;
|
|
1952
|
+
}
|
|
1953
|
+
process.stderr.write(`${message}
|
|
1954
|
+
`);
|
|
1729
1955
|
}
|
|
1730
1956
|
function encodeCredential(credential) {
|
|
1731
1957
|
return JSON.stringify(credential);
|
|
@@ -1754,6 +1980,68 @@ async function readStoredCredential(store, server) {
|
|
|
1754
1980
|
const rawCredential = await store.getCredential(server);
|
|
1755
1981
|
return rawCredential ? decodeCredential(rawCredential) : null;
|
|
1756
1982
|
}
|
|
1983
|
+
function normalizeServerForCredentialLock(server) {
|
|
1984
|
+
try {
|
|
1985
|
+
return new URL(server).toString().replace(/\/$/, "");
|
|
1986
|
+
} catch {
|
|
1987
|
+
return server.trim();
|
|
1988
|
+
}
|
|
1989
|
+
}
|
|
1990
|
+
function credentialRefreshLockPath(config) {
|
|
1991
|
+
const normalizedServer = normalizeServerForCredentialLock(config.server);
|
|
1992
|
+
const serverHash = createHash("sha256").update(normalizedServer).digest("hex").slice(0, 24);
|
|
1993
|
+
return join(dirname(config.globalConfigPath), "locks", `credential-refresh-${serverHash}.lock`);
|
|
1994
|
+
}
|
|
1995
|
+
async function waitForCredentialRefreshLockPoll() {
|
|
1996
|
+
await new Promise((resolve) => setTimeout(resolve, credentialRefreshLockPollMs));
|
|
1997
|
+
}
|
|
1998
|
+
async function removeStaleCredentialRefreshLock(lockPath, now) {
|
|
1999
|
+
try {
|
|
2000
|
+
const lockStats = await stat(lockPath);
|
|
2001
|
+
if (now - lockStats.mtimeMs < credentialRefreshLockStaleMs) {
|
|
2002
|
+
return false;
|
|
2003
|
+
}
|
|
2004
|
+
await rm(lockPath, { recursive: true, force: true });
|
|
2005
|
+
return true;
|
|
2006
|
+
} catch (error) {
|
|
2007
|
+
if (error && typeof error === "object" && "code" in error && error.code === "ENOENT") {
|
|
2008
|
+
return false;
|
|
2009
|
+
}
|
|
2010
|
+
throw error;
|
|
2011
|
+
}
|
|
2012
|
+
}
|
|
2013
|
+
async function acquireCredentialRefreshLock(config) {
|
|
2014
|
+
const lockPath = credentialRefreshLockPath(config);
|
|
2015
|
+
const deadline = Date.now() + credentialRefreshLockTimeoutMs;
|
|
2016
|
+
await mkdir(dirname(lockPath), { recursive: true });
|
|
2017
|
+
while (true) {
|
|
2018
|
+
try {
|
|
2019
|
+
await mkdir(lockPath);
|
|
2020
|
+
await writeFile(join(lockPath, "holder.json"), `${JSON.stringify({
|
|
2021
|
+
pid: process.pid,
|
|
2022
|
+
server: normalizeServerForCredentialLock(config.server),
|
|
2023
|
+
acquiredAt: new Date().toISOString()
|
|
2024
|
+
}, null, 2)}
|
|
2025
|
+
`);
|
|
2026
|
+
return { path: lockPath };
|
|
2027
|
+
} catch (error) {
|
|
2028
|
+
if (!error || typeof error !== "object" || !("code" in error) || error.code !== "EEXIST") {
|
|
2029
|
+
throw error;
|
|
2030
|
+
}
|
|
2031
|
+
const now = Date.now();
|
|
2032
|
+
const removedStaleLock = await removeStaleCredentialRefreshLock(lockPath, now);
|
|
2033
|
+
if (!removedStaleLock && now >= deadline) {
|
|
2034
|
+
throw new CliError("AUTH_REFRESH_LOCK_TIMEOUT", "Timed out waiting for another Cadence credential refresh to finish.", {
|
|
2035
|
+
server: config.server
|
|
2036
|
+
});
|
|
2037
|
+
}
|
|
2038
|
+
await waitForCredentialRefreshLockPoll();
|
|
2039
|
+
}
|
|
2040
|
+
}
|
|
2041
|
+
}
|
|
2042
|
+
async function releaseCredentialRefreshLock(lock) {
|
|
2043
|
+
await rm(lock.path, { recursive: true, force: true });
|
|
2044
|
+
}
|
|
1757
2045
|
async function requireCredentialStore(store) {
|
|
1758
2046
|
if (!await store.isAvailable()) {
|
|
1759
2047
|
throw new CliError("CREDENTIAL_STORE_UNAVAILABLE", "OS secure credential storage is unavailable.");
|
|
@@ -1773,19 +2061,6 @@ async function promptText(message) {
|
|
|
1773
2061
|
readline.close();
|
|
1774
2062
|
}
|
|
1775
2063
|
}
|
|
1776
|
-
async function promptSecret(message) {
|
|
1777
|
-
if (!process.stdin.isTTY || !process.stderr.isTTY) {
|
|
1778
|
-
throw new CliError("AUTH_INTERACTIVE_REQUIRED", "Interactive auth requires a TTY.");
|
|
1779
|
-
}
|
|
1780
|
-
spawnSync("stty", ["-echo"], { stdio: ["inherit", "ignore", "ignore"] });
|
|
1781
|
-
try {
|
|
1782
|
-
return await promptText(message);
|
|
1783
|
-
} finally {
|
|
1784
|
-
spawnSync("stty", ["echo"], { stdio: ["inherit", "ignore", "ignore"] });
|
|
1785
|
-
process.stderr.write(`
|
|
1786
|
-
`);
|
|
1787
|
-
}
|
|
1788
|
-
}
|
|
1789
2064
|
function successEnvelope(data, meta) {
|
|
1790
2065
|
return {
|
|
1791
2066
|
success: true,
|
|
@@ -1816,11 +2091,13 @@ function helpText() {
|
|
|
1816
2091
|
"Cadence CLI",
|
|
1817
2092
|
"",
|
|
1818
2093
|
"Usage:",
|
|
1819
|
-
" cadence init --project <project-id> [--json]",
|
|
2094
|
+
" cadence init [--project <project-id|org/project>] [--json]",
|
|
1820
2095
|
" cadence auth login [--json]",
|
|
1821
2096
|
" cadence auth status [--json]",
|
|
1822
2097
|
" cadence auth logout [--json]",
|
|
2098
|
+
" cadence actors ensure-workspace-agent --agent-kind <kind> [--workspace-name <name>] [--workspace-ref <ref>] [--display-name <name>] [--project <project-id>] [--json]",
|
|
1823
2099
|
" cadence status [--project <project-id>] [--json]",
|
|
2100
|
+
" cadence projects list [--json]",
|
|
1824
2101
|
" cadence work overview [--project <project-id>] [--json]",
|
|
1825
2102
|
" cadence tickets get <ticket-id> [--project <project-id>] [--json]",
|
|
1826
2103
|
" cadence tickets list [--project <project-id>] [--status <status>] [--json]",
|
|
@@ -1828,36 +2105,103 @@ function helpText() {
|
|
|
1828
2105
|
" cadence tickets attach <ticket-id> --from-intake <intake-id> --if-version <version> [--project <project-id>] [--json]",
|
|
1829
2106
|
" cadence tickets create --title <text> [--from-intake <intake-id>] [--project <project-id>] [--json]",
|
|
1830
2107
|
" cadence tickets update <ticket-id> --if-version <version> [--title <text>] [--description <text>] [--priority <priority>] [--status <status>] [--project <project-id>] [--json]",
|
|
1831
|
-
" cadence tickets claim <ticket-id> --session <session-id> [--project <project-id>] [--json]",
|
|
2108
|
+
" cadence tickets claim <ticket-id> --session <session-id> [--actor <actor-id>] [--project <project-id>] [--json]",
|
|
1832
2109
|
" cadence tickets release <ticket-id> --lease <lease-id> [--project <project-id>] [--json]",
|
|
2110
|
+
" cadence tickets log <ticket-id> --kind <intent|decision|rationale|action|verification|blocker|correction|note> --body <text> [--summary <text>] [--under <entry-id|ticket-last|session-last|last-decision|last-correction|last-action>] [--session <session-id>] [--changeset <changeset-id>] [--project <project-id>] [--json]",
|
|
1833
2111
|
" cadence tickets complete <ticket-id> --if-version <version> [--summary <summary>] [--project <project-id>] [--json]",
|
|
1834
|
-
" cadence sessions start --ticket <ticket-id> [--changeset <changeset-id>] [--project <project-id>] [--json]",
|
|
2112
|
+
" cadence sessions start --ticket <ticket-id> [--changeset <changeset-id>] [--actor <actor-id>] [--project <project-id>] [--json]",
|
|
1835
2113
|
" cadence sessions end <session-id> --summary <summary> [--project <project-id>] [--json]",
|
|
2114
|
+
" cadence sessions files <session-id> --file <path> [--file <path>] [--kind <added|modified|deleted|renamed|unknown>] [--changeset <changeset-id>] [--project <project-id>] [--json]",
|
|
1836
2115
|
" cadence changesets create --ticket <ticket-id> --branch <branch> [--base-branch <branch>] [--project <project-id>] [--json]",
|
|
1837
2116
|
" cadence intake dismiss <intake-id> --reason <reason> [--project <project-id>] [--json]",
|
|
1838
2117
|
" cadence sessions current [--project <project-id>] [--ticket <ticket-id>] [--changeset <changeset-id>] [--json]",
|
|
2118
|
+
" cadence changesets current [--branch current|<branch>] [--project <project-id>] [--json]",
|
|
1839
2119
|
" cadence changesets get <changeset-id> [--project <project-id>] [--json]",
|
|
1840
2120
|
" cadence changesets list [--project <project-id>] [--ticket <ticket-id>] [--status <status>] [--json]",
|
|
2121
|
+
" cadence changesets notes get [--changeset <id>|--branch current|<branch>] [--project <project-id>] [--json]",
|
|
2122
|
+
" cadence changesets notes put [--changeset <id>|--branch current|<branch>] --title <text> --body-file <path> [--head-sha <sha>] [--base-sha <sha>] [--pr-url <url>] [--pr-number <n>] [--project <project-id>] [--json]",
|
|
2123
|
+
" cadence changesets notes apply [--changeset <id>|--branch current|<branch>] --provider github --pr-number <n> --pr-url <url> [--project <project-id>] [--json]",
|
|
1841
2124
|
" cadence events list [--project <project-id>] [--ticket <ticket-id>] [--changeset <changeset-id>] [--session <session-id>] [--json]",
|
|
1842
2125
|
"",
|
|
1843
2126
|
"Global flags:",
|
|
1844
|
-
" --project <id> Cadence project ID",
|
|
2127
|
+
" --project <id> Cadence project ID or org/project slug",
|
|
2128
|
+
" --server <url> Cadence API server override",
|
|
1845
2129
|
" --json Print stable JSON envelope",
|
|
1846
2130
|
"",
|
|
2131
|
+
"Work log parent selectors:",
|
|
2132
|
+
" --under ticket-last attaches under the latest work-log entry on the Ticket.",
|
|
2133
|
+
" --under session-last attaches under the latest entry in --session.",
|
|
2134
|
+
" --under last-decision|last-correction|last-action attaches under the latest matching kind.",
|
|
2135
|
+
" --under last is only valid with --session; without a session use ticket-last or a kind selector.",
|
|
2136
|
+
"",
|
|
1847
2137
|
"Auth options:",
|
|
1848
|
-
" --
|
|
2138
|
+
" --web-base-url <url> Browser login base URL"
|
|
1849
2139
|
].join(`
|
|
1850
2140
|
`);
|
|
1851
2141
|
}
|
|
2142
|
+
function credentialExpiresSoon(credential, now = new Date) {
|
|
2143
|
+
if (!credential.expiresAt) {
|
|
2144
|
+
return false;
|
|
2145
|
+
}
|
|
2146
|
+
const expiresAt = new Date(credential.expiresAt).getTime();
|
|
2147
|
+
if (Number.isNaN(expiresAt)) {
|
|
2148
|
+
return false;
|
|
2149
|
+
}
|
|
2150
|
+
return expiresAt <= now.getTime() + credentialRefreshSkewMs;
|
|
2151
|
+
}
|
|
2152
|
+
async function readFreshAccessToken(config, store, authClient) {
|
|
2153
|
+
const credential = await readStoredCredential(store, config.server);
|
|
2154
|
+
if (!credential) {
|
|
2155
|
+
return "";
|
|
2156
|
+
}
|
|
2157
|
+
if (!credentialExpiresSoon(credential)) {
|
|
2158
|
+
return credential.accessToken;
|
|
2159
|
+
}
|
|
2160
|
+
const lock = await acquireCredentialRefreshLock(config);
|
|
2161
|
+
try {
|
|
2162
|
+
const lockedCredential = await readStoredCredential(store, config.server);
|
|
2163
|
+
if (!lockedCredential) {
|
|
2164
|
+
return "";
|
|
2165
|
+
}
|
|
2166
|
+
if (!credentialExpiresSoon(lockedCredential)) {
|
|
2167
|
+
return lockedCredential.accessToken;
|
|
2168
|
+
}
|
|
2169
|
+
if (!lockedCredential.refreshToken) {
|
|
2170
|
+
throw new CliError("AUTH_REFRESH_REQUIRED", "Stored Cadence credential is expired and cannot be refreshed because no refresh token is stored.");
|
|
2171
|
+
}
|
|
2172
|
+
try {
|
|
2173
|
+
const refreshed = await authClient.auth.cli.refresh({
|
|
2174
|
+
refreshToken: lockedCredential.refreshToken
|
|
2175
|
+
});
|
|
2176
|
+
await store.setCredential(config.server, encodeCredential(refreshed));
|
|
2177
|
+
return refreshed.accessToken;
|
|
2178
|
+
} catch (error) {
|
|
2179
|
+
const recoveredCredential = await readStoredCredential(store, config.server);
|
|
2180
|
+
if (recoveredCredential && !credentialExpiresSoon(recoveredCredential)) {
|
|
2181
|
+
return recoveredCredential.accessToken;
|
|
2182
|
+
}
|
|
2183
|
+
throw new CliError("AUTH_REFRESH_FAILED", "Stored Cadence credential is expired and refresh failed. Run `cadence auth login` to reconnect.", {
|
|
2184
|
+
server: config.server,
|
|
2185
|
+
cause: error instanceof Error ? error.message : "Credential refresh failed."
|
|
2186
|
+
});
|
|
2187
|
+
}
|
|
2188
|
+
} finally {
|
|
2189
|
+
await releaseCredentialRefreshLock(lock);
|
|
2190
|
+
}
|
|
2191
|
+
}
|
|
1852
2192
|
async function createClient(config, options) {
|
|
1853
2193
|
if (options.client) {
|
|
1854
2194
|
return options.client;
|
|
1855
2195
|
}
|
|
1856
2196
|
const store = getCredentialStore(options);
|
|
1857
2197
|
const credential = await readStoredCredential(store, config.server);
|
|
2198
|
+
const authClient = createCadenceClient({
|
|
2199
|
+
baseUrl: config.server,
|
|
2200
|
+
...options.fetch ? { fetch: options.fetch } : {}
|
|
2201
|
+
});
|
|
1858
2202
|
return createCadenceClient({
|
|
1859
2203
|
baseUrl: config.server,
|
|
1860
|
-
...credential ? { getAuthToken: async () => (
|
|
2204
|
+
...credential ? { getAuthToken: async () => readFreshAccessToken(config, store, authClient) } : {},
|
|
1861
2205
|
...options.fetch ? { fetch: options.fetch } : {}
|
|
1862
2206
|
});
|
|
1863
2207
|
}
|
|
@@ -1865,6 +2209,7 @@ function commandMeta(parsed, config) {
|
|
|
1865
2209
|
return {
|
|
1866
2210
|
command: parsed.command.name,
|
|
1867
2211
|
server: config.server,
|
|
2212
|
+
...config.profile ? { profile: config.profile } : {},
|
|
1868
2213
|
projectId: config.projectId
|
|
1869
2214
|
};
|
|
1870
2215
|
}
|
|
@@ -1926,6 +2271,55 @@ function parseTicketStatus(value) {
|
|
|
1926
2271
|
}
|
|
1927
2272
|
return value;
|
|
1928
2273
|
}
|
|
2274
|
+
function parseSessionFileChangeKind(value) {
|
|
2275
|
+
if (!value) {
|
|
2276
|
+
return "unknown";
|
|
2277
|
+
}
|
|
2278
|
+
if (sessionFileChangeKinds.includes(value)) {
|
|
2279
|
+
return value;
|
|
2280
|
+
}
|
|
2281
|
+
throw new CliError("CLI_USAGE", "--kind must be one of added, modified, deleted, renamed, or unknown.");
|
|
2282
|
+
}
|
|
2283
|
+
function parseChangeSetPrNoteSource(value) {
|
|
2284
|
+
if (!value) {
|
|
2285
|
+
return;
|
|
2286
|
+
}
|
|
2287
|
+
if (changesetPrNoteSources.includes(value)) {
|
|
2288
|
+
return value;
|
|
2289
|
+
}
|
|
2290
|
+
throw new CliError("CLI_USAGE", "--source must be one of agent, human, system.");
|
|
2291
|
+
}
|
|
2292
|
+
function parseWorkLogEntryKind(value) {
|
|
2293
|
+
if (!value) {
|
|
2294
|
+
throw new CliError("CLI_USAGE", "--kind must be one of intent, decision, rationale, action, verification, blocker, correction, or note.");
|
|
2295
|
+
}
|
|
2296
|
+
if (workLogEntryKinds.includes(value)) {
|
|
2297
|
+
return value;
|
|
2298
|
+
}
|
|
2299
|
+
throw new CliError("CLI_USAGE", "--kind must be one of intent, decision, rationale, action, verification, blocker, correction, or note.");
|
|
2300
|
+
}
|
|
2301
|
+
var uuidPattern = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
|
|
2302
|
+
function parseWorkLogParent(value) {
|
|
2303
|
+
if (!value) {
|
|
2304
|
+
return {};
|
|
2305
|
+
}
|
|
2306
|
+
if (uuidPattern.test(value)) {
|
|
2307
|
+
return { parentEntryId: value };
|
|
2308
|
+
}
|
|
2309
|
+
if (workLogParentSelectors.includes(value)) {
|
|
2310
|
+
return { parentSelector: value };
|
|
2311
|
+
}
|
|
2312
|
+
throw new CliError("CLI_USAGE", "--under must be a work-log entry UUID or one of ticket-last, session-last, last-decision, last-correction, last-action, or last.");
|
|
2313
|
+
}
|
|
2314
|
+
function parseSessionFilePaths(parsed) {
|
|
2315
|
+
const value = requireOption(parsed, "file");
|
|
2316
|
+
const paths = value.split(`
|
|
2317
|
+
`).map((path) => path.trim()).filter(Boolean);
|
|
2318
|
+
if (paths.length === 0) {
|
|
2319
|
+
throw new CliError("CLI_USAGE", "sessions files requires at least one --file.");
|
|
2320
|
+
}
|
|
2321
|
+
return paths;
|
|
2322
|
+
}
|
|
1929
2323
|
function leaseExpiresAt(ttlSeconds) {
|
|
1930
2324
|
return new Date(Date.now() + ttlSeconds * 1000).toISOString();
|
|
1931
2325
|
}
|
|
@@ -1938,20 +2332,73 @@ function commandMetadata() {
|
|
|
1938
2332
|
source: "cli"
|
|
1939
2333
|
};
|
|
1940
2334
|
}
|
|
2335
|
+
function notesCommandMetadata() {
|
|
2336
|
+
return {
|
|
2337
|
+
occurredAt: new Date().toISOString(),
|
|
2338
|
+
eventSource: "cli"
|
|
2339
|
+
};
|
|
2340
|
+
}
|
|
2341
|
+
async function resolveCurrentBranch(options) {
|
|
2342
|
+
const injected = await options.resolveCurrentBranch?.();
|
|
2343
|
+
if (injected !== undefined) {
|
|
2344
|
+
const branch2 = injected.trim();
|
|
2345
|
+
if (!branch2) {
|
|
2346
|
+
throw new CliError("GIT_BRANCH_ERROR", "Current git branch is empty.");
|
|
2347
|
+
}
|
|
2348
|
+
return branch2;
|
|
2349
|
+
}
|
|
2350
|
+
const result = spawnSync("git", ["branch", "--show-current"], {
|
|
2351
|
+
cwd: options.cwd,
|
|
2352
|
+
encoding: "utf8"
|
|
2353
|
+
});
|
|
2354
|
+
if (result.status !== 0) {
|
|
2355
|
+
throw new CliError("GIT_BRANCH_ERROR", "Failed to resolve current git branch.", {
|
|
2356
|
+
stderr: result.stderr?.trim() ?? ""
|
|
2357
|
+
});
|
|
2358
|
+
}
|
|
2359
|
+
const branch = result.stdout.trim();
|
|
2360
|
+
if (!branch) {
|
|
2361
|
+
throw new CliError("GIT_BRANCH_ERROR", "Current git branch is empty.");
|
|
2362
|
+
}
|
|
2363
|
+
return branch;
|
|
2364
|
+
}
|
|
2365
|
+
async function changesetLookupFromOptions(parsed, options) {
|
|
2366
|
+
if (parsed.options.changeset && parsed.options.branch) {
|
|
2367
|
+
throw new CliError("CLI_USAGE", "Use either --changeset or --branch, not both.");
|
|
2368
|
+
}
|
|
2369
|
+
if (parsed.options.changeset) {
|
|
2370
|
+
return {
|
|
2371
|
+
changesetId: parsed.options.changeset
|
|
2372
|
+
};
|
|
2373
|
+
}
|
|
2374
|
+
const branchOption = parsed.options.branch ?? "current";
|
|
2375
|
+
const branchName = branchOption === "current" ? await resolveCurrentBranch(options) : branchOption;
|
|
2376
|
+
return {
|
|
2377
|
+
branchName
|
|
2378
|
+
};
|
|
2379
|
+
}
|
|
2380
|
+
async function readBodyFile(path, options) {
|
|
2381
|
+
const resolvedPath = isAbsolute(path) ? path : join(options.cwd ?? process.cwd(), path);
|
|
2382
|
+
return readFile(resolvedPath, "utf8");
|
|
2383
|
+
}
|
|
1941
2384
|
async function runStatus(parsed, options) {
|
|
1942
2385
|
const config = await resolveCliConfig(parsed.flags, options);
|
|
1943
2386
|
const store = getCredentialStore(options);
|
|
1944
2387
|
const credential = await readStoredCredential(store, config.server);
|
|
1945
2388
|
const client = await createClient(config, options);
|
|
1946
2389
|
const health = await client.health();
|
|
2390
|
+
const webBaseUrl = getCliWebBaseUrl(config, parsed, options);
|
|
1947
2391
|
const data = {
|
|
1948
2392
|
server: config.server,
|
|
2393
|
+
webBaseUrl,
|
|
2394
|
+
profile: config.profile,
|
|
1949
2395
|
projectId: config.projectId,
|
|
1950
2396
|
credentialConfigured: Boolean(credential),
|
|
1951
2397
|
authTokenConfigured: Boolean(credential),
|
|
1952
2398
|
health,
|
|
1953
2399
|
config: {
|
|
1954
2400
|
repoConfigPath: config.repoConfigPath,
|
|
2401
|
+
localConfigPath: config.localConfigPath,
|
|
1955
2402
|
globalConfigPath: config.globalConfigPath
|
|
1956
2403
|
}
|
|
1957
2404
|
};
|
|
@@ -1973,25 +2420,28 @@ async function runStatus(parsed, options) {
|
|
|
1973
2420
|
async function runInit(parsed, options) {
|
|
1974
2421
|
const cwd = options.cwd ?? process.cwd();
|
|
1975
2422
|
const repoConfigPath = join(cwd, ".cadence", "config.json");
|
|
1976
|
-
const
|
|
1977
|
-
|
|
1978
|
-
|
|
1979
|
-
}
|
|
2423
|
+
const config = await resolveCliConfig(parsed.flags, {
|
|
2424
|
+
...options,
|
|
2425
|
+
cwd
|
|
2426
|
+
});
|
|
2427
|
+
const client = await createClient(config, options);
|
|
2428
|
+
const project = await selectProject(parsed, client, options);
|
|
2429
|
+
const projectId = project.id;
|
|
1980
2430
|
const updates = {
|
|
1981
2431
|
projectId,
|
|
1982
2432
|
...parsed.flags.server ? { server: parsed.flags.server } : {}
|
|
1983
2433
|
};
|
|
1984
2434
|
await mergeConfigFile(repoConfigPath, updates);
|
|
1985
|
-
const config = await resolveCliConfig(parsed.flags, {
|
|
1986
|
-
...options,
|
|
1987
|
-
cwd
|
|
1988
|
-
});
|
|
1989
2435
|
const data = {
|
|
1990
2436
|
repoConfigPath,
|
|
1991
2437
|
projectId,
|
|
2438
|
+
project,
|
|
1992
2439
|
...parsed.flags.server ? { server: parsed.flags.server } : {}
|
|
1993
2440
|
};
|
|
1994
|
-
const meta = commandMeta(parsed,
|
|
2441
|
+
const meta = commandMeta(parsed, {
|
|
2442
|
+
...config,
|
|
2443
|
+
projectId
|
|
2444
|
+
});
|
|
1995
2445
|
if (parsed.flags.json) {
|
|
1996
2446
|
return {
|
|
1997
2447
|
stdout: formatJson(successEnvelope(data, meta)),
|
|
@@ -2006,6 +2456,69 @@ async function runInit(parsed, options) {
|
|
|
2006
2456
|
exitCode: 0
|
|
2007
2457
|
};
|
|
2008
2458
|
}
|
|
2459
|
+
function isUuid(value) {
|
|
2460
|
+
return /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test(value);
|
|
2461
|
+
}
|
|
2462
|
+
async function resolveProjectReference(projectReference, client) {
|
|
2463
|
+
if (isUuid(projectReference)) {
|
|
2464
|
+
return {
|
|
2465
|
+
id: projectReference,
|
|
2466
|
+
orgSlug: null,
|
|
2467
|
+
projectSlug: null,
|
|
2468
|
+
name: null
|
|
2469
|
+
};
|
|
2470
|
+
}
|
|
2471
|
+
const [orgSlug, projectSlug, extra] = projectReference.split("/");
|
|
2472
|
+
if (!orgSlug || !projectSlug || extra) {
|
|
2473
|
+
throw new CliError("CLI_USAGE", "--project must be a project ID or org/project slug.");
|
|
2474
|
+
}
|
|
2475
|
+
return await client.projects.resolve({
|
|
2476
|
+
orgSlug,
|
|
2477
|
+
projectSlug
|
|
2478
|
+
});
|
|
2479
|
+
}
|
|
2480
|
+
function formatProjectChoice(project, index) {
|
|
2481
|
+
const slug = project.orgSlug && project.projectSlug ? `${project.orgSlug}/${project.projectSlug}` : project.id;
|
|
2482
|
+
const name = project.name ? ` ${project.name}` : "";
|
|
2483
|
+
return `${index + 1}. ${slug}${name}`;
|
|
2484
|
+
}
|
|
2485
|
+
async function selectProject(parsed, client, options) {
|
|
2486
|
+
if (parsed.flags.project) {
|
|
2487
|
+
return await resolveProjectReference(parsed.flags.project, client);
|
|
2488
|
+
}
|
|
2489
|
+
const projects = await client.projects.list();
|
|
2490
|
+
if (projects.length === 1) {
|
|
2491
|
+
return projects[0];
|
|
2492
|
+
}
|
|
2493
|
+
if (parsed.flags.json || !isInteractive(options)) {
|
|
2494
|
+
throw new CliError("PROJECT_REQUIRED", "Choose a Cadence project with --project.", {
|
|
2495
|
+
projects
|
|
2496
|
+
});
|
|
2497
|
+
}
|
|
2498
|
+
if (projects.length === 0) {
|
|
2499
|
+
throw new CliError("PROJECT_REQUIRED", "No accessible Cadence projects were found.");
|
|
2500
|
+
}
|
|
2501
|
+
const message = [
|
|
2502
|
+
"Choose a Cadence project:",
|
|
2503
|
+
...projects.map(formatProjectChoice),
|
|
2504
|
+
"",
|
|
2505
|
+
"Project number, ID, or org/project: "
|
|
2506
|
+
].join(`
|
|
2507
|
+
`);
|
|
2508
|
+
const answer = (await readPromptText(options, message)).trim();
|
|
2509
|
+
const selectedIndex = Number(answer);
|
|
2510
|
+
const selected = Number.isInteger(selectedIndex) && selectedIndex > 0 ? projects[selectedIndex - 1] : undefined;
|
|
2511
|
+
if (selected) {
|
|
2512
|
+
return selected;
|
|
2513
|
+
}
|
|
2514
|
+
const matching = projects.find((project) => project.id === answer || `${project.orgSlug}/${project.projectSlug}` === answer);
|
|
2515
|
+
if (!matching) {
|
|
2516
|
+
throw new CliError("PROJECT_REQUIRED", "Selected project was not in the accessible project list.", {
|
|
2517
|
+
projects
|
|
2518
|
+
});
|
|
2519
|
+
}
|
|
2520
|
+
return matching;
|
|
2521
|
+
}
|
|
2009
2522
|
async function runAuthCommand(parsed, options) {
|
|
2010
2523
|
const config = await resolveCliConfig(parsed.flags, options);
|
|
2011
2524
|
const store = getCredentialStore(options);
|
|
@@ -2014,18 +2527,48 @@ async function runAuthCommand(parsed, options) {
|
|
|
2014
2527
|
switch (parsed.command.name) {
|
|
2015
2528
|
case "auth.login":
|
|
2016
2529
|
{
|
|
2017
|
-
await requireCredentialStore(store);
|
|
2018
2530
|
const client = await createClient(config, options);
|
|
2019
|
-
const
|
|
2020
|
-
|
|
2021
|
-
password: await readPromptSecret(options, "Password: ")
|
|
2531
|
+
const challenge = await client.auth.cli.start({
|
|
2532
|
+
loginBaseUrl: getCliWebBaseUrl(config, parsed, options)
|
|
2022
2533
|
});
|
|
2023
|
-
|
|
2534
|
+
if (parsed.flags.json || !isInteractive(options)) {
|
|
2535
|
+
throw new CliError("HUMAN_AUTH_REQUIRED", "Cadence login must be completed by a human in the browser.", {
|
|
2536
|
+
loginUrl: challenge.loginUrl,
|
|
2537
|
+
deviceCode: challenge.deviceCode,
|
|
2538
|
+
expiresAt: challenge.expiresAt
|
|
2539
|
+
});
|
|
2540
|
+
}
|
|
2541
|
+
await requireCredentialStore(store);
|
|
2542
|
+
await writeInteractiveStatus("Opening browser to approve Cadence CLI access.", options);
|
|
2543
|
+
await writeInteractiveStatus(`Verification code: ${challenge.deviceCode}`, options);
|
|
2544
|
+
await writeInteractiveStatus(`Login URL: ${challenge.loginUrl}`, options);
|
|
2545
|
+
await openBrowser(challenge.loginUrl, options);
|
|
2546
|
+
await writeInteractiveStatus("Waiting for browser approval...", options);
|
|
2547
|
+
let poll = await client.auth.cli.poll({
|
|
2548
|
+
deviceId: challenge.deviceId,
|
|
2549
|
+
deviceCode: challenge.deviceCode
|
|
2550
|
+
});
|
|
2551
|
+
while (poll.status === "pending") {
|
|
2552
|
+
await sleep2(poll.pollIntervalSeconds * 1000, options);
|
|
2553
|
+
poll = await client.auth.cli.poll({
|
|
2554
|
+
deviceId: challenge.deviceId,
|
|
2555
|
+
deviceCode: challenge.deviceCode
|
|
2556
|
+
});
|
|
2557
|
+
}
|
|
2558
|
+
if (poll.status === "expired") {
|
|
2559
|
+
throw new CliError("AUTH_LOGIN_EXPIRED", "Cadence browser login expired.");
|
|
2560
|
+
}
|
|
2561
|
+
if (poll.status === "denied") {
|
|
2562
|
+
throw new CliError("AUTH_LOGIN_DENIED", "Cadence browser login was denied.");
|
|
2563
|
+
}
|
|
2564
|
+
await store.setCredential(config.server, encodeCredential(poll.credential));
|
|
2024
2565
|
await mergeConfigFile(config.globalConfigPath, { server: config.server });
|
|
2566
|
+
await writeInteractiveStatus("Cadence CLI login approved. Credential stored.", options);
|
|
2025
2567
|
data = {
|
|
2026
2568
|
server: config.server,
|
|
2027
2569
|
credentialStored: true,
|
|
2028
|
-
globalConfigPath: config.globalConfigPath
|
|
2570
|
+
globalConfigPath: config.globalConfigPath,
|
|
2571
|
+
loginUrl: challenge.loginUrl
|
|
2029
2572
|
};
|
|
2030
2573
|
}
|
|
2031
2574
|
break;
|
|
@@ -2125,6 +2668,12 @@ async function runReadCommand(parsed, options) {
|
|
|
2125
2668
|
changesetId: requireArg(parsed, 0, "<changeset-id>")
|
|
2126
2669
|
});
|
|
2127
2670
|
break;
|
|
2671
|
+
case "changesets.current":
|
|
2672
|
+
data = await client.changesets.context({
|
|
2673
|
+
projectId,
|
|
2674
|
+
query: await changesetLookupFromOptions(parsed, options)
|
|
2675
|
+
});
|
|
2676
|
+
break;
|
|
2128
2677
|
case "changesets.list":
|
|
2129
2678
|
data = await client.changesets.list({
|
|
2130
2679
|
projectId,
|
|
@@ -2136,6 +2685,64 @@ async function runReadCommand(parsed, options) {
|
|
|
2136
2685
|
})
|
|
2137
2686
|
});
|
|
2138
2687
|
break;
|
|
2688
|
+
case "changesets.notes.get":
|
|
2689
|
+
data = await client.changesets.notes.get({
|
|
2690
|
+
projectId,
|
|
2691
|
+
query: await changesetLookupFromOptions(parsed, options)
|
|
2692
|
+
});
|
|
2693
|
+
break;
|
|
2694
|
+
default:
|
|
2695
|
+
throw new CliError("CLI_USAGE", `Unknown command: ${parsed.command.path.join(" ")}`);
|
|
2696
|
+
}
|
|
2697
|
+
if (parsed.flags.json) {
|
|
2698
|
+
return {
|
|
2699
|
+
stdout: formatJson(successEnvelope(data, meta)),
|
|
2700
|
+
stderr: "",
|
|
2701
|
+
exitCode: 0
|
|
2702
|
+
};
|
|
2703
|
+
}
|
|
2704
|
+
return {
|
|
2705
|
+
stdout: `${JSON.stringify(data, null, 2)}
|
|
2706
|
+
`,
|
|
2707
|
+
stderr: "",
|
|
2708
|
+
exitCode: 0
|
|
2709
|
+
};
|
|
2710
|
+
}
|
|
2711
|
+
async function runProjectCommand(parsed, options) {
|
|
2712
|
+
const config = await resolveCliConfig(parsed.flags, options);
|
|
2713
|
+
const client = await createClient(config, options);
|
|
2714
|
+
const meta = commandMeta(parsed, config);
|
|
2715
|
+
let data;
|
|
2716
|
+
switch (parsed.command.name) {
|
|
2717
|
+
case "projects.list":
|
|
2718
|
+
data = await client.projects.list();
|
|
2719
|
+
break;
|
|
2720
|
+
default:
|
|
2721
|
+
throw new CliError("CLI_USAGE", `Unknown command: ${parsed.command.path.join(" ")}`);
|
|
2722
|
+
}
|
|
2723
|
+
if (parsed.flags.json) {
|
|
2724
|
+
return {
|
|
2725
|
+
stdout: formatJson(successEnvelope(data, meta)),
|
|
2726
|
+
stderr: "",
|
|
2727
|
+
exitCode: 0
|
|
2728
|
+
};
|
|
2729
|
+
}
|
|
2730
|
+
return {
|
|
2731
|
+
stdout: `${JSON.stringify(data, null, 2)}
|
|
2732
|
+
`,
|
|
2733
|
+
stderr: "",
|
|
2734
|
+
exitCode: 0
|
|
2735
|
+
};
|
|
2736
|
+
}
|
|
2737
|
+
async function runActorCommand(parsed, options) {
|
|
2738
|
+
const config = await resolveCliConfig(parsed.flags, options);
|
|
2739
|
+
const client = await createClient(config, options);
|
|
2740
|
+
const meta = commandMeta(parsed, config);
|
|
2741
|
+
let data;
|
|
2742
|
+
switch (parsed.command.name) {
|
|
2743
|
+
case "actors.ensure-workspace-agent":
|
|
2744
|
+
data = await ensureWorkspaceAgentIdentity(parsed, config, client, options);
|
|
2745
|
+
break;
|
|
2139
2746
|
default:
|
|
2140
2747
|
throw new CliError("CLI_USAGE", `Unknown command: ${parsed.command.path.join(" ")}`);
|
|
2141
2748
|
}
|
|
@@ -2249,6 +2856,7 @@ async function runIntakeCommand(parsed, options) {
|
|
|
2249
2856
|
ticketId: requireArg(parsed, 0, "<ticket-id>"),
|
|
2250
2857
|
sessionId: requireOption(parsed, "session"),
|
|
2251
2858
|
...parsed.options.changeset ? { changesetId: parsed.options.changeset } : {},
|
|
2859
|
+
...parsed.options.actor ? { actorId: parsed.options.actor } : {},
|
|
2252
2860
|
expiresAt: leaseExpiresAt(parsePositiveInteger(parsed.options["ttl-seconds"], "--ttl-seconds") ?? defaultLeaseTtlSeconds),
|
|
2253
2861
|
...commandMetadata()
|
|
2254
2862
|
}
|
|
@@ -2265,6 +2873,21 @@ async function runIntakeCommand(parsed, options) {
|
|
|
2265
2873
|
}
|
|
2266
2874
|
});
|
|
2267
2875
|
break;
|
|
2876
|
+
case "tickets.log":
|
|
2877
|
+
data = await client.tickets.log({
|
|
2878
|
+
projectId,
|
|
2879
|
+
ticketId: requireArg(parsed, 0, "<ticket-id>"),
|
|
2880
|
+
entry: {
|
|
2881
|
+
entryKind: parseWorkLogEntryKind(parsed.options.kind),
|
|
2882
|
+
body: requireOption(parsed, "body"),
|
|
2883
|
+
...parsed.options.summary ? { summary: parsed.options.summary } : {},
|
|
2884
|
+
...parseWorkLogParent(parsed.options.under),
|
|
2885
|
+
...parsed.options.session ? { sessionId: parsed.options.session } : {},
|
|
2886
|
+
...parsed.options.changeset ? { changesetId: parsed.options.changeset } : {},
|
|
2887
|
+
...commandMetadata()
|
|
2888
|
+
}
|
|
2889
|
+
});
|
|
2890
|
+
break;
|
|
2268
2891
|
case "tickets.complete":
|
|
2269
2892
|
data = await client.tickets.complete({
|
|
2270
2893
|
projectId,
|
|
@@ -2282,6 +2905,7 @@ async function runIntakeCommand(parsed, options) {
|
|
|
2282
2905
|
session: {
|
|
2283
2906
|
ticketId: requireOption(parsed, "ticket"),
|
|
2284
2907
|
...parsed.options.changeset ? { changesetId: parsed.options.changeset } : {},
|
|
2908
|
+
...parsed.options.actor ? { actorId: parsed.options.actor } : {},
|
|
2285
2909
|
...parsed.options["local-session-ref"] ? { localSessionRef: parsed.options["local-session-ref"] } : {},
|
|
2286
2910
|
...commandMetadata()
|
|
2287
2911
|
}
|
|
@@ -2297,6 +2921,23 @@ async function runIntakeCommand(parsed, options) {
|
|
|
2297
2921
|
}
|
|
2298
2922
|
});
|
|
2299
2923
|
break;
|
|
2924
|
+
case "sessions.files":
|
|
2925
|
+
{
|
|
2926
|
+
const changeKind = parseSessionFileChangeKind(parsed.options.kind);
|
|
2927
|
+
data = await client.sessions.files({
|
|
2928
|
+
projectId,
|
|
2929
|
+
sessionId: requireArg(parsed, 0, "<session-id>"),
|
|
2930
|
+
files: {
|
|
2931
|
+
...parsed.options.changeset ? { changesetId: parsed.options.changeset } : {},
|
|
2932
|
+
files: parseSessionFilePaths(parsed).map((path) => ({
|
|
2933
|
+
path,
|
|
2934
|
+
changeKind
|
|
2935
|
+
})),
|
|
2936
|
+
...commandMetadata()
|
|
2937
|
+
}
|
|
2938
|
+
});
|
|
2939
|
+
}
|
|
2940
|
+
break;
|
|
2300
2941
|
case "changesets.create":
|
|
2301
2942
|
data = await client.changesets.create({
|
|
2302
2943
|
projectId,
|
|
@@ -2309,6 +2950,41 @@ async function runIntakeCommand(parsed, options) {
|
|
|
2309
2950
|
}
|
|
2310
2951
|
});
|
|
2311
2952
|
break;
|
|
2953
|
+
case "changesets.notes.put":
|
|
2954
|
+
{
|
|
2955
|
+
const source = parseChangeSetPrNoteSource(parsed.options.source);
|
|
2956
|
+
data = await client.changesets.notes.put({
|
|
2957
|
+
projectId,
|
|
2958
|
+
query: await changesetLookupFromOptions(parsed, options),
|
|
2959
|
+
notes: {
|
|
2960
|
+
title: requireOption(parsed, "title"),
|
|
2961
|
+
body: await readBodyFile(requireOption(parsed, "body-file"), options),
|
|
2962
|
+
...source ? { source } : {},
|
|
2963
|
+
...parsed.options.provider ? { prProvider: parsed.options.provider } : {},
|
|
2964
|
+
...parsed.options["pr-number"] ? { prNumber: parseRequiredPositiveInteger(parsed.options["pr-number"], "--pr-number") } : {},
|
|
2965
|
+
...parsed.options["pr-url"] ? { prUrl: parsed.options["pr-url"] } : {},
|
|
2966
|
+
...parsed.options["base-branch"] ? { baseBranch: parsed.options["base-branch"] } : {},
|
|
2967
|
+
...parsed.options["head-branch"] ? { headBranch: parsed.options["head-branch"] } : {},
|
|
2968
|
+
...parsed.options["base-sha"] ? { baseSha: parsed.options["base-sha"] } : {},
|
|
2969
|
+
...parsed.options["head-sha"] ? { headSha: parsed.options["head-sha"] } : {},
|
|
2970
|
+
...notesCommandMetadata()
|
|
2971
|
+
}
|
|
2972
|
+
});
|
|
2973
|
+
}
|
|
2974
|
+
break;
|
|
2975
|
+
case "changesets.notes.apply":
|
|
2976
|
+
data = await client.changesets.notes.apply({
|
|
2977
|
+
projectId,
|
|
2978
|
+
query: await changesetLookupFromOptions(parsed, options),
|
|
2979
|
+
notes: {
|
|
2980
|
+
prProvider: requireOption(parsed, "provider"),
|
|
2981
|
+
prNumber: parseRequiredPositiveInteger(requireOption(parsed, "pr-number"), "--pr-number"),
|
|
2982
|
+
prUrl: requireOption(parsed, "pr-url"),
|
|
2983
|
+
...parsed.options["head-sha"] ? { headSha: parsed.options["head-sha"] } : {},
|
|
2984
|
+
...notesCommandMetadata()
|
|
2985
|
+
}
|
|
2986
|
+
});
|
|
2987
|
+
break;
|
|
2312
2988
|
default:
|
|
2313
2989
|
throw new CliError("CLI_USAGE", `Unknown command: ${parsed.command.path.join(" ")}`);
|
|
2314
2990
|
}
|
|
@@ -2370,13 +3046,19 @@ async function runCli(argv, options = {}) {
|
|
|
2370
3046
|
if (parsed.command.name === "init") {
|
|
2371
3047
|
return await runInit(parsed, options);
|
|
2372
3048
|
}
|
|
3049
|
+
if (parsed.command.name === "actors.ensure-workspace-agent") {
|
|
3050
|
+
return await runActorCommand(parsed, options);
|
|
3051
|
+
}
|
|
2373
3052
|
if (parsed.command.name === "auth.login" || parsed.command.name === "auth.status" || parsed.command.name === "auth.logout") {
|
|
2374
3053
|
return await runAuthCommand(parsed, options);
|
|
2375
3054
|
}
|
|
2376
|
-
if (parsed.command.name === "events.list" || parsed.command.name === "work.overview" || parsed.command.name === "tickets.get" || parsed.command.name === "tickets.list" || parsed.command.name === "sessions.current" || parsed.command.name === "changesets.get" || parsed.command.name === "changesets.list") {
|
|
3055
|
+
if (parsed.command.name === "events.list" || parsed.command.name === "work.overview" || parsed.command.name === "tickets.get" || parsed.command.name === "tickets.list" || parsed.command.name === "sessions.current" || parsed.command.name === "changesets.get" || parsed.command.name === "changesets.current" || parsed.command.name === "changesets.list" || parsed.command.name === "changesets.notes.get") {
|
|
2377
3056
|
return await runReadCommand(parsed, options);
|
|
2378
3057
|
}
|
|
2379
|
-
if (parsed.command.name === "
|
|
3058
|
+
if (parsed.command.name === "projects.list") {
|
|
3059
|
+
return await runProjectCommand(parsed, options);
|
|
3060
|
+
}
|
|
3061
|
+
if (parsed.command.name === "intake" || parsed.command.name === "intake.dismiss" || parsed.command.name === "tickets.attach" || parsed.command.name === "tickets.create" || parsed.command.name === "tickets.update" || parsed.command.name === "tickets.claim" || parsed.command.name === "tickets.release" || parsed.command.name === "tickets.log" || parsed.command.name === "tickets.complete" || parsed.command.name === "sessions.start" || parsed.command.name === "sessions.end" || parsed.command.name === "sessions.files" || parsed.command.name === "changesets.create" || parsed.command.name === "changesets.notes.put" || parsed.command.name === "changesets.notes.apply") {
|
|
2380
3062
|
return await runIntakeCommand(parsed, options);
|
|
2381
3063
|
}
|
|
2382
3064
|
throw new CliError("CLI_USAGE", `Unknown command: ${parsed.command.path.join(" ")}`);
|