@cleocode/caamp 2026.4.6 → 2026.4.9
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 +9 -8
- package/dist/{chunk-43GULI6J.js → chunk-JC77OAHA.js} +1766 -400
- package/dist/chunk-JC77OAHA.js.map +1 -0
- package/dist/cli.js +1420 -71
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +1860 -293
- package/dist/index.js +29 -1
- package/dist/index.js.map +1 -1
- package/package.json +3 -3
- package/dist/chunk-43GULI6J.js.map +0 -1
package/dist/cli.js
CHANGED
|
@@ -2,14 +2,17 @@
|
|
|
2
2
|
import {
|
|
3
3
|
CANONICAL_SKILLS_DIR,
|
|
4
4
|
MarketplaceClient,
|
|
5
|
+
PiHarness,
|
|
5
6
|
RECOMMENDATION_ERROR_CODES,
|
|
6
7
|
checkSkillUpdate,
|
|
7
8
|
detectAllProviders,
|
|
9
|
+
detectMcpInstallations,
|
|
8
10
|
detectProjectProviders,
|
|
9
11
|
discoverSkill,
|
|
10
12
|
discoverSkillsMulti,
|
|
11
13
|
dispatchInstallSkillAcrossProviders,
|
|
12
14
|
dispatchRemoveSkillAcrossProviders,
|
|
15
|
+
fetchWithTimeout,
|
|
13
16
|
formatNetworkError,
|
|
14
17
|
formatSkillRecommendations,
|
|
15
18
|
getHarnessFor,
|
|
@@ -18,12 +21,15 @@ import {
|
|
|
18
21
|
getSkillDir,
|
|
19
22
|
getTrackedSkills,
|
|
20
23
|
installBatchWithRollback,
|
|
24
|
+
installMcpServer,
|
|
21
25
|
isCatalogAvailable,
|
|
22
26
|
isHuman,
|
|
23
27
|
isMarketplaceScoped,
|
|
24
28
|
isQuiet,
|
|
25
29
|
isVerbose,
|
|
30
|
+
listAllMcpServers,
|
|
26
31
|
listCanonicalSkills,
|
|
32
|
+
listMcpServers,
|
|
27
33
|
listProfiles,
|
|
28
34
|
listSkills,
|
|
29
35
|
parseSource,
|
|
@@ -31,6 +37,8 @@ import {
|
|
|
31
37
|
readLockFile,
|
|
32
38
|
recommendSkills,
|
|
33
39
|
recordSkillInstall,
|
|
40
|
+
removeMcpServer,
|
|
41
|
+
removeMcpServerFromAll,
|
|
34
42
|
removeSkillFromLock,
|
|
35
43
|
resolveDefaultTargetProviders,
|
|
36
44
|
resolveProfile,
|
|
@@ -44,7 +52,7 @@ import {
|
|
|
44
52
|
tokenizeCriteriaValue,
|
|
45
53
|
updateInstructionsSingleOperation,
|
|
46
54
|
validateSkill
|
|
47
|
-
} from "./chunk-
|
|
55
|
+
} from "./chunk-JC77OAHA.js";
|
|
48
56
|
import {
|
|
49
57
|
buildSkillsMap,
|
|
50
58
|
checkAllInjections,
|
|
@@ -1537,6 +1545,1399 @@ function registerInstructionsCommands(program2) {
|
|
|
1537
1545
|
registerInstructionsUpdate(instructions);
|
|
1538
1546
|
}
|
|
1539
1547
|
|
|
1548
|
+
// src/commands/mcp/common.ts
|
|
1549
|
+
var MCP_ERROR_CODES = {
|
|
1550
|
+
/** Caller-supplied input failed validation (shape, type, enum). */
|
|
1551
|
+
VALIDATION: "E_VALIDATION_SCHEMA",
|
|
1552
|
+
/** Referenced resource does not exist on disk or in the registry. */
|
|
1553
|
+
NOT_FOUND: "E_NOT_FOUND_RESOURCE",
|
|
1554
|
+
/** Server entry already exists and overwrite was not requested. */
|
|
1555
|
+
CONFLICT: "E_CONFLICT_VERSION",
|
|
1556
|
+
/** Upstream operation failed; retry is viable. */
|
|
1557
|
+
TRANSIENT: "E_TRANSIENT_UPSTREAM"
|
|
1558
|
+
};
|
|
1559
|
+
function requireMcpProvider(providerId) {
|
|
1560
|
+
const provider = getProvider(providerId);
|
|
1561
|
+
if (provider === void 0) {
|
|
1562
|
+
throw new LAFSCommandError(
|
|
1563
|
+
MCP_ERROR_CODES.NOT_FOUND,
|
|
1564
|
+
`Unknown provider id: ${providerId}`,
|
|
1565
|
+
"Run `caamp providers list` to see registered provider ids.",
|
|
1566
|
+
false
|
|
1567
|
+
);
|
|
1568
|
+
}
|
|
1569
|
+
if (provider.capabilities.mcp === null) {
|
|
1570
|
+
throw new LAFSCommandError(
|
|
1571
|
+
MCP_ERROR_CODES.NOT_FOUND,
|
|
1572
|
+
`Provider ${providerId} does not declare an MCP capability.`,
|
|
1573
|
+
"This provider does not consume MCP servers via a config file. Pick a different provider, or check `caamp providers list` for MCP-capable providers.",
|
|
1574
|
+
false
|
|
1575
|
+
);
|
|
1576
|
+
}
|
|
1577
|
+
return provider;
|
|
1578
|
+
}
|
|
1579
|
+
function parseScope(raw, defaultScope) {
|
|
1580
|
+
if (raw === void 0) return defaultScope;
|
|
1581
|
+
if (raw === "project" || raw === "global") return raw;
|
|
1582
|
+
throw new LAFSCommandError(
|
|
1583
|
+
MCP_ERROR_CODES.VALIDATION,
|
|
1584
|
+
`Invalid --scope value: ${raw}`,
|
|
1585
|
+
"Use one of: 'project', 'global'.",
|
|
1586
|
+
false
|
|
1587
|
+
);
|
|
1588
|
+
}
|
|
1589
|
+
function resolveProjectDir(scope, explicit) {
|
|
1590
|
+
if (scope !== "project") return void 0;
|
|
1591
|
+
if (explicit !== void 0 && explicit.length > 0) return explicit;
|
|
1592
|
+
return process.cwd();
|
|
1593
|
+
}
|
|
1594
|
+
function parseEnvAssignment(raw) {
|
|
1595
|
+
const idx = raw.indexOf("=");
|
|
1596
|
+
if (idx <= 0) {
|
|
1597
|
+
throw new LAFSCommandError(
|
|
1598
|
+
MCP_ERROR_CODES.VALIDATION,
|
|
1599
|
+
`Invalid --env value: ${raw}`,
|
|
1600
|
+
"Use KEY=VALUE format, e.g. --env GITHUB_TOKEN=ghp_...",
|
|
1601
|
+
false
|
|
1602
|
+
);
|
|
1603
|
+
}
|
|
1604
|
+
const key = raw.slice(0, idx);
|
|
1605
|
+
const value = raw.slice(idx + 1);
|
|
1606
|
+
if (key.length === 0) {
|
|
1607
|
+
throw new LAFSCommandError(
|
|
1608
|
+
MCP_ERROR_CODES.VALIDATION,
|
|
1609
|
+
`Invalid --env value: ${raw}`,
|
|
1610
|
+
"KEY must be non-empty.",
|
|
1611
|
+
false
|
|
1612
|
+
);
|
|
1613
|
+
}
|
|
1614
|
+
return [key, value];
|
|
1615
|
+
}
|
|
1616
|
+
|
|
1617
|
+
// src/commands/mcp/detect.ts
|
|
1618
|
+
function registerMcpDetectCommand(parent) {
|
|
1619
|
+
parent.command("detect").description("Detect which providers currently have MCP config files on disk").option("--scope <scope>", "Scope: project|global (default: project)").option("--project-dir <path>", "Project directory for the project scope (default: cwd)").option("--only-existing", "Only include providers whose config file exists on disk").action(
|
|
1620
|
+
async (opts) => runLafsCommand("mcp.detect", "standard", async () => {
|
|
1621
|
+
const scope = parseScope(opts.scope, "project");
|
|
1622
|
+
const projectDir = resolveProjectDir(scope, opts.projectDir);
|
|
1623
|
+
const all = await detectMcpInstallations(scope, projectDir);
|
|
1624
|
+
const filtered = opts.onlyExisting === true ? all.filter((e) => e.exists) : all;
|
|
1625
|
+
const existingCount = filtered.filter((e) => e.exists).length;
|
|
1626
|
+
const totalServers = filtered.reduce((sum, e) => sum + (e.serverCount ?? 0), 0);
|
|
1627
|
+
return {
|
|
1628
|
+
scope,
|
|
1629
|
+
providersProbed: all.length,
|
|
1630
|
+
existingCount,
|
|
1631
|
+
totalServers,
|
|
1632
|
+
entries: filtered
|
|
1633
|
+
};
|
|
1634
|
+
})
|
|
1635
|
+
);
|
|
1636
|
+
}
|
|
1637
|
+
|
|
1638
|
+
// src/commands/mcp/install.ts
|
|
1639
|
+
import { existsSync as existsSync3 } from "fs";
|
|
1640
|
+
import { readFile as readFile2 } from "fs/promises";
|
|
1641
|
+
function coerceServerConfig(value, source) {
|
|
1642
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) {
|
|
1643
|
+
throw new LAFSCommandError(
|
|
1644
|
+
MCP_ERROR_CODES.VALIDATION,
|
|
1645
|
+
`${source} did not contain a JSON object.`,
|
|
1646
|
+
"Pass an object with at least `command` or `url` (and optional `args`, `env`).",
|
|
1647
|
+
false
|
|
1648
|
+
);
|
|
1649
|
+
}
|
|
1650
|
+
const obj = value;
|
|
1651
|
+
const hasCommand = typeof obj["command"] === "string";
|
|
1652
|
+
const hasUrl = typeof obj["url"] === "string";
|
|
1653
|
+
const hasType = typeof obj["type"] === "string";
|
|
1654
|
+
if (!hasCommand && !hasUrl && !hasType) {
|
|
1655
|
+
throw new LAFSCommandError(
|
|
1656
|
+
MCP_ERROR_CODES.VALIDATION,
|
|
1657
|
+
`${source} must contain at least one of: command, url, type.`,
|
|
1658
|
+
"Provide either a stdio `command` (with optional `args`/`env`) or a remote `url`/`type`.",
|
|
1659
|
+
false
|
|
1660
|
+
);
|
|
1661
|
+
}
|
|
1662
|
+
return obj;
|
|
1663
|
+
}
|
|
1664
|
+
async function buildConfigFromOptions(inlineArgs, opts) {
|
|
1665
|
+
let base = null;
|
|
1666
|
+
if (opts.from !== void 0 && opts.from.length > 0) {
|
|
1667
|
+
if (!existsSync3(opts.from)) {
|
|
1668
|
+
throw new LAFSCommandError(
|
|
1669
|
+
MCP_ERROR_CODES.NOT_FOUND,
|
|
1670
|
+
`--from file does not exist: ${opts.from}`,
|
|
1671
|
+
"Check the path and try again.",
|
|
1672
|
+
false
|
|
1673
|
+
);
|
|
1674
|
+
}
|
|
1675
|
+
let parsed;
|
|
1676
|
+
try {
|
|
1677
|
+
const content = await readFile2(opts.from, "utf8");
|
|
1678
|
+
parsed = JSON.parse(content);
|
|
1679
|
+
} catch (err) {
|
|
1680
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
1681
|
+
throw new LAFSCommandError(
|
|
1682
|
+
MCP_ERROR_CODES.VALIDATION,
|
|
1683
|
+
`Failed to read --from JSON: ${message}`,
|
|
1684
|
+
"Ensure the file is valid JSON containing an MCP server config object.",
|
|
1685
|
+
false
|
|
1686
|
+
);
|
|
1687
|
+
}
|
|
1688
|
+
base = coerceServerConfig(parsed, `--from ${opts.from}`);
|
|
1689
|
+
}
|
|
1690
|
+
if (inlineArgs.length > 0) {
|
|
1691
|
+
const command = inlineArgs[0];
|
|
1692
|
+
if (command === void 0 || command.length === 0) {
|
|
1693
|
+
throw new LAFSCommandError(
|
|
1694
|
+
MCP_ERROR_CODES.VALIDATION,
|
|
1695
|
+
"Inline command was empty.",
|
|
1696
|
+
"Pass `--` followed by a command, e.g. `-- npx -y @mcp/server-github`.",
|
|
1697
|
+
false
|
|
1698
|
+
);
|
|
1699
|
+
}
|
|
1700
|
+
const args = inlineArgs.slice(1);
|
|
1701
|
+
base = {
|
|
1702
|
+
...base ?? {},
|
|
1703
|
+
command,
|
|
1704
|
+
...args.length > 0 ? { args } : {}
|
|
1705
|
+
};
|
|
1706
|
+
}
|
|
1707
|
+
if (base === null) {
|
|
1708
|
+
throw new LAFSCommandError(
|
|
1709
|
+
MCP_ERROR_CODES.VALIDATION,
|
|
1710
|
+
"Either an inline `-- <command> [args...]` or `--from <file>` is required.",
|
|
1711
|
+
"Pass an MCP server definition via inline command or a JSON file.",
|
|
1712
|
+
false
|
|
1713
|
+
);
|
|
1714
|
+
}
|
|
1715
|
+
if (opts.env !== void 0 && opts.env.length > 0) {
|
|
1716
|
+
const env = { ...base.env ?? {} };
|
|
1717
|
+
for (const entry of opts.env) {
|
|
1718
|
+
const [k, v] = parseEnvAssignment(entry);
|
|
1719
|
+
env[k] = v;
|
|
1720
|
+
}
|
|
1721
|
+
base = { ...base, env };
|
|
1722
|
+
}
|
|
1723
|
+
return base;
|
|
1724
|
+
}
|
|
1725
|
+
function registerMcpInstallCommand(parent) {
|
|
1726
|
+
parent.command("install <serverName> [args...]").description("Install an MCP server entry into a provider config file").option("--provider <id>", "Provider id to install into (required)").option("--from <file>", "Path to a JSON file containing an MCP server config").option(
|
|
1727
|
+
"--env <kv>",
|
|
1728
|
+
"Repeatable env var KEY=VALUE",
|
|
1729
|
+
(value, prev = []) => [...prev, value],
|
|
1730
|
+
[]
|
|
1731
|
+
).option("--scope <scope>", "Scope: project|global (default: project)").option("--force", "Overwrite an existing server entry").option("--project-dir <path>", "Project directory for the project scope (default: cwd)").action(
|
|
1732
|
+
async (serverName, inlineArgs, opts) => runLafsCommand("mcp.install", "standard", async () => {
|
|
1733
|
+
if (opts.provider === void 0 || opts.provider.length === 0) {
|
|
1734
|
+
throw new LAFSCommandError(
|
|
1735
|
+
MCP_ERROR_CODES.VALIDATION,
|
|
1736
|
+
"--provider <id> is required",
|
|
1737
|
+
"Pass a provider id, e.g. --provider claude-desktop.",
|
|
1738
|
+
false
|
|
1739
|
+
);
|
|
1740
|
+
}
|
|
1741
|
+
if (serverName.length === 0) {
|
|
1742
|
+
throw new LAFSCommandError(
|
|
1743
|
+
MCP_ERROR_CODES.VALIDATION,
|
|
1744
|
+
"Server name is required",
|
|
1745
|
+
"Pass a non-empty server name as the first positional argument.",
|
|
1746
|
+
false
|
|
1747
|
+
);
|
|
1748
|
+
}
|
|
1749
|
+
const provider = requireMcpProvider(opts.provider);
|
|
1750
|
+
const scope = parseScope(opts.scope, "project");
|
|
1751
|
+
const projectDir = resolveProjectDir(scope, opts.projectDir);
|
|
1752
|
+
const config = await buildConfigFromOptions(inlineArgs, opts);
|
|
1753
|
+
const result = await installMcpServer(provider, serverName, config, {
|
|
1754
|
+
scope,
|
|
1755
|
+
force: opts.force ?? false,
|
|
1756
|
+
projectDir
|
|
1757
|
+
});
|
|
1758
|
+
if (!result.installed && result.conflicted) {
|
|
1759
|
+
throw new LAFSCommandError(
|
|
1760
|
+
MCP_ERROR_CODES.CONFLICT,
|
|
1761
|
+
`Server ${serverName} already exists in ${result.sourcePath}`,
|
|
1762
|
+
"Re-run with --force to overwrite the existing entry.",
|
|
1763
|
+
false,
|
|
1764
|
+
{ sourcePath: result.sourcePath, providerId: result.providerId }
|
|
1765
|
+
);
|
|
1766
|
+
}
|
|
1767
|
+
return {
|
|
1768
|
+
installed: true,
|
|
1769
|
+
conflicted: result.conflicted,
|
|
1770
|
+
provider: provider.id,
|
|
1771
|
+
serverName,
|
|
1772
|
+
scope,
|
|
1773
|
+
sourcePath: result.sourcePath,
|
|
1774
|
+
config
|
|
1775
|
+
};
|
|
1776
|
+
})
|
|
1777
|
+
);
|
|
1778
|
+
}
|
|
1779
|
+
|
|
1780
|
+
// src/commands/mcp/list.ts
|
|
1781
|
+
function registerMcpListCommand(parent) {
|
|
1782
|
+
parent.command("list").description("List MCP servers configured for one or every MCP-capable provider").option("--provider <id>", "Restrict to a single provider id").option("--scope <scope>", "Scope: project|global (default: project)").option("--project-dir <path>", "Project directory for the project scope (default: cwd)").action(
|
|
1783
|
+
async (opts) => runLafsCommand("mcp.list", "standard", async () => {
|
|
1784
|
+
const scope = parseScope(opts.scope, "project");
|
|
1785
|
+
const projectDir = resolveProjectDir(scope, opts.projectDir);
|
|
1786
|
+
if (opts.provider !== void 0 && opts.provider.length > 0) {
|
|
1787
|
+
const provider = requireMcpProvider(opts.provider);
|
|
1788
|
+
const entries = await listMcpServers(provider, scope, projectDir);
|
|
1789
|
+
return {
|
|
1790
|
+
scope,
|
|
1791
|
+
provider: {
|
|
1792
|
+
id: provider.id,
|
|
1793
|
+
toolName: provider.toolName
|
|
1794
|
+
},
|
|
1795
|
+
count: entries.length,
|
|
1796
|
+
entries
|
|
1797
|
+
};
|
|
1798
|
+
}
|
|
1799
|
+
const map = await listAllMcpServers(scope, projectDir);
|
|
1800
|
+
const flat = [];
|
|
1801
|
+
const byProvider = {};
|
|
1802
|
+
for (const [providerId, entries] of map.entries()) {
|
|
1803
|
+
byProvider[providerId] = entries.length;
|
|
1804
|
+
flat.push(...entries);
|
|
1805
|
+
}
|
|
1806
|
+
return {
|
|
1807
|
+
scope,
|
|
1808
|
+
providers: byProvider,
|
|
1809
|
+
count: flat.length,
|
|
1810
|
+
entries: flat
|
|
1811
|
+
};
|
|
1812
|
+
})
|
|
1813
|
+
);
|
|
1814
|
+
}
|
|
1815
|
+
|
|
1816
|
+
// src/commands/mcp/remove.ts
|
|
1817
|
+
function registerMcpRemoveCommand(parent) {
|
|
1818
|
+
parent.command("remove <serverName>").description("Remove an MCP server entry from a provider config file").option("--provider <id>", "Provider id to remove from").option("--all-providers", "Remove from every MCP-capable provider in the registry").option("--scope <scope>", "Scope: project|global (default: project)").option("--project-dir <path>", "Project directory for the project scope (default: cwd)").action(
|
|
1819
|
+
async (serverName, opts) => runLafsCommand("mcp.remove", "standard", async () => {
|
|
1820
|
+
if (serverName.length === 0) {
|
|
1821
|
+
throw new LAFSCommandError(
|
|
1822
|
+
MCP_ERROR_CODES.VALIDATION,
|
|
1823
|
+
"Server name is required",
|
|
1824
|
+
"Pass a non-empty server name as the positional argument.",
|
|
1825
|
+
false
|
|
1826
|
+
);
|
|
1827
|
+
}
|
|
1828
|
+
const usingAll = opts.allProviders === true;
|
|
1829
|
+
const usingProvider = opts.provider !== void 0 && opts.provider.length > 0;
|
|
1830
|
+
if (usingAll === usingProvider) {
|
|
1831
|
+
throw new LAFSCommandError(
|
|
1832
|
+
MCP_ERROR_CODES.VALIDATION,
|
|
1833
|
+
"Pass exactly one of --provider <id> or --all-providers",
|
|
1834
|
+
"Use --provider for a single target, or --all-providers to remove everywhere.",
|
|
1835
|
+
false
|
|
1836
|
+
);
|
|
1837
|
+
}
|
|
1838
|
+
const scope = parseScope(opts.scope, "project");
|
|
1839
|
+
const projectDir = resolveProjectDir(scope, opts.projectDir);
|
|
1840
|
+
if (usingAll) {
|
|
1841
|
+
const results = await removeMcpServerFromAll(serverName, { scope, projectDir });
|
|
1842
|
+
const removedCount = results.filter((r) => r.removed).length;
|
|
1843
|
+
return {
|
|
1844
|
+
mode: "all-providers",
|
|
1845
|
+
serverName,
|
|
1846
|
+
scope,
|
|
1847
|
+
removedCount,
|
|
1848
|
+
providersProbed: results.length,
|
|
1849
|
+
results
|
|
1850
|
+
};
|
|
1851
|
+
}
|
|
1852
|
+
const provider = requireMcpProvider(opts.provider);
|
|
1853
|
+
const result = await removeMcpServer(provider, serverName, { scope, projectDir });
|
|
1854
|
+
return {
|
|
1855
|
+
mode: "single-provider",
|
|
1856
|
+
serverName,
|
|
1857
|
+
scope,
|
|
1858
|
+
provider: provider.id,
|
|
1859
|
+
removed: result.removed,
|
|
1860
|
+
reason: result.reason,
|
|
1861
|
+
sourcePath: result.sourcePath
|
|
1862
|
+
};
|
|
1863
|
+
})
|
|
1864
|
+
);
|
|
1865
|
+
}
|
|
1866
|
+
|
|
1867
|
+
// src/commands/mcp/index.ts
|
|
1868
|
+
function registerMcpCommands(program2) {
|
|
1869
|
+
const mcp = program2.command("mcp").description("MCP server config management across providers");
|
|
1870
|
+
registerMcpDetectCommand(mcp);
|
|
1871
|
+
registerMcpListCommand(mcp);
|
|
1872
|
+
registerMcpInstallCommand(mcp);
|
|
1873
|
+
registerMcpRemoveCommand(mcp);
|
|
1874
|
+
}
|
|
1875
|
+
|
|
1876
|
+
// src/commands/pi/cant.ts
|
|
1877
|
+
import { existsSync as existsSync4 } from "fs";
|
|
1878
|
+
import { writeFile } from "fs/promises";
|
|
1879
|
+
import { tmpdir as tmpdir3 } from "os";
|
|
1880
|
+
import { join as join5 } from "path";
|
|
1881
|
+
|
|
1882
|
+
// src/core/sources/github.ts
|
|
1883
|
+
import { mkdtemp, rm } from "fs/promises";
|
|
1884
|
+
import { tmpdir } from "os";
|
|
1885
|
+
import { join as join3 } from "path";
|
|
1886
|
+
import { simpleGit } from "simple-git";
|
|
1887
|
+
async function cloneRepo(owner, repo, ref, subPath) {
|
|
1888
|
+
const tmpDir = await mkdtemp(join3(tmpdir(), "caamp-"));
|
|
1889
|
+
const repoUrl = `https://github.com/${owner}/${repo}.git`;
|
|
1890
|
+
const git = simpleGit();
|
|
1891
|
+
const cloneOptions = ["--depth", "1"];
|
|
1892
|
+
if (ref) {
|
|
1893
|
+
cloneOptions.push("--branch", ref);
|
|
1894
|
+
}
|
|
1895
|
+
await git.clone(repoUrl, tmpDir, cloneOptions);
|
|
1896
|
+
const localPath = subPath ? join3(tmpDir, subPath) : tmpDir;
|
|
1897
|
+
return {
|
|
1898
|
+
localPath,
|
|
1899
|
+
cleanup: async () => {
|
|
1900
|
+
try {
|
|
1901
|
+
await rm(tmpDir, { recursive: true });
|
|
1902
|
+
} catch {
|
|
1903
|
+
}
|
|
1904
|
+
}
|
|
1905
|
+
};
|
|
1906
|
+
}
|
|
1907
|
+
|
|
1908
|
+
// src/core/sources/gitlab.ts
|
|
1909
|
+
import { mkdtemp as mkdtemp2, rm as rm2 } from "fs/promises";
|
|
1910
|
+
import { tmpdir as tmpdir2 } from "os";
|
|
1911
|
+
import { join as join4 } from "path";
|
|
1912
|
+
import { simpleGit as simpleGit2 } from "simple-git";
|
|
1913
|
+
async function cloneGitLabRepo(owner, repo, ref, subPath) {
|
|
1914
|
+
const tmpDir = await mkdtemp2(join4(tmpdir2(), "caamp-gl-"));
|
|
1915
|
+
const repoUrl = `https://gitlab.com/${owner}/${repo}.git`;
|
|
1916
|
+
const git = simpleGit2();
|
|
1917
|
+
const cloneOptions = ["--depth", "1"];
|
|
1918
|
+
if (ref) {
|
|
1919
|
+
cloneOptions.push("--branch", ref);
|
|
1920
|
+
}
|
|
1921
|
+
await git.clone(repoUrl, tmpDir, cloneOptions);
|
|
1922
|
+
const localPath = subPath ? join4(tmpDir, subPath) : tmpDir;
|
|
1923
|
+
return {
|
|
1924
|
+
localPath,
|
|
1925
|
+
cleanup: async () => {
|
|
1926
|
+
try {
|
|
1927
|
+
await rm2(tmpDir, { recursive: true });
|
|
1928
|
+
} catch {
|
|
1929
|
+
}
|
|
1930
|
+
}
|
|
1931
|
+
};
|
|
1932
|
+
}
|
|
1933
|
+
|
|
1934
|
+
// src/commands/pi/common.ts
|
|
1935
|
+
var PI_ERROR_CODES = {
|
|
1936
|
+
/** Caller-supplied input failed validation (shape, type, enum). */
|
|
1937
|
+
VALIDATION: "E_VALIDATION_SCHEMA",
|
|
1938
|
+
/** Referenced resource does not exist on disk or in the registry. */
|
|
1939
|
+
NOT_FOUND: "E_NOT_FOUND_RESOURCE",
|
|
1940
|
+
/** Write target already exists and overwrite was not requested. */
|
|
1941
|
+
CONFLICT: "E_CONFLICT_VERSION",
|
|
1942
|
+
/** Network/upstream call failed; retry is viable. */
|
|
1943
|
+
TRANSIENT: "E_TRANSIENT_UPSTREAM"
|
|
1944
|
+
};
|
|
1945
|
+
function requirePiHarness() {
|
|
1946
|
+
const provider = getProvider("pi");
|
|
1947
|
+
if (provider === void 0) {
|
|
1948
|
+
throw new LAFSCommandError(
|
|
1949
|
+
PI_ERROR_CODES.NOT_FOUND,
|
|
1950
|
+
"Pi provider is not registered in the CAAMP registry.",
|
|
1951
|
+
"This is a configuration bug \u2014 open an issue with `caamp providers list`.",
|
|
1952
|
+
false
|
|
1953
|
+
);
|
|
1954
|
+
}
|
|
1955
|
+
const installed = getInstalledProviders();
|
|
1956
|
+
const piInstalled = installed.some((p) => p.id === "pi");
|
|
1957
|
+
if (!piInstalled) {
|
|
1958
|
+
throw new LAFSCommandError(
|
|
1959
|
+
PI_ERROR_CODES.NOT_FOUND,
|
|
1960
|
+
"Pi is not installed. Run: caamp providers install pi",
|
|
1961
|
+
"Install Pi via its official installer, then retry this command.",
|
|
1962
|
+
true
|
|
1963
|
+
);
|
|
1964
|
+
}
|
|
1965
|
+
const harness = getHarnessFor(provider);
|
|
1966
|
+
if (!(harness instanceof PiHarness)) {
|
|
1967
|
+
throw new LAFSCommandError(
|
|
1968
|
+
"E_INTERNAL_UNEXPECTED",
|
|
1969
|
+
"Pi provider is registered but no PiHarness implementation was returned.",
|
|
1970
|
+
"This is a programming error \u2014 the harness dispatcher should always return a PiHarness for Pi.",
|
|
1971
|
+
false
|
|
1972
|
+
);
|
|
1973
|
+
}
|
|
1974
|
+
return harness;
|
|
1975
|
+
}
|
|
1976
|
+
function parseScope2(raw, defaultTier) {
|
|
1977
|
+
if (raw === void 0) return defaultTier;
|
|
1978
|
+
if (raw === "project" || raw === "user" || raw === "global") return raw;
|
|
1979
|
+
throw new LAFSCommandError(
|
|
1980
|
+
PI_ERROR_CODES.VALIDATION,
|
|
1981
|
+
`Invalid --scope value: ${raw}`,
|
|
1982
|
+
"Use one of: 'project', 'user', 'global'.",
|
|
1983
|
+
false
|
|
1984
|
+
);
|
|
1985
|
+
}
|
|
1986
|
+
function resolveProjectDir2(tier, explicit) {
|
|
1987
|
+
if (tier !== "project") return void 0;
|
|
1988
|
+
if (explicit !== void 0 && explicit.length > 0) return explicit;
|
|
1989
|
+
return process.cwd();
|
|
1990
|
+
}
|
|
1991
|
+
|
|
1992
|
+
// src/commands/pi/cant.ts
|
|
1993
|
+
async function resolveCantSource(source) {
|
|
1994
|
+
if (source.startsWith("/") || source.startsWith("./") || source.startsWith("../") || source.startsWith("~")) {
|
|
1995
|
+
const expanded = source.startsWith("~/") ? join5(process.env["HOME"] ?? "", source.slice(2)) : source;
|
|
1996
|
+
if (!existsSync4(expanded)) {
|
|
1997
|
+
throw new LAFSCommandError(
|
|
1998
|
+
PI_ERROR_CODES.NOT_FOUND,
|
|
1999
|
+
`Source file does not exist: ${expanded}`,
|
|
2000
|
+
"Check the path and try again.",
|
|
2001
|
+
false
|
|
2002
|
+
);
|
|
2003
|
+
}
|
|
2004
|
+
return {
|
|
2005
|
+
localPath: expanded,
|
|
2006
|
+
cleanup: async () => {
|
|
2007
|
+
},
|
|
2008
|
+
inferredName: inferNameFromPath(expanded)
|
|
2009
|
+
};
|
|
2010
|
+
}
|
|
2011
|
+
if (/^https?:\/\//.test(source)) {
|
|
2012
|
+
const parsed2 = parseSource(source);
|
|
2013
|
+
if (parsed2.type === "github" && parsed2.owner !== void 0 && parsed2.repo !== void 0) {
|
|
2014
|
+
const cloneResult = await cloneRepo(parsed2.owner, parsed2.repo, parsed2.ref);
|
|
2015
|
+
const filePath = parsed2.path !== void 0 ? join5(cloneResult.localPath, parsed2.path) : cloneResult.localPath;
|
|
2016
|
+
if (!existsSync4(filePath)) {
|
|
2017
|
+
await cloneResult.cleanup();
|
|
2018
|
+
throw new LAFSCommandError(
|
|
2019
|
+
PI_ERROR_CODES.NOT_FOUND,
|
|
2020
|
+
`Source path not found inside cloned repo: ${parsed2.path ?? "(root)"}`,
|
|
2021
|
+
"Check the repository URL and path.",
|
|
2022
|
+
false
|
|
2023
|
+
);
|
|
2024
|
+
}
|
|
2025
|
+
return {
|
|
2026
|
+
localPath: filePath,
|
|
2027
|
+
cleanup: cloneResult.cleanup,
|
|
2028
|
+
inferredName: inferNameFromPath(filePath)
|
|
2029
|
+
};
|
|
2030
|
+
}
|
|
2031
|
+
if (parsed2.type === "gitlab" && parsed2.owner !== void 0 && parsed2.repo !== void 0) {
|
|
2032
|
+
const cloneResult = await cloneGitLabRepo(parsed2.owner, parsed2.repo, parsed2.ref);
|
|
2033
|
+
const filePath = parsed2.path !== void 0 ? join5(cloneResult.localPath, parsed2.path) : cloneResult.localPath;
|
|
2034
|
+
if (!existsSync4(filePath)) {
|
|
2035
|
+
await cloneResult.cleanup();
|
|
2036
|
+
throw new LAFSCommandError(
|
|
2037
|
+
PI_ERROR_CODES.NOT_FOUND,
|
|
2038
|
+
`Source path not found inside cloned repo: ${parsed2.path ?? "(root)"}`,
|
|
2039
|
+
"Check the repository URL and path.",
|
|
2040
|
+
false
|
|
2041
|
+
);
|
|
2042
|
+
}
|
|
2043
|
+
return {
|
|
2044
|
+
localPath: filePath,
|
|
2045
|
+
cleanup: cloneResult.cleanup,
|
|
2046
|
+
inferredName: inferNameFromPath(filePath)
|
|
2047
|
+
};
|
|
2048
|
+
}
|
|
2049
|
+
const resp = await fetchWithTimeout(source);
|
|
2050
|
+
if (!resp.ok) {
|
|
2051
|
+
throw new LAFSCommandError(
|
|
2052
|
+
PI_ERROR_CODES.TRANSIENT,
|
|
2053
|
+
`Failed to download source from ${source}: HTTP ${resp.status}`,
|
|
2054
|
+
"Check the URL and network connectivity.",
|
|
2055
|
+
true
|
|
2056
|
+
);
|
|
2057
|
+
}
|
|
2058
|
+
const body = await resp.text();
|
|
2059
|
+
const baseName = inferNameFromUrl(source);
|
|
2060
|
+
const tmp = join5(tmpdir3(), `caamp-pi-cant-${process.pid}-${Date.now()}-${baseName}.cant`);
|
|
2061
|
+
await writeFile(tmp, body, "utf8");
|
|
2062
|
+
return {
|
|
2063
|
+
localPath: tmp,
|
|
2064
|
+
cleanup: async () => {
|
|
2065
|
+
try {
|
|
2066
|
+
await (await import("fs/promises")).rm(tmp, { force: true });
|
|
2067
|
+
} catch {
|
|
2068
|
+
}
|
|
2069
|
+
},
|
|
2070
|
+
inferredName: baseName
|
|
2071
|
+
};
|
|
2072
|
+
}
|
|
2073
|
+
const parsed = parseSource(source);
|
|
2074
|
+
if (parsed.type === "github" && parsed.owner !== void 0 && parsed.repo !== void 0) {
|
|
2075
|
+
const cloneResult = await cloneRepo(parsed.owner, parsed.repo, parsed.ref);
|
|
2076
|
+
const filePath = parsed.path !== void 0 ? join5(cloneResult.localPath, parsed.path) : cloneResult.localPath;
|
|
2077
|
+
if (!existsSync4(filePath)) {
|
|
2078
|
+
await cloneResult.cleanup();
|
|
2079
|
+
throw new LAFSCommandError(
|
|
2080
|
+
PI_ERROR_CODES.NOT_FOUND,
|
|
2081
|
+
`Source path not found inside cloned repo: ${parsed.path ?? "(root)"}`,
|
|
2082
|
+
"Check the repository shorthand and path.",
|
|
2083
|
+
false
|
|
2084
|
+
);
|
|
2085
|
+
}
|
|
2086
|
+
return {
|
|
2087
|
+
localPath: filePath,
|
|
2088
|
+
cleanup: cloneResult.cleanup,
|
|
2089
|
+
inferredName: inferNameFromPath(filePath)
|
|
2090
|
+
};
|
|
2091
|
+
}
|
|
2092
|
+
throw new LAFSCommandError(
|
|
2093
|
+
PI_ERROR_CODES.VALIDATION,
|
|
2094
|
+
`Unsupported source: ${source}`,
|
|
2095
|
+
"Use a local file path, HTTPS URL, or GitHub shorthand (owner/repo/path.cant).",
|
|
2096
|
+
false
|
|
2097
|
+
);
|
|
2098
|
+
}
|
|
2099
|
+
function inferNameFromPath(filePath) {
|
|
2100
|
+
const base = filePath.split(/[/\\]/).pop() ?? filePath;
|
|
2101
|
+
return base.replace(/\.cant$/, "");
|
|
2102
|
+
}
|
|
2103
|
+
function inferNameFromUrl(url) {
|
|
2104
|
+
try {
|
|
2105
|
+
const u = new URL(url);
|
|
2106
|
+
const seg = u.pathname.split("/").filter(Boolean).pop() ?? "profile";
|
|
2107
|
+
return seg.replace(/\.cant$/, "");
|
|
2108
|
+
} catch {
|
|
2109
|
+
return "profile";
|
|
2110
|
+
}
|
|
2111
|
+
}
|
|
2112
|
+
async function invokeInstallCantProfile(harness, sourcePath, name, tier, projectDir, installOpts) {
|
|
2113
|
+
try {
|
|
2114
|
+
return await harness.installCantProfile(sourcePath, name, tier, projectDir, installOpts);
|
|
2115
|
+
} catch (err) {
|
|
2116
|
+
rethrowAsLafs(err);
|
|
2117
|
+
}
|
|
2118
|
+
}
|
|
2119
|
+
function rethrowAsLafs(err) {
|
|
2120
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
2121
|
+
if (/already exists/i.test(message)) {
|
|
2122
|
+
throw new LAFSCommandError(
|
|
2123
|
+
PI_ERROR_CODES.CONFLICT,
|
|
2124
|
+
message,
|
|
2125
|
+
"Pass --force to overwrite the existing profile.",
|
|
2126
|
+
false
|
|
2127
|
+
);
|
|
2128
|
+
}
|
|
2129
|
+
if (/does not exist|not found/i.test(message)) {
|
|
2130
|
+
throw new LAFSCommandError(PI_ERROR_CODES.NOT_FOUND, message, "Check the path.", false);
|
|
2131
|
+
}
|
|
2132
|
+
if (/failed cant-core validation|expected a CANT source file|not a regular file/i.test(message)) {
|
|
2133
|
+
throw new LAFSCommandError(
|
|
2134
|
+
PI_ERROR_CODES.VALIDATION,
|
|
2135
|
+
message,
|
|
2136
|
+
"Run `caamp pi cant validate <path>` to inspect the diagnostics.",
|
|
2137
|
+
false
|
|
2138
|
+
);
|
|
2139
|
+
}
|
|
2140
|
+
throw new LAFSCommandError(
|
|
2141
|
+
"E_INTERNAL_UNEXPECTED",
|
|
2142
|
+
message,
|
|
2143
|
+
"Inspect the message for the underlying cant-core failure mode.",
|
|
2144
|
+
false
|
|
2145
|
+
);
|
|
2146
|
+
}
|
|
2147
|
+
function registerPiCantCommands(parent) {
|
|
2148
|
+
const cant = parent.command("cant").description("Manage Pi CANT profiles across tiers");
|
|
2149
|
+
cant.command("list").description("List Pi CANT profiles across project, user, and global tiers").option("--scope <tier>", "Filter to a single tier: project|user|global").option("--project-dir <path>", "Project directory for the project tier (default: cwd)").action(
|
|
2150
|
+
async (opts) => runLafsCommand("pi.cant.list", "standard", async () => {
|
|
2151
|
+
const harness = requirePiHarness();
|
|
2152
|
+
const projectDir = opts.projectDir ?? process.cwd();
|
|
2153
|
+
const allEntries = await harness.listCantProfiles(projectDir);
|
|
2154
|
+
const filterTier = opts.scope === void 0 ? null : parseScope2(opts.scope, "project");
|
|
2155
|
+
const entries = filterTier === null ? allEntries : allEntries.filter((e) => e.tier === filterTier);
|
|
2156
|
+
const sorted = [...entries].sort((a, b) => a.name.localeCompare(b.name));
|
|
2157
|
+
return {
|
|
2158
|
+
count: sorted.length,
|
|
2159
|
+
entries: sorted
|
|
2160
|
+
};
|
|
2161
|
+
})
|
|
2162
|
+
);
|
|
2163
|
+
cant.command("install <source>").description("Install a Pi CANT profile from a local path, HTTPS URL, or GitHub shorthand").option("--scope <tier>", "Install tier: project|user|global (default: project)").option("--name <name>", "Override the inferred profile name").option("--force", "Overwrite an existing profile at the target tier").option("--project-dir <path>", "Project directory for the project tier (default: cwd)").action(
|
|
2164
|
+
async (source, opts) => runLafsCommand("pi.cant.install", "standard", async () => {
|
|
2165
|
+
const harness = requirePiHarness();
|
|
2166
|
+
const tier = parseScope2(opts.scope, "project");
|
|
2167
|
+
const projectDir = resolveProjectDir2(tier, opts.projectDir);
|
|
2168
|
+
const resolved = await resolveCantSource(source);
|
|
2169
|
+
try {
|
|
2170
|
+
const name = opts.name ?? resolved.inferredName;
|
|
2171
|
+
const installOpts = { force: opts.force ?? false };
|
|
2172
|
+
const result = await invokeInstallCantProfile(
|
|
2173
|
+
harness,
|
|
2174
|
+
resolved.localPath,
|
|
2175
|
+
name,
|
|
2176
|
+
tier,
|
|
2177
|
+
projectDir,
|
|
2178
|
+
installOpts
|
|
2179
|
+
);
|
|
2180
|
+
return {
|
|
2181
|
+
installed: {
|
|
2182
|
+
name,
|
|
2183
|
+
tier: result.tier,
|
|
2184
|
+
targetPath: result.targetPath,
|
|
2185
|
+
counts: result.counts,
|
|
2186
|
+
source
|
|
2187
|
+
}
|
|
2188
|
+
};
|
|
2189
|
+
} finally {
|
|
2190
|
+
await resolved.cleanup();
|
|
2191
|
+
}
|
|
2192
|
+
})
|
|
2193
|
+
);
|
|
2194
|
+
cant.command("remove <name>").description("Remove a Pi CANT profile from the given tier").option("--scope <tier>", "Target tier: project|user|global (default: project)").option("--project-dir <path>", "Project directory for the project tier (default: cwd)").action(
|
|
2195
|
+
async (name, opts) => runLafsCommand("pi.cant.remove", "standard", async () => {
|
|
2196
|
+
const harness = requirePiHarness();
|
|
2197
|
+
const tier = parseScope2(opts.scope, "project");
|
|
2198
|
+
const projectDir = resolveProjectDir2(tier, opts.projectDir);
|
|
2199
|
+
const removed = await harness.removeCantProfile(name, tier, projectDir);
|
|
2200
|
+
return {
|
|
2201
|
+
name,
|
|
2202
|
+
tier,
|
|
2203
|
+
removed
|
|
2204
|
+
};
|
|
2205
|
+
})
|
|
2206
|
+
);
|
|
2207
|
+
cant.command("validate <path>").description("Validate a .cant file via cant-core without installing it").action(
|
|
2208
|
+
async (path) => runLafsCommand("pi.cant.validate", "standard", async () => {
|
|
2209
|
+
const harness = requirePiHarness();
|
|
2210
|
+
if (!existsSync4(path)) {
|
|
2211
|
+
throw new LAFSCommandError(
|
|
2212
|
+
PI_ERROR_CODES.NOT_FOUND,
|
|
2213
|
+
`Source file does not exist: ${path}`,
|
|
2214
|
+
"Check the path and try again.",
|
|
2215
|
+
false
|
|
2216
|
+
);
|
|
2217
|
+
}
|
|
2218
|
+
const result = await harness.validateCantProfile(path);
|
|
2219
|
+
if (!result.valid) {
|
|
2220
|
+
throw new LAFSCommandError(
|
|
2221
|
+
PI_ERROR_CODES.VALIDATION,
|
|
2222
|
+
`cant-core validation failed with ${result.errors.length} diagnostic(s)`,
|
|
2223
|
+
"See the `errors` field for ruleId/line/col/message details.",
|
|
2224
|
+
false,
|
|
2225
|
+
{ valid: false, counts: result.counts, errors: result.errors }
|
|
2226
|
+
);
|
|
2227
|
+
}
|
|
2228
|
+
return {
|
|
2229
|
+
valid: true,
|
|
2230
|
+
counts: result.counts,
|
|
2231
|
+
errors: result.errors
|
|
2232
|
+
};
|
|
2233
|
+
})
|
|
2234
|
+
);
|
|
2235
|
+
}
|
|
2236
|
+
|
|
2237
|
+
// src/commands/pi/extensions.ts
|
|
2238
|
+
import { existsSync as existsSync5 } from "fs";
|
|
2239
|
+
import { writeFile as writeFile2 } from "fs/promises";
|
|
2240
|
+
import { tmpdir as tmpdir4 } from "os";
|
|
2241
|
+
import { join as join6 } from "path";
|
|
2242
|
+
async function resolveExtensionSource(source) {
|
|
2243
|
+
if (source.startsWith("/") || source.startsWith("./") || source.startsWith("../") || source.startsWith("~")) {
|
|
2244
|
+
const expanded = source.startsWith("~/") ? join6(process.env["HOME"] ?? "", source.slice(2)) : source;
|
|
2245
|
+
if (!existsSync5(expanded)) {
|
|
2246
|
+
throw new LAFSCommandError(
|
|
2247
|
+
PI_ERROR_CODES.NOT_FOUND,
|
|
2248
|
+
`Source file does not exist: ${expanded}`,
|
|
2249
|
+
"Check the path and try again.",
|
|
2250
|
+
false
|
|
2251
|
+
);
|
|
2252
|
+
}
|
|
2253
|
+
const inferredName = inferNameFromPath2(expanded);
|
|
2254
|
+
return {
|
|
2255
|
+
localPath: expanded,
|
|
2256
|
+
cleanup: async () => {
|
|
2257
|
+
},
|
|
2258
|
+
inferredName
|
|
2259
|
+
};
|
|
2260
|
+
}
|
|
2261
|
+
if (/^https?:\/\//.test(source)) {
|
|
2262
|
+
const parsed2 = parseSource(source);
|
|
2263
|
+
if (parsed2.type === "github" && parsed2.owner !== void 0 && parsed2.repo !== void 0) {
|
|
2264
|
+
const cloneResult = await cloneRepo(parsed2.owner, parsed2.repo, parsed2.ref);
|
|
2265
|
+
const filePath = parsed2.path !== void 0 ? join6(cloneResult.localPath, parsed2.path) : cloneResult.localPath;
|
|
2266
|
+
if (!existsSync5(filePath)) {
|
|
2267
|
+
await cloneResult.cleanup();
|
|
2268
|
+
throw new LAFSCommandError(
|
|
2269
|
+
PI_ERROR_CODES.NOT_FOUND,
|
|
2270
|
+
`Source path not found inside cloned repo: ${parsed2.path ?? "(root)"}`,
|
|
2271
|
+
"Check the repository URL and path.",
|
|
2272
|
+
false
|
|
2273
|
+
);
|
|
2274
|
+
}
|
|
2275
|
+
return {
|
|
2276
|
+
localPath: filePath,
|
|
2277
|
+
cleanup: cloneResult.cleanup,
|
|
2278
|
+
inferredName: inferNameFromPath2(filePath)
|
|
2279
|
+
};
|
|
2280
|
+
}
|
|
2281
|
+
if (parsed2.type === "gitlab" && parsed2.owner !== void 0 && parsed2.repo !== void 0) {
|
|
2282
|
+
const cloneResult = await cloneGitLabRepo(parsed2.owner, parsed2.repo, parsed2.ref);
|
|
2283
|
+
const filePath = parsed2.path !== void 0 ? join6(cloneResult.localPath, parsed2.path) : cloneResult.localPath;
|
|
2284
|
+
if (!existsSync5(filePath)) {
|
|
2285
|
+
await cloneResult.cleanup();
|
|
2286
|
+
throw new LAFSCommandError(
|
|
2287
|
+
PI_ERROR_CODES.NOT_FOUND,
|
|
2288
|
+
`Source path not found inside cloned repo: ${parsed2.path ?? "(root)"}`,
|
|
2289
|
+
"Check the repository URL and path.",
|
|
2290
|
+
false
|
|
2291
|
+
);
|
|
2292
|
+
}
|
|
2293
|
+
return {
|
|
2294
|
+
localPath: filePath,
|
|
2295
|
+
cleanup: cloneResult.cleanup,
|
|
2296
|
+
inferredName: inferNameFromPath2(filePath)
|
|
2297
|
+
};
|
|
2298
|
+
}
|
|
2299
|
+
const resp = await fetchWithTimeout(source);
|
|
2300
|
+
if (!resp.ok) {
|
|
2301
|
+
throw new LAFSCommandError(
|
|
2302
|
+
PI_ERROR_CODES.TRANSIENT,
|
|
2303
|
+
`Failed to download source from ${source}: HTTP ${resp.status}`,
|
|
2304
|
+
"Check the URL and network connectivity.",
|
|
2305
|
+
true
|
|
2306
|
+
);
|
|
2307
|
+
}
|
|
2308
|
+
const body = await resp.text();
|
|
2309
|
+
const baseName = inferNameFromUrl2(source);
|
|
2310
|
+
const tmp = join6(tmpdir4(), `caamp-pi-ext-${process.pid}-${Date.now()}-${baseName}.ts`);
|
|
2311
|
+
await writeFile2(tmp, body, "utf8");
|
|
2312
|
+
return {
|
|
2313
|
+
localPath: tmp,
|
|
2314
|
+
cleanup: async () => {
|
|
2315
|
+
try {
|
|
2316
|
+
await (await import("fs/promises")).rm(tmp, { force: true });
|
|
2317
|
+
} catch {
|
|
2318
|
+
}
|
|
2319
|
+
},
|
|
2320
|
+
inferredName: baseName
|
|
2321
|
+
};
|
|
2322
|
+
}
|
|
2323
|
+
const parsed = parseSource(source);
|
|
2324
|
+
if (parsed.type === "github" && parsed.owner !== void 0 && parsed.repo !== void 0) {
|
|
2325
|
+
const cloneResult = await cloneRepo(parsed.owner, parsed.repo, parsed.ref);
|
|
2326
|
+
const filePath = parsed.path !== void 0 ? join6(cloneResult.localPath, parsed.path) : cloneResult.localPath;
|
|
2327
|
+
if (!existsSync5(filePath)) {
|
|
2328
|
+
await cloneResult.cleanup();
|
|
2329
|
+
throw new LAFSCommandError(
|
|
2330
|
+
PI_ERROR_CODES.NOT_FOUND,
|
|
2331
|
+
`Source path not found inside cloned repo: ${parsed.path ?? "(root)"}`,
|
|
2332
|
+
"Check the repository shorthand and path.",
|
|
2333
|
+
false
|
|
2334
|
+
);
|
|
2335
|
+
}
|
|
2336
|
+
return {
|
|
2337
|
+
localPath: filePath,
|
|
2338
|
+
cleanup: cloneResult.cleanup,
|
|
2339
|
+
inferredName: inferNameFromPath2(filePath)
|
|
2340
|
+
};
|
|
2341
|
+
}
|
|
2342
|
+
throw new LAFSCommandError(
|
|
2343
|
+
PI_ERROR_CODES.VALIDATION,
|
|
2344
|
+
`Unsupported source: ${source}`,
|
|
2345
|
+
"Use a local file path, HTTPS URL, or GitHub shorthand (owner/repo/path.ts).",
|
|
2346
|
+
false
|
|
2347
|
+
);
|
|
2348
|
+
}
|
|
2349
|
+
function inferNameFromPath2(filePath) {
|
|
2350
|
+
const base = filePath.split(/[/\\]/).pop() ?? filePath;
|
|
2351
|
+
return base.replace(/\.(ts|tsx|mts)$/, "");
|
|
2352
|
+
}
|
|
2353
|
+
function inferNameFromUrl2(url) {
|
|
2354
|
+
try {
|
|
2355
|
+
const u = new URL(url);
|
|
2356
|
+
const seg = u.pathname.split("/").filter(Boolean).pop() ?? "extension";
|
|
2357
|
+
return seg.replace(/\.(ts|tsx|mts)$/, "");
|
|
2358
|
+
} catch {
|
|
2359
|
+
return "extension";
|
|
2360
|
+
}
|
|
2361
|
+
}
|
|
2362
|
+
function registerPiExtensionsCommands(parent) {
|
|
2363
|
+
const ext = parent.command("extensions").description("Manage Pi extensions across tiers");
|
|
2364
|
+
ext.command("list").description("List Pi extensions across project, user, and global tiers").option("--project-dir <path>", "Project directory for the project tier (default: cwd)").action(
|
|
2365
|
+
async (opts) => runLafsCommand("pi.extensions.list", "standard", async () => {
|
|
2366
|
+
const harness = requirePiHarness();
|
|
2367
|
+
const projectDir = opts.projectDir ?? process.cwd();
|
|
2368
|
+
const entries = await harness.listExtensions(projectDir);
|
|
2369
|
+
return {
|
|
2370
|
+
count: entries.length,
|
|
2371
|
+
extensions: entries
|
|
2372
|
+
};
|
|
2373
|
+
})
|
|
2374
|
+
);
|
|
2375
|
+
ext.command("install <source>").description("Install a Pi extension from a local path, HTTPS URL, or GitHub shorthand").option("--scope <tier>", "Install tier: project|user|global (default: project)").option("--name <name>", "Override the inferred extension name").option("--force", "Overwrite an existing extension at the target tier").option("--project-dir <path>", "Project directory for the project tier (default: cwd)").action(
|
|
2376
|
+
async (source, opts) => runLafsCommand("pi.extensions.install", "standard", async () => {
|
|
2377
|
+
const harness = requirePiHarness();
|
|
2378
|
+
const tier = parseScope2(opts.scope, "project");
|
|
2379
|
+
const projectDir = resolveProjectDir2(tier, opts.projectDir);
|
|
2380
|
+
const resolved = await resolveExtensionSource(source);
|
|
2381
|
+
try {
|
|
2382
|
+
const name = opts.name ?? resolved.inferredName;
|
|
2383
|
+
const installOpts = { force: opts.force ?? false };
|
|
2384
|
+
const result = await harness.installExtension(
|
|
2385
|
+
resolved.localPath,
|
|
2386
|
+
name,
|
|
2387
|
+
tier,
|
|
2388
|
+
projectDir,
|
|
2389
|
+
installOpts
|
|
2390
|
+
);
|
|
2391
|
+
return {
|
|
2392
|
+
installed: {
|
|
2393
|
+
name,
|
|
2394
|
+
tier: result.tier,
|
|
2395
|
+
targetPath: result.targetPath,
|
|
2396
|
+
source
|
|
2397
|
+
}
|
|
2398
|
+
};
|
|
2399
|
+
} finally {
|
|
2400
|
+
await resolved.cleanup();
|
|
2401
|
+
}
|
|
2402
|
+
})
|
|
2403
|
+
);
|
|
2404
|
+
ext.command("remove <name>").description("Remove a Pi extension from the given tier").option("--scope <tier>", "Target tier: project|user|global (default: project)").option("--project-dir <path>", "Project directory for the project tier (default: cwd)").action(
|
|
2405
|
+
async (name, opts) => runLafsCommand("pi.extensions.remove", "standard", async () => {
|
|
2406
|
+
const harness = requirePiHarness();
|
|
2407
|
+
const tier = parseScope2(opts.scope, "project");
|
|
2408
|
+
const projectDir = resolveProjectDir2(tier, opts.projectDir);
|
|
2409
|
+
const removed = await harness.removeExtension(name, tier, projectDir);
|
|
2410
|
+
return {
|
|
2411
|
+
name,
|
|
2412
|
+
tier,
|
|
2413
|
+
removed
|
|
2414
|
+
};
|
|
2415
|
+
})
|
|
2416
|
+
);
|
|
2417
|
+
}
|
|
2418
|
+
|
|
2419
|
+
// src/commands/pi/models.ts
|
|
2420
|
+
function parseModelSpec(spec) {
|
|
2421
|
+
const idx = spec.indexOf(":");
|
|
2422
|
+
if (idx <= 0 || idx === spec.length - 1) {
|
|
2423
|
+
throw new LAFSCommandError(
|
|
2424
|
+
PI_ERROR_CODES.VALIDATION,
|
|
2425
|
+
`Invalid model specifier: ${spec}`,
|
|
2426
|
+
"Use 'provider:model-id', e.g. 'anthropic:claude-sonnet-4-20250514'.",
|
|
2427
|
+
false
|
|
2428
|
+
);
|
|
2429
|
+
}
|
|
2430
|
+
return { provider: spec.slice(0, idx), id: spec.slice(idx + 1) };
|
|
2431
|
+
}
|
|
2432
|
+
function resolveModelsScope(opts) {
|
|
2433
|
+
if (opts.global === true) return { kind: "global" };
|
|
2434
|
+
if (opts.projectDir !== void 0 && opts.projectDir.length > 0) {
|
|
2435
|
+
return { kind: "project", projectDir: opts.projectDir };
|
|
2436
|
+
}
|
|
2437
|
+
return { kind: "global" };
|
|
2438
|
+
}
|
|
2439
|
+
function parsePositiveInt(raw, name) {
|
|
2440
|
+
if (raw === void 0) return void 0;
|
|
2441
|
+
const parsed = Number.parseInt(raw, 10);
|
|
2442
|
+
if (!Number.isFinite(parsed) || parsed <= 0 || String(parsed) !== raw.trim()) {
|
|
2443
|
+
throw new LAFSCommandError(
|
|
2444
|
+
PI_ERROR_CODES.VALIDATION,
|
|
2445
|
+
`Invalid value for --${name}: ${raw}`,
|
|
2446
|
+
`--${name} must be a positive integer.`,
|
|
2447
|
+
false
|
|
2448
|
+
);
|
|
2449
|
+
}
|
|
2450
|
+
return parsed;
|
|
2451
|
+
}
|
|
2452
|
+
function registerPiModelsCommands(parent) {
|
|
2453
|
+
const models = parent.command("models").description("Manage Pi's dual-file models configuration");
|
|
2454
|
+
models.command("list").description("List every model known to Pi (union of models.json and enabledModels)").option("--global", "Read from the Pi global state root (default)").option("--project-dir <path>", "Read from a project-scoped Pi config").action(
|
|
2455
|
+
async (opts) => runLafsCommand("pi.models.list", "standard", async () => {
|
|
2456
|
+
const harness = requirePiHarness();
|
|
2457
|
+
const scope = resolveModelsScope(opts);
|
|
2458
|
+
const entries = await harness.listModels(scope);
|
|
2459
|
+
const active = entries.filter((e) => e.enabled);
|
|
2460
|
+
const def = entries.find((e) => e.isDefault) ?? null;
|
|
2461
|
+
return {
|
|
2462
|
+
scope: scope.kind,
|
|
2463
|
+
count: entries.length,
|
|
2464
|
+
activeCount: active.length,
|
|
2465
|
+
default: def,
|
|
2466
|
+
models: entries
|
|
2467
|
+
};
|
|
2468
|
+
})
|
|
2469
|
+
);
|
|
2470
|
+
models.command("add <spec>").description("Add a custom model definition to models.json (e.g. provider:model-id)").option("--global", "Write to the Pi global state root (default)").option("--project-dir <path>", "Write to a project-scoped Pi config").option("--display-name <name>", "Human-readable model name").option("--base-url <url>", "Override the provider base URL").option("--reasoning", "Mark the model as reasoning-capable").option("--context-window <tokens>", "Context window size in tokens").option("--max-tokens <tokens>", "Maximum output tokens").action(
|
|
2471
|
+
async (spec, opts) => runLafsCommand("pi.models.add", "standard", async () => {
|
|
2472
|
+
const harness = requirePiHarness();
|
|
2473
|
+
const scope = resolveModelsScope(opts);
|
|
2474
|
+
const { provider, id } = parseModelSpec(spec);
|
|
2475
|
+
const contextWindow = parsePositiveInt(opts.contextWindow, "context-window");
|
|
2476
|
+
const maxTokens = parsePositiveInt(opts.maxTokens, "max-tokens");
|
|
2477
|
+
const config = await harness.readModelsConfig(scope);
|
|
2478
|
+
const providerBlock = config.providers[provider] ?? {};
|
|
2479
|
+
if (opts.baseUrl !== void 0) providerBlock.baseUrl = opts.baseUrl;
|
|
2480
|
+
const nextModels = providerBlock.models ? [...providerBlock.models] : [];
|
|
2481
|
+
const existingIdx = nextModels.findIndex((m) => m.id === id);
|
|
2482
|
+
const definition = {
|
|
2483
|
+
id,
|
|
2484
|
+
name: opts.displayName ?? id
|
|
2485
|
+
};
|
|
2486
|
+
if (opts.reasoning === true) definition.reasoning = true;
|
|
2487
|
+
if (contextWindow !== void 0) definition.contextWindow = contextWindow;
|
|
2488
|
+
if (maxTokens !== void 0) definition.maxTokens = maxTokens;
|
|
2489
|
+
if (existingIdx >= 0) {
|
|
2490
|
+
nextModels[existingIdx] = definition;
|
|
2491
|
+
} else {
|
|
2492
|
+
nextModels.push(definition);
|
|
2493
|
+
}
|
|
2494
|
+
providerBlock.models = nextModels;
|
|
2495
|
+
const nextConfig = {
|
|
2496
|
+
providers: { ...config.providers, [provider]: providerBlock }
|
|
2497
|
+
};
|
|
2498
|
+
await harness.writeModelsConfig(nextConfig, scope);
|
|
2499
|
+
return {
|
|
2500
|
+
added: { provider, id, name: definition.name },
|
|
2501
|
+
replaced: existingIdx >= 0,
|
|
2502
|
+
scope: scope.kind
|
|
2503
|
+
};
|
|
2504
|
+
})
|
|
2505
|
+
);
|
|
2506
|
+
models.command("remove <spec>").description("Remove a custom model definition from models.json").option("--global", "Write to the Pi global state root (default)").option("--project-dir <path>", "Write to a project-scoped Pi config").action(
|
|
2507
|
+
async (spec, opts) => runLafsCommand("pi.models.remove", "standard", async () => {
|
|
2508
|
+
const harness = requirePiHarness();
|
|
2509
|
+
const scope = resolveModelsScope(opts);
|
|
2510
|
+
const { provider, id } = parseModelSpec(spec);
|
|
2511
|
+
const config = await harness.readModelsConfig(scope);
|
|
2512
|
+
const providerBlock = config.providers[provider];
|
|
2513
|
+
if (providerBlock === void 0 || providerBlock.models === void 0) {
|
|
2514
|
+
return { removed: false, provider, id, reason: "provider-not-found" };
|
|
2515
|
+
}
|
|
2516
|
+
const before = providerBlock.models.length;
|
|
2517
|
+
const filtered = providerBlock.models.filter((m) => m.id !== id);
|
|
2518
|
+
if (filtered.length === before) {
|
|
2519
|
+
return { removed: false, provider, id, reason: "model-not-found" };
|
|
2520
|
+
}
|
|
2521
|
+
const nextProviderBlock = { ...providerBlock, models: filtered };
|
|
2522
|
+
if (filtered.length === 0) {
|
|
2523
|
+
delete nextProviderBlock.models;
|
|
2524
|
+
}
|
|
2525
|
+
const nextConfig = {
|
|
2526
|
+
providers: { ...config.providers, [provider]: nextProviderBlock }
|
|
2527
|
+
};
|
|
2528
|
+
await harness.writeModelsConfig(nextConfig, scope);
|
|
2529
|
+
return { removed: true, provider, id, scope: scope.kind };
|
|
2530
|
+
})
|
|
2531
|
+
);
|
|
2532
|
+
models.command("enable <spec>").description("Enable a model by appending it to settings.json:enabledModels").option("--global", "Write to the Pi global state root (default)").option("--project-dir <path>", "Write to a project-scoped Pi config").action(
|
|
2533
|
+
async (spec, opts) => runLafsCommand("pi.models.enable", "standard", async () => {
|
|
2534
|
+
const harness = requirePiHarness();
|
|
2535
|
+
const scope = resolveModelsScope(opts);
|
|
2536
|
+
const { provider, id } = parseModelSpec(spec);
|
|
2537
|
+
if (!id.includes("*")) {
|
|
2538
|
+
const config = await harness.readModelsConfig(scope);
|
|
2539
|
+
const providerBlock = config.providers[provider];
|
|
2540
|
+
const defined = providerBlock?.models?.some((m) => m.id === id) ?? false;
|
|
2541
|
+
if (!defined) {
|
|
2542
|
+
}
|
|
2543
|
+
}
|
|
2544
|
+
const current = await harness.readSettings(scope);
|
|
2545
|
+
const currentObj = typeof current === "object" && current !== null && !Array.isArray(current) ? current : {};
|
|
2546
|
+
const enabledRaw = currentObj["enabledModels"];
|
|
2547
|
+
const enabled = Array.isArray(enabledRaw) ? enabledRaw.filter((v) => typeof v === "string") : [];
|
|
2548
|
+
const already = enabled.includes(spec);
|
|
2549
|
+
if (already) {
|
|
2550
|
+
return { enabled: false, reason: "already-enabled", spec, scope: scope.kind };
|
|
2551
|
+
}
|
|
2552
|
+
enabled.push(spec);
|
|
2553
|
+
await harness.writeSettings({ enabledModels: enabled }, scope);
|
|
2554
|
+
return { enabled: true, spec, provider, id, scope: scope.kind };
|
|
2555
|
+
})
|
|
2556
|
+
);
|
|
2557
|
+
models.command("disable <spec>").description("Disable a model by removing it from settings.json:enabledModels").option("--global", "Write to the Pi global state root (default)").option("--project-dir <path>", "Write to a project-scoped Pi config").action(
|
|
2558
|
+
async (spec, opts) => runLafsCommand("pi.models.disable", "standard", async () => {
|
|
2559
|
+
const harness = requirePiHarness();
|
|
2560
|
+
const scope = resolveModelsScope(opts);
|
|
2561
|
+
const current = await harness.readSettings(scope);
|
|
2562
|
+
const currentObj = typeof current === "object" && current !== null && !Array.isArray(current) ? current : {};
|
|
2563
|
+
const enabledRaw = currentObj["enabledModels"];
|
|
2564
|
+
const enabled = Array.isArray(enabledRaw) ? enabledRaw.filter((v) => typeof v === "string") : [];
|
|
2565
|
+
const filtered = enabled.filter((e) => e !== spec);
|
|
2566
|
+
if (filtered.length === enabled.length) {
|
|
2567
|
+
return { disabled: false, reason: "not-enabled", spec, scope: scope.kind };
|
|
2568
|
+
}
|
|
2569
|
+
await harness.writeSettings({ enabledModels: filtered }, scope);
|
|
2570
|
+
return { disabled: true, spec, scope: scope.kind };
|
|
2571
|
+
})
|
|
2572
|
+
);
|
|
2573
|
+
models.command("default <spec>").description("Set settings.json:defaultProvider and defaultModel").option("--global", "Write to the Pi global state root (default)").option("--project-dir <path>", "Write to a project-scoped Pi config").action(
|
|
2574
|
+
async (spec, opts) => runLafsCommand("pi.models.default", "standard", async () => {
|
|
2575
|
+
const harness = requirePiHarness();
|
|
2576
|
+
const scope = resolveModelsScope(opts);
|
|
2577
|
+
const { provider, id } = parseModelSpec(spec);
|
|
2578
|
+
const config = await harness.readModelsConfig(scope);
|
|
2579
|
+
const providerBlock = config.providers[provider];
|
|
2580
|
+
const defined = providerBlock?.models?.some((m) => m.id === id) ?? false;
|
|
2581
|
+
await harness.writeSettings({ defaultProvider: provider, defaultModel: id }, scope);
|
|
2582
|
+
return {
|
|
2583
|
+
set: true,
|
|
2584
|
+
provider,
|
|
2585
|
+
id,
|
|
2586
|
+
knownInModelsJson: defined,
|
|
2587
|
+
scope: scope.kind
|
|
2588
|
+
};
|
|
2589
|
+
})
|
|
2590
|
+
);
|
|
2591
|
+
}
|
|
2592
|
+
|
|
2593
|
+
// src/commands/pi/prompts.ts
|
|
2594
|
+
import { existsSync as existsSync6 } from "fs";
|
|
2595
|
+
import { join as join7, resolve } from "path";
|
|
2596
|
+
function inferPromptName(sourceDir) {
|
|
2597
|
+
const normalized = resolve(sourceDir).replace(/[\\/]+$/, "");
|
|
2598
|
+
const base = normalized.split(/[\\/]/).pop();
|
|
2599
|
+
if (base === void 0 || base.length === 0) {
|
|
2600
|
+
throw new LAFSCommandError(
|
|
2601
|
+
PI_ERROR_CODES.VALIDATION,
|
|
2602
|
+
`Could not infer a prompt name from source: ${sourceDir}`,
|
|
2603
|
+
"Pass --name <name> to override the inferred name.",
|
|
2604
|
+
false
|
|
2605
|
+
);
|
|
2606
|
+
}
|
|
2607
|
+
return base;
|
|
2608
|
+
}
|
|
2609
|
+
function registerPiPromptsCommands(parent) {
|
|
2610
|
+
const prompts = parent.command("prompts").description("Manage Pi prompts across tiers");
|
|
2611
|
+
prompts.command("install <source>").description("Install a Pi prompt directory (contains prompt.md + optional metadata)").option("--scope <tier>", "Install tier: project|user|global (default: project)").option("--name <name>", "Override the inferred prompt name").option("--force", "Overwrite an existing prompt at the target tier").option("--project-dir <path>", "Project directory for the project tier (default: cwd)").action(
|
|
2612
|
+
async (source, opts) => runLafsCommand("pi.prompts.install", "standard", async () => {
|
|
2613
|
+
const harness = requirePiHarness();
|
|
2614
|
+
const tier = parseScope2(opts.scope, "project");
|
|
2615
|
+
const projectDir = resolveProjectDir2(tier, opts.projectDir);
|
|
2616
|
+
const absSource = resolve(source);
|
|
2617
|
+
if (!existsSync6(absSource)) {
|
|
2618
|
+
throw new LAFSCommandError(
|
|
2619
|
+
PI_ERROR_CODES.NOT_FOUND,
|
|
2620
|
+
`Source directory does not exist: ${absSource}`,
|
|
2621
|
+
"Check the path and try again.",
|
|
2622
|
+
false
|
|
2623
|
+
);
|
|
2624
|
+
}
|
|
2625
|
+
if (!existsSync6(join7(absSource, "prompt.md"))) {
|
|
2626
|
+
throw new LAFSCommandError(
|
|
2627
|
+
PI_ERROR_CODES.VALIDATION,
|
|
2628
|
+
`Source directory is missing prompt.md: ${absSource}`,
|
|
2629
|
+
"Add a prompt.md to the source directory and retry.",
|
|
2630
|
+
false
|
|
2631
|
+
);
|
|
2632
|
+
}
|
|
2633
|
+
const name = opts.name ?? inferPromptName(absSource);
|
|
2634
|
+
const installOpts = { force: opts.force ?? false };
|
|
2635
|
+
const result = await harness.installPrompt(absSource, name, tier, projectDir, installOpts);
|
|
2636
|
+
return {
|
|
2637
|
+
installed: {
|
|
2638
|
+
name,
|
|
2639
|
+
tier: result.tier,
|
|
2640
|
+
targetPath: result.targetPath,
|
|
2641
|
+
source: absSource
|
|
2642
|
+
}
|
|
2643
|
+
};
|
|
2644
|
+
})
|
|
2645
|
+
);
|
|
2646
|
+
prompts.command("list").description("List Pi prompts across project, user, and global tiers").option("--project-dir <path>", "Project directory for the project tier (default: cwd)").action(
|
|
2647
|
+
async (opts) => runLafsCommand("pi.prompts.list", "standard", async () => {
|
|
2648
|
+
const harness = requirePiHarness();
|
|
2649
|
+
const projectDir = opts.projectDir ?? process.cwd();
|
|
2650
|
+
const entries = await harness.listPrompts(projectDir);
|
|
2651
|
+
return {
|
|
2652
|
+
count: entries.length,
|
|
2653
|
+
prompts: entries
|
|
2654
|
+
};
|
|
2655
|
+
})
|
|
2656
|
+
);
|
|
2657
|
+
prompts.command("remove <name>").description("Remove a Pi prompt from the given tier").option("--scope <tier>", "Target tier: project|user|global (default: project)").option("--project-dir <path>", "Project directory for the project tier (default: cwd)").action(
|
|
2658
|
+
async (name, opts) => runLafsCommand("pi.prompts.remove", "standard", async () => {
|
|
2659
|
+
const harness = requirePiHarness();
|
|
2660
|
+
const tier = parseScope2(opts.scope, "project");
|
|
2661
|
+
const projectDir = resolveProjectDir2(tier, opts.projectDir);
|
|
2662
|
+
const removed = await harness.removePrompt(name, tier, projectDir);
|
|
2663
|
+
return { name, tier, removed };
|
|
2664
|
+
})
|
|
2665
|
+
);
|
|
2666
|
+
}
|
|
2667
|
+
|
|
2668
|
+
// src/commands/pi/sessions.ts
|
|
2669
|
+
import { spawn } from "child_process";
|
|
2670
|
+
import { createReadStream, createWriteStream, existsSync as existsSync7 } from "fs";
|
|
2671
|
+
import { createInterface } from "readline/promises";
|
|
2672
|
+
async function streamSession(filePath, outputPath, transform) {
|
|
2673
|
+
const writeToFile = outputPath !== void 0 && outputPath.length > 0;
|
|
2674
|
+
const out = writeToFile ? createWriteStream(outputPath) : process.stdout;
|
|
2675
|
+
const reader = createInterface({
|
|
2676
|
+
input: createReadStream(filePath, { encoding: "utf8" }),
|
|
2677
|
+
crlfDelay: Infinity
|
|
2678
|
+
});
|
|
2679
|
+
let emitted = 0;
|
|
2680
|
+
try {
|
|
2681
|
+
for await (const line of reader) {
|
|
2682
|
+
const result = transform(line);
|
|
2683
|
+
if (result === null) continue;
|
|
2684
|
+
out.write(`${result}
|
|
2685
|
+
`);
|
|
2686
|
+
emitted += 1;
|
|
2687
|
+
}
|
|
2688
|
+
} finally {
|
|
2689
|
+
reader.close();
|
|
2690
|
+
if (writeToFile && "end" in out) {
|
|
2691
|
+
await new Promise((resolve3) => {
|
|
2692
|
+
out.end(resolve3);
|
|
2693
|
+
});
|
|
2694
|
+
}
|
|
2695
|
+
}
|
|
2696
|
+
return emitted;
|
|
2697
|
+
}
|
|
2698
|
+
function sessionEntryToMarkdown(line) {
|
|
2699
|
+
let parsed;
|
|
2700
|
+
try {
|
|
2701
|
+
parsed = JSON.parse(line);
|
|
2702
|
+
} catch {
|
|
2703
|
+
return null;
|
|
2704
|
+
}
|
|
2705
|
+
if (typeof parsed !== "object" || parsed === null) return null;
|
|
2706
|
+
const obj = parsed;
|
|
2707
|
+
const type = typeof obj["type"] === "string" ? obj["type"] : null;
|
|
2708
|
+
if (type === "session") {
|
|
2709
|
+
const id = typeof obj["id"] === "string" ? obj["id"] : "(no id)";
|
|
2710
|
+
const ts = typeof obj["timestamp"] === "string" ? obj["timestamp"] : "";
|
|
2711
|
+
return `# Session ${id}${ts.length > 0 ? ` \xB7 ${ts}` : ""}
|
|
2712
|
+
`;
|
|
2713
|
+
}
|
|
2714
|
+
if (type === "message") {
|
|
2715
|
+
const role = typeof obj["role"] === "string" ? obj["role"] : "assistant";
|
|
2716
|
+
const content = extractMessageContent(obj["content"]);
|
|
2717
|
+
if (content === null) return null;
|
|
2718
|
+
const label = role.charAt(0).toUpperCase() + role.slice(1);
|
|
2719
|
+
return `## ${label}
|
|
2720
|
+
|
|
2721
|
+
${content}
|
|
2722
|
+
`;
|
|
2723
|
+
}
|
|
2724
|
+
if (type === "custom_message") {
|
|
2725
|
+
const label = typeof obj["label"] === "string" ? obj["label"] : "Custom";
|
|
2726
|
+
const text = typeof obj["text"] === "string" ? obj["text"] : "";
|
|
2727
|
+
return `### ${label}
|
|
2728
|
+
|
|
2729
|
+
${text}
|
|
2730
|
+
`;
|
|
2731
|
+
}
|
|
2732
|
+
return null;
|
|
2733
|
+
}
|
|
2734
|
+
function extractMessageContent(content) {
|
|
2735
|
+
if (typeof content === "string") return content;
|
|
2736
|
+
if (!Array.isArray(content)) return null;
|
|
2737
|
+
const parts = [];
|
|
2738
|
+
for (const block of content) {
|
|
2739
|
+
if (typeof block === "string") {
|
|
2740
|
+
parts.push(block);
|
|
2741
|
+
continue;
|
|
2742
|
+
}
|
|
2743
|
+
if (typeof block !== "object" || block === null) continue;
|
|
2744
|
+
const b = block;
|
|
2745
|
+
if (b["type"] === "text" && typeof b["text"] === "string") {
|
|
2746
|
+
parts.push(b["text"]);
|
|
2747
|
+
}
|
|
2748
|
+
}
|
|
2749
|
+
if (parts.length === 0) return null;
|
|
2750
|
+
return parts.join("\n\n");
|
|
2751
|
+
}
|
|
2752
|
+
function registerPiSessionsCommands(parent) {
|
|
2753
|
+
const sessions = parent.command("sessions").description("Inspect and resume Pi sessions");
|
|
2754
|
+
sessions.command("list").description("List Pi sessions (reads only line 1 of each JSONL file)").option("--no-subagents", "Skip sessions under subagents/").action(
|
|
2755
|
+
async (opts) => runLafsCommand("pi.sessions.list", "standard", async () => {
|
|
2756
|
+
const harness = requirePiHarness();
|
|
2757
|
+
const summaries = await harness.listSessions({
|
|
2758
|
+
includeSubagents: opts.includeSubagents !== false
|
|
2759
|
+
});
|
|
2760
|
+
return {
|
|
2761
|
+
count: summaries.length,
|
|
2762
|
+
sessions: summaries
|
|
2763
|
+
};
|
|
2764
|
+
})
|
|
2765
|
+
);
|
|
2766
|
+
sessions.command("show <id>").description("Show the full body of a Pi session by id").action(
|
|
2767
|
+
async (id) => runLafsCommand("pi.sessions.show", "full", async () => {
|
|
2768
|
+
const harness = requirePiHarness();
|
|
2769
|
+
const doc = await harness.showSession(id);
|
|
2770
|
+
return {
|
|
2771
|
+
summary: doc.summary,
|
|
2772
|
+
entryCount: doc.entries.length,
|
|
2773
|
+
entries: doc.entries
|
|
2774
|
+
};
|
|
2775
|
+
})
|
|
2776
|
+
);
|
|
2777
|
+
sessions.command("export <id>").description("Export a Pi session to JSONL or Markdown").option("--jsonl", "Emit the raw JSONL body (default)").option("--md", "Emit a Markdown transcription (messages only)").option("--output <path>", "Write to this file instead of stdout").action(
|
|
2778
|
+
async (id, opts) => runLafsCommand("pi.sessions.export", "standard", async () => {
|
|
2779
|
+
if (opts.jsonl === true && opts.md === true) {
|
|
2780
|
+
throw new LAFSCommandError(
|
|
2781
|
+
PI_ERROR_CODES.VALIDATION,
|
|
2782
|
+
"Cannot pass both --jsonl and --md",
|
|
2783
|
+
"Pick one of --jsonl or --md.",
|
|
2784
|
+
false
|
|
2785
|
+
);
|
|
2786
|
+
}
|
|
2787
|
+
const harness = requirePiHarness();
|
|
2788
|
+
const summaries = await harness.listSessions({ includeSubagents: true });
|
|
2789
|
+
const match = summaries.find((s) => s.id === id);
|
|
2790
|
+
if (match === void 0) {
|
|
2791
|
+
throw new LAFSCommandError(
|
|
2792
|
+
PI_ERROR_CODES.NOT_FOUND,
|
|
2793
|
+
`No session found with id ${id}`,
|
|
2794
|
+
"Run `caamp pi sessions list` to see known ids.",
|
|
2795
|
+
false
|
|
2796
|
+
);
|
|
2797
|
+
}
|
|
2798
|
+
const format = opts.md === true ? "md" : "jsonl";
|
|
2799
|
+
const emitted = format === "md" ? await streamSession(match.filePath, opts.output, sessionEntryToMarkdown) : await streamSession(
|
|
2800
|
+
match.filePath,
|
|
2801
|
+
opts.output,
|
|
2802
|
+
(line) => line.length === 0 ? null : line
|
|
2803
|
+
);
|
|
2804
|
+
return {
|
|
2805
|
+
id,
|
|
2806
|
+
format,
|
|
2807
|
+
filePath: match.filePath,
|
|
2808
|
+
output: opts.output ?? "stdout",
|
|
2809
|
+
entriesEmitted: emitted
|
|
2810
|
+
};
|
|
2811
|
+
})
|
|
2812
|
+
);
|
|
2813
|
+
sessions.command("resume <id>").description("Resume a Pi session by shelling out to `pi --session <id>`").action(
|
|
2814
|
+
async (id) => runLafsCommand("pi.sessions.resume", "standard", async () => {
|
|
2815
|
+
const harness = requirePiHarness();
|
|
2816
|
+
const summaries = await harness.listSessions({ includeSubagents: true });
|
|
2817
|
+
const match = summaries.find((s) => s.id === id);
|
|
2818
|
+
if (match === void 0) {
|
|
2819
|
+
throw new LAFSCommandError(
|
|
2820
|
+
PI_ERROR_CODES.NOT_FOUND,
|
|
2821
|
+
`No session found with id ${id}`,
|
|
2822
|
+
"Run `caamp pi sessions list` to see known ids.",
|
|
2823
|
+
false
|
|
2824
|
+
);
|
|
2825
|
+
}
|
|
2826
|
+
const piBinary = harness.provider.detection.binary ?? "pi";
|
|
2827
|
+
if (!existsSync7(piBinary) && piBinary === "pi") {
|
|
2828
|
+
}
|
|
2829
|
+
const child = spawn(piBinary, ["--session", id], {
|
|
2830
|
+
stdio: "inherit",
|
|
2831
|
+
detached: false
|
|
2832
|
+
});
|
|
2833
|
+
const exitCode = await new Promise((resolve3) => {
|
|
2834
|
+
child.on("exit", (code) => resolve3(code ?? 0));
|
|
2835
|
+
});
|
|
2836
|
+
if (exitCode !== 0) {
|
|
2837
|
+
throw new LAFSCommandError(
|
|
2838
|
+
PI_ERROR_CODES.TRANSIENT,
|
|
2839
|
+
`pi --session ${id} exited with code ${exitCode}`,
|
|
2840
|
+
"Check the Pi binary output for details.",
|
|
2841
|
+
true
|
|
2842
|
+
);
|
|
2843
|
+
}
|
|
2844
|
+
return {
|
|
2845
|
+
id,
|
|
2846
|
+
filePath: match.filePath,
|
|
2847
|
+
exitCode
|
|
2848
|
+
};
|
|
2849
|
+
})
|
|
2850
|
+
);
|
|
2851
|
+
}
|
|
2852
|
+
|
|
2853
|
+
// src/commands/pi/themes.ts
|
|
2854
|
+
import { existsSync as existsSync8, statSync } from "fs";
|
|
2855
|
+
import { extname, resolve as resolve2 } from "path";
|
|
2856
|
+
function inferThemeName(sourceFile) {
|
|
2857
|
+
const base = resolve2(sourceFile).split(/[\\/]/).pop();
|
|
2858
|
+
if (base === void 0 || base.length === 0) {
|
|
2859
|
+
throw new LAFSCommandError(
|
|
2860
|
+
PI_ERROR_CODES.VALIDATION,
|
|
2861
|
+
`Could not infer a theme name from source: ${sourceFile}`,
|
|
2862
|
+
"Pass --name <name> to override the inferred name.",
|
|
2863
|
+
false
|
|
2864
|
+
);
|
|
2865
|
+
}
|
|
2866
|
+
const ext = extname(base);
|
|
2867
|
+
if (ext === "") return base;
|
|
2868
|
+
return base.slice(0, -ext.length);
|
|
2869
|
+
}
|
|
2870
|
+
function registerPiThemesCommands(parent) {
|
|
2871
|
+
const themes = parent.command("themes").description("Manage Pi themes across tiers");
|
|
2872
|
+
themes.command("install <source>").description("Install a Pi theme file (.ts/.tsx/.mts/.json)").option("--scope <tier>", "Install tier: project|user|global (default: project)").option("--name <name>", "Override the inferred theme name").option("--force", "Overwrite an existing theme at the target tier").option("--project-dir <path>", "Project directory for the project tier (default: cwd)").action(
|
|
2873
|
+
async (source, opts) => runLafsCommand("pi.themes.install", "standard", async () => {
|
|
2874
|
+
const harness = requirePiHarness();
|
|
2875
|
+
const tier = parseScope2(opts.scope, "project");
|
|
2876
|
+
const projectDir = resolveProjectDir2(tier, opts.projectDir);
|
|
2877
|
+
const absSource = resolve2(source);
|
|
2878
|
+
if (!existsSync8(absSource)) {
|
|
2879
|
+
throw new LAFSCommandError(
|
|
2880
|
+
PI_ERROR_CODES.NOT_FOUND,
|
|
2881
|
+
`Source theme does not exist: ${absSource}`,
|
|
2882
|
+
"Check the path and try again.",
|
|
2883
|
+
false
|
|
2884
|
+
);
|
|
2885
|
+
}
|
|
2886
|
+
const stats = statSync(absSource);
|
|
2887
|
+
if (!stats.isFile()) {
|
|
2888
|
+
throw new LAFSCommandError(
|
|
2889
|
+
PI_ERROR_CODES.VALIDATION,
|
|
2890
|
+
`Source theme is not a regular file: ${absSource}`,
|
|
2891
|
+
"Themes must be a single .ts/.tsx/.mts/.json file.",
|
|
2892
|
+
false
|
|
2893
|
+
);
|
|
2894
|
+
}
|
|
2895
|
+
const name = opts.name ?? inferThemeName(absSource);
|
|
2896
|
+
const installOpts = { force: opts.force ?? false };
|
|
2897
|
+
const result = await harness.installTheme(absSource, name, tier, projectDir, installOpts);
|
|
2898
|
+
return {
|
|
2899
|
+
installed: {
|
|
2900
|
+
name,
|
|
2901
|
+
tier: result.tier,
|
|
2902
|
+
targetPath: result.targetPath,
|
|
2903
|
+
source: absSource
|
|
2904
|
+
}
|
|
2905
|
+
};
|
|
2906
|
+
})
|
|
2907
|
+
);
|
|
2908
|
+
themes.command("list").description("List Pi themes across project, user, and global tiers").option("--project-dir <path>", "Project directory for the project tier (default: cwd)").action(
|
|
2909
|
+
async (opts) => runLafsCommand("pi.themes.list", "standard", async () => {
|
|
2910
|
+
const harness = requirePiHarness();
|
|
2911
|
+
const projectDir = opts.projectDir ?? process.cwd();
|
|
2912
|
+
const entries = await harness.listThemes(projectDir);
|
|
2913
|
+
return {
|
|
2914
|
+
count: entries.length,
|
|
2915
|
+
themes: entries
|
|
2916
|
+
};
|
|
2917
|
+
})
|
|
2918
|
+
);
|
|
2919
|
+
themes.command("remove <name>").description("Remove a Pi theme from the given tier").option("--scope <tier>", "Target tier: project|user|global (default: project)").option("--project-dir <path>", "Project directory for the project tier (default: cwd)").action(
|
|
2920
|
+
async (name, opts) => runLafsCommand("pi.themes.remove", "standard", async () => {
|
|
2921
|
+
const harness = requirePiHarness();
|
|
2922
|
+
const tier = parseScope2(opts.scope, "project");
|
|
2923
|
+
const projectDir = resolveProjectDir2(tier, opts.projectDir);
|
|
2924
|
+
const removed = await harness.removeTheme(name, tier, projectDir);
|
|
2925
|
+
return { name, tier, removed };
|
|
2926
|
+
})
|
|
2927
|
+
);
|
|
2928
|
+
}
|
|
2929
|
+
|
|
2930
|
+
// src/commands/pi/index.ts
|
|
2931
|
+
function registerPiCommands(program2) {
|
|
2932
|
+
const pi = program2.command("pi").description("Pi harness operations (extensions, sessions, models, prompts, themes, cant)");
|
|
2933
|
+
registerPiExtensionsCommands(pi);
|
|
2934
|
+
registerPiSessionsCommands(pi);
|
|
2935
|
+
registerPiModelsCommands(pi);
|
|
2936
|
+
registerPiPromptsCommands(pi);
|
|
2937
|
+
registerPiThemesCommands(pi);
|
|
2938
|
+
registerPiCantCommands(pi);
|
|
2939
|
+
}
|
|
2940
|
+
|
|
1540
2941
|
// src/commands/providers.ts
|
|
1541
2942
|
import { randomUUID as randomUUID3 } from "crypto";
|
|
1542
2943
|
import { resolveOutputFormat as resolveOutputFormat2 } from "@cleocode/lafs";
|
|
@@ -2083,9 +3484,9 @@ CAMP Hook Support (mappings v${getHookMappingsVersion()})
|
|
|
2083
3484
|
console.log(` ${"\u2500".repeat(22)} ${"\u2500".repeat(20)} ${"\u2500".repeat(8)} ${"\u2500".repeat(20)}`);
|
|
2084
3485
|
for (const row of matrix) {
|
|
2085
3486
|
const hooks2 = row.hooksCount > 0 ? String(row.hooksCount) : "-";
|
|
2086
|
-
const
|
|
3487
|
+
const spawn2 = row.spawnMechanism ?? "-";
|
|
2087
3488
|
console.log(
|
|
2088
|
-
` ${row.toolName.padEnd(22)} ${row.skillsPrecedence.padEnd(20)} ${hooks2.padEnd(8)} ${
|
|
3489
|
+
` ${row.toolName.padEnd(22)} ${row.skillsPrecedence.padEnd(20)} ${hooks2.padEnd(8)} ${spawn2}`
|
|
2089
3490
|
);
|
|
2090
3491
|
}
|
|
2091
3492
|
console.log(pc6.dim(`
|
|
@@ -2126,13 +3527,13 @@ function emitJsonError2(operation, mvi, code, message, category, details = {}) {
|
|
|
2126
3527
|
}
|
|
2127
3528
|
|
|
2128
3529
|
// src/commands/skills/audit.ts
|
|
2129
|
-
import { existsSync as
|
|
3530
|
+
import { existsSync as existsSync9, statSync as statSync2 } from "fs";
|
|
2130
3531
|
import pc7 from "picocolors";
|
|
2131
3532
|
function registerSkillsAudit(parent) {
|
|
2132
3533
|
parent.command("audit").description("Security scan skill files (46+ rules, SARIF output)").argument("[path]", "Path to SKILL.md or directory", ".").option("--sarif", "Output in SARIF format (raw SARIF, not LAFS envelope)").option("--json", "Output as JSON (LAFS envelope)").option("--human", "Output in human-readable format").action(async (path, opts) => {
|
|
2133
3534
|
const operation = "skills.audit";
|
|
2134
3535
|
const mvi = "standard";
|
|
2135
|
-
if (!
|
|
3536
|
+
if (!existsSync9(path)) {
|
|
2136
3537
|
const message = `Path not found: ${path}`;
|
|
2137
3538
|
if (opts.sarif) {
|
|
2138
3539
|
console.error(
|
|
@@ -2194,7 +3595,7 @@ function registerSkillsAudit(parent) {
|
|
|
2194
3595
|
);
|
|
2195
3596
|
process.exit(1);
|
|
2196
3597
|
}
|
|
2197
|
-
const stat =
|
|
3598
|
+
const stat = statSync2(path);
|
|
2198
3599
|
let results;
|
|
2199
3600
|
try {
|
|
2200
3601
|
if (stat.isFile()) {
|
|
@@ -2704,9 +4105,9 @@ function emitJsonError3(operation, mvi, code, message, category, details = {}) {
|
|
|
2704
4105
|
}
|
|
2705
4106
|
|
|
2706
4107
|
// src/commands/skills/init.ts
|
|
2707
|
-
import { existsSync as
|
|
2708
|
-
import { mkdir, writeFile } from "fs/promises";
|
|
2709
|
-
import { join as
|
|
4108
|
+
import { existsSync as existsSync10 } from "fs";
|
|
4109
|
+
import { mkdir, writeFile as writeFile3 } from "fs/promises";
|
|
4110
|
+
import { join as join8 } from "path";
|
|
2710
4111
|
import pc10 from "picocolors";
|
|
2711
4112
|
function registerSkillsInit(parent) {
|
|
2712
4113
|
parent.command("init").description("Create a new SKILL.md template").argument("[name]", "Skill name").option("-d, --dir <path>", "Output directory", ".").option("--json", "Output as JSON (default)").option("--human", "Output in human-readable format").action(
|
|
@@ -2732,8 +4133,8 @@ function registerSkillsInit(parent) {
|
|
|
2732
4133
|
process.exit(1);
|
|
2733
4134
|
}
|
|
2734
4135
|
const skillName = name ?? "my-skill";
|
|
2735
|
-
const skillDir =
|
|
2736
|
-
if (
|
|
4136
|
+
const skillDir = join8(opts.dir, skillName);
|
|
4137
|
+
if (existsSync10(skillDir)) {
|
|
2737
4138
|
const message = `Directory already exists: ${skillDir}`;
|
|
2738
4139
|
if (format === "json") {
|
|
2739
4140
|
emitJsonError(
|
|
@@ -2775,7 +4176,7 @@ Provide detailed instructions for the AI agent here.
|
|
|
2775
4176
|
|
|
2776
4177
|
Show example inputs and expected outputs.
|
|
2777
4178
|
`;
|
|
2778
|
-
await
|
|
4179
|
+
await writeFile3(join8(skillDir, "SKILL.md"), template, "utf-8");
|
|
2779
4180
|
const result = {
|
|
2780
4181
|
name: skillName,
|
|
2781
4182
|
directory: skillDir,
|
|
@@ -2789,69 +4190,15 @@ Show example inputs and expected outputs.
|
|
|
2789
4190
|
console.log(pc10.green(`\u2713 Created skill template: ${skillDir}/SKILL.md`));
|
|
2790
4191
|
console.log(pc10.dim("\nNext steps:"));
|
|
2791
4192
|
console.log(pc10.dim(" 1. Edit SKILL.md with your instructions"));
|
|
2792
|
-
console.log(pc10.dim(` 2. Validate: caamp skills validate ${
|
|
4193
|
+
console.log(pc10.dim(` 2. Validate: caamp skills validate ${join8(skillDir, "SKILL.md")}`));
|
|
2793
4194
|
console.log(pc10.dim(` 3. Install: caamp skills install ${skillDir}`));
|
|
2794
4195
|
}
|
|
2795
4196
|
);
|
|
2796
4197
|
}
|
|
2797
4198
|
|
|
2798
4199
|
// src/commands/skills/install.ts
|
|
2799
|
-
import { existsSync as
|
|
4200
|
+
import { existsSync as existsSync11 } from "fs";
|
|
2800
4201
|
import pc11 from "picocolors";
|
|
2801
|
-
|
|
2802
|
-
// src/core/sources/github.ts
|
|
2803
|
-
import { mkdtemp, rm } from "fs/promises";
|
|
2804
|
-
import { tmpdir } from "os";
|
|
2805
|
-
import { join as join4 } from "path";
|
|
2806
|
-
import { simpleGit } from "simple-git";
|
|
2807
|
-
async function cloneRepo(owner, repo, ref, subPath) {
|
|
2808
|
-
const tmpDir = await mkdtemp(join4(tmpdir(), "caamp-"));
|
|
2809
|
-
const repoUrl = `https://github.com/${owner}/${repo}.git`;
|
|
2810
|
-
const git = simpleGit();
|
|
2811
|
-
const cloneOptions = ["--depth", "1"];
|
|
2812
|
-
if (ref) {
|
|
2813
|
-
cloneOptions.push("--branch", ref);
|
|
2814
|
-
}
|
|
2815
|
-
await git.clone(repoUrl, tmpDir, cloneOptions);
|
|
2816
|
-
const localPath = subPath ? join4(tmpDir, subPath) : tmpDir;
|
|
2817
|
-
return {
|
|
2818
|
-
localPath,
|
|
2819
|
-
cleanup: async () => {
|
|
2820
|
-
try {
|
|
2821
|
-
await rm(tmpDir, { recursive: true });
|
|
2822
|
-
} catch {
|
|
2823
|
-
}
|
|
2824
|
-
}
|
|
2825
|
-
};
|
|
2826
|
-
}
|
|
2827
|
-
|
|
2828
|
-
// src/core/sources/gitlab.ts
|
|
2829
|
-
import { mkdtemp as mkdtemp2, rm as rm2 } from "fs/promises";
|
|
2830
|
-
import { tmpdir as tmpdir2 } from "os";
|
|
2831
|
-
import { join as join5 } from "path";
|
|
2832
|
-
import { simpleGit as simpleGit2 } from "simple-git";
|
|
2833
|
-
async function cloneGitLabRepo(owner, repo, ref, subPath) {
|
|
2834
|
-
const tmpDir = await mkdtemp2(join5(tmpdir2(), "caamp-gl-"));
|
|
2835
|
-
const repoUrl = `https://gitlab.com/${owner}/${repo}.git`;
|
|
2836
|
-
const git = simpleGit2();
|
|
2837
|
-
const cloneOptions = ["--depth", "1"];
|
|
2838
|
-
if (ref) {
|
|
2839
|
-
cloneOptions.push("--branch", ref);
|
|
2840
|
-
}
|
|
2841
|
-
await git.clone(repoUrl, tmpDir, cloneOptions);
|
|
2842
|
-
const localPath = subPath ? join5(tmpDir, subPath) : tmpDir;
|
|
2843
|
-
return {
|
|
2844
|
-
localPath,
|
|
2845
|
-
cleanup: async () => {
|
|
2846
|
-
try {
|
|
2847
|
-
await rm2(tmpDir, { recursive: true });
|
|
2848
|
-
} catch {
|
|
2849
|
-
}
|
|
2850
|
-
}
|
|
2851
|
-
};
|
|
2852
|
-
}
|
|
2853
|
-
|
|
2854
|
-
// src/commands/skills/install.ts
|
|
2855
4202
|
function registerSkillsInstall(parent) {
|
|
2856
4203
|
parent.command("install").description("Install a skill from GitHub, URL, marketplace, or registered skill library").argument("[source]", "Skill source (GitHub URL, owner/repo, @author/name, skill-name)").option(
|
|
2857
4204
|
"-a, --agent <name>",
|
|
@@ -3335,7 +4682,7 @@ async function handleMarketplaceSource(source, _providers, _isGlobal, format, op
|
|
|
3335
4682
|
for (const subPath of subPathCandidates) {
|
|
3336
4683
|
try {
|
|
3337
4684
|
const result = await cloneRepo(parsed.owner, parsed.repo, parsed.ref, subPath);
|
|
3338
|
-
if (subPath && !
|
|
4685
|
+
if (subPath && !existsSync11(result.localPath)) {
|
|
3339
4686
|
await result.cleanup();
|
|
3340
4687
|
continue;
|
|
3341
4688
|
}
|
|
@@ -3650,8 +4997,8 @@ ${outdated.length} skill(s) have updates available:
|
|
|
3650
4997
|
if (!opts.yes && format === "human") {
|
|
3651
4998
|
const readline = await import("readline");
|
|
3652
4999
|
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
3653
|
-
const answer = await new Promise((
|
|
3654
|
-
rl.question(pc14.dim("\nProceed with update? [y/N] "),
|
|
5000
|
+
const answer = await new Promise((resolve3) => {
|
|
5001
|
+
rl.question(pc14.dim("\nProceed with update? [y/N] "), resolve3);
|
|
3655
5002
|
});
|
|
3656
5003
|
rl.close();
|
|
3657
5004
|
if (answer.toLowerCase() !== "y" && answer.toLowerCase() !== "yes") {
|
|
@@ -3884,6 +5231,8 @@ registerInstructionsCommands(program);
|
|
|
3884
5231
|
registerConfigCommand(program);
|
|
3885
5232
|
registerDoctorCommand(program);
|
|
3886
5233
|
registerAdvancedCommands(program);
|
|
5234
|
+
registerMcpCommands(program);
|
|
5235
|
+
registerPiCommands(program);
|
|
3887
5236
|
function toError(error) {
|
|
3888
5237
|
if (error instanceof Error) return error;
|
|
3889
5238
|
return new Error(String(error));
|