@overscore/cli 0.13.15 → 0.13.17
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/index.js +175 -28
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -913,7 +913,7 @@ function formatCell(value) {
|
|
|
913
913
|
}
|
|
914
914
|
async function pull(slug) {
|
|
915
915
|
if (!slug) {
|
|
916
|
-
console.error("\n Usage: npx @overscore/cli pull <dashboard-slug> [--version N]\n");
|
|
916
|
+
console.error("\n Usage: npx @overscore/cli pull <dashboard-slug> [--version N] [--force]\n");
|
|
917
917
|
console.error(" Example:");
|
|
918
918
|
console.error(" npx @overscore/cli pull my-dashboard\n");
|
|
919
919
|
process.exit(1);
|
|
@@ -923,35 +923,47 @@ async function pull(slug) {
|
|
|
923
923
|
const versionParam = versionIndex !== -1 && process.argv[versionIndex + 1]
|
|
924
924
|
? process.argv[versionIndex + 1]
|
|
925
925
|
: null;
|
|
926
|
-
//
|
|
926
|
+
// --force skips the "directory exists, overwrite?" confirm — needed for non-interactive
|
|
927
|
+
// re-pulls (scripted rebuilds / `workspace sync`). Source is versioned in the Hub, and the
|
|
928
|
+
// clean step below already preserves node_modules + .env, so overwriting is safe.
|
|
929
|
+
const force = process.argv.includes("--force");
|
|
930
|
+
// Resolve API key: device token → auto-create os_ key, else the saved project key, else prompt.
|
|
927
931
|
let apiKey = "";
|
|
928
932
|
const configPath = path.join(process.env.HOME || "", ".overscore", "config");
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
})
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
if (keyRes.ok) {
|
|
947
|
-
const keyData = (await keyRes.json());
|
|
948
|
-
apiKey = keyData.raw_key || "";
|
|
949
|
-
}
|
|
933
|
+
const cfg = fs.existsSync(configPath)
|
|
934
|
+
? parseEnv(fs.readFileSync(configPath, "utf-8"))
|
|
935
|
+
: {};
|
|
936
|
+
const deviceToken = cfg.OVERSCORE_DEVICE_TOKEN;
|
|
937
|
+
if (deviceToken) {
|
|
938
|
+
try {
|
|
939
|
+
const keyRes = await fetch(`${HUB_URL}/api/projects/${slug}/api-keys`, {
|
|
940
|
+
method: "POST",
|
|
941
|
+
headers: {
|
|
942
|
+
Authorization: `Bearer ${deviceToken}`,
|
|
943
|
+
"Content-Type": "application/json",
|
|
944
|
+
},
|
|
945
|
+
body: JSON.stringify({ name: `${slug} pull key` }),
|
|
946
|
+
});
|
|
947
|
+
if (keyRes.status === 401 || keyRes.status === 403) {
|
|
948
|
+
console.error("\n Error: Device token is invalid or expired. Run 'npx @overscore/cli auth login' to re-authenticate.\n");
|
|
949
|
+
process.exit(1);
|
|
950
950
|
}
|
|
951
|
-
|
|
952
|
-
|
|
951
|
+
if (keyRes.ok) {
|
|
952
|
+
const keyData = (await keyRes.json());
|
|
953
|
+
apiKey = keyData.raw_key || "";
|
|
953
954
|
}
|
|
954
955
|
}
|
|
956
|
+
catch {
|
|
957
|
+
// Network error — fall through to the saved key / prompt
|
|
958
|
+
}
|
|
959
|
+
}
|
|
960
|
+
// The mint above targets /api/projects/<slug>, but `slug` is a DASHBOARD slug — for any
|
|
961
|
+
// project whose name differs from the dashboard (i.e. any multi-dashboard project) that
|
|
962
|
+
// 404s and leaves apiKey empty. Fall back to the project API key saved by `auth login`:
|
|
963
|
+
// it authorizes every dashboard in the logged-in project, which is the common pull case
|
|
964
|
+
// (and what makes non-interactive `pull` / scripted rebuilds work).
|
|
965
|
+
if (!apiKey && cfg.OVERSCORE_API_KEY) {
|
|
966
|
+
apiKey = cfg.OVERSCORE_API_KEY;
|
|
955
967
|
}
|
|
956
968
|
if (!apiKey) {
|
|
957
969
|
console.log(" Tip: Run 'npx @overscore/cli auth login' to authenticate once and skip this step.");
|
|
@@ -989,7 +1001,7 @@ async function pull(slug) {
|
|
|
989
1001
|
const zipBuffer = Buffer.from(await res.arrayBuffer());
|
|
990
1002
|
// Extract to ./<slug>/
|
|
991
1003
|
const targetDir = path.resolve(process.cwd(), slug);
|
|
992
|
-
if (fs.existsSync(targetDir)) {
|
|
1004
|
+
if (fs.existsSync(targetDir) && !force) {
|
|
993
1005
|
const yes = await confirm(`Directory "${slug}" already exists. Overwrite source files?`);
|
|
994
1006
|
if (!yes) {
|
|
995
1007
|
console.log(" Cancelled.\n");
|
|
@@ -1569,6 +1581,123 @@ function syncClaudeMdImports(targetDir) {
|
|
|
1569
1581
|
fs.writeFileSync(claudeMdPath, updated);
|
|
1570
1582
|
return true;
|
|
1571
1583
|
}
|
|
1584
|
+
/** Recursively copy a directory (files + subdirs), overwriting at the destination. */
|
|
1585
|
+
function copyDirRecursive(src, dest) {
|
|
1586
|
+
fs.mkdirSync(dest, { recursive: true });
|
|
1587
|
+
for (const entry of fs.readdirSync(src, { withFileTypes: true })) {
|
|
1588
|
+
const s = path.join(src, entry.name);
|
|
1589
|
+
const d = path.join(dest, entry.name);
|
|
1590
|
+
if (entry.isDirectory())
|
|
1591
|
+
copyDirRecursive(s, d);
|
|
1592
|
+
else if (entry.isFile())
|
|
1593
|
+
fs.copyFileSync(s, d);
|
|
1594
|
+
}
|
|
1595
|
+
}
|
|
1596
|
+
/**
|
|
1597
|
+
* Derive a cross-tool AGENTS.md from an existing .claude/CLAUDE.md. Codex,
|
|
1598
|
+
* Cursor, Copilot, and Gemini read AGENTS.md but do NOT resolve Claude Code's
|
|
1599
|
+
* `@import` lines, so the leading @import block becomes prose "read when
|
|
1600
|
+
* relevant" pointers, a reference-guides list of .claude/rules/*.md is added,
|
|
1601
|
+
* and the rest of the CLAUDE.md body is kept verbatim. Returns null if the
|
|
1602
|
+
* CLAUDE.md doesn't look like an Overscore artifact.
|
|
1603
|
+
*/
|
|
1604
|
+
function deriveAgentsMdFromClaudeMd(targetDir, content) {
|
|
1605
|
+
const lines = content.split("\n");
|
|
1606
|
+
const headingIdx = lines.findIndex((l) => /^#\s+Overscore\s+(Dashboard|Analysis)/i.test(l));
|
|
1607
|
+
if (headingIdx === -1)
|
|
1608
|
+
return null;
|
|
1609
|
+
// The @import lines follow the heading (blank lines allowed between); the
|
|
1610
|
+
// first other non-blank line begins the body.
|
|
1611
|
+
const importTargets = [];
|
|
1612
|
+
let bodyStart = headingIdx + 1;
|
|
1613
|
+
for (let i = headingIdx + 1; i < lines.length; i++) {
|
|
1614
|
+
const t = lines[i].trim();
|
|
1615
|
+
if (t === "") {
|
|
1616
|
+
bodyStart = i + 1;
|
|
1617
|
+
continue;
|
|
1618
|
+
}
|
|
1619
|
+
if (t.startsWith("@")) {
|
|
1620
|
+
importTargets.push(t.slice(1));
|
|
1621
|
+
bodyStart = i + 1;
|
|
1622
|
+
continue;
|
|
1623
|
+
}
|
|
1624
|
+
bodyStart = i;
|
|
1625
|
+
break;
|
|
1626
|
+
}
|
|
1627
|
+
const body = lines.slice(bodyStart).join("\n").replace(/^\n+/, "");
|
|
1628
|
+
const memBullets = importTargets.length
|
|
1629
|
+
? importTargets.map((p) => `- **${p}** — load when relevant to your task.`).join("\n")
|
|
1630
|
+
: "- (this project has no shared context files yet)";
|
|
1631
|
+
// Reference guides: every .claude/rules/*.md except the per-artifact memory
|
|
1632
|
+
// file (already listed above as a memory pointer).
|
|
1633
|
+
const memBasenames = new Set(importTargets.map((p) => path.basename(p)));
|
|
1634
|
+
const rulesDir = path.join(targetDir, ".claude", "rules");
|
|
1635
|
+
let refBlock = "";
|
|
1636
|
+
if (fs.existsSync(rulesDir)) {
|
|
1637
|
+
const ruleFiles = fs
|
|
1638
|
+
.readdirSync(rulesDir)
|
|
1639
|
+
.filter((f) => f.endsWith(".md") &&
|
|
1640
|
+
!memBasenames.has(f) &&
|
|
1641
|
+
f !== "this-dashboard.md" &&
|
|
1642
|
+
f !== "this-analysis.md")
|
|
1643
|
+
.sort();
|
|
1644
|
+
if (ruleFiles.length > 0) {
|
|
1645
|
+
refBlock =
|
|
1646
|
+
"\n## Reference guides — read the relevant one when the task calls for it\n\n" +
|
|
1647
|
+
"This project's authoring rules live in these files. Read the relevant one on demand; you don't need to read them all every session.\n\n" +
|
|
1648
|
+
ruleFiles.map((f) => `- **.claude/rules/${f}**`).join("\n") +
|
|
1649
|
+
"\n";
|
|
1650
|
+
}
|
|
1651
|
+
}
|
|
1652
|
+
return `${lines[headingIdx]}
|
|
1653
|
+
|
|
1654
|
+
## Project instructions and memory
|
|
1655
|
+
|
|
1656
|
+
This project keeps its context in Markdown files. You don't need to read all of them every session; load the ones relevant to your task:
|
|
1657
|
+
|
|
1658
|
+
${memBullets}
|
|
1659
|
+
${refBlock}
|
|
1660
|
+
${body}`;
|
|
1661
|
+
}
|
|
1662
|
+
/**
|
|
1663
|
+
* Ensure the cross-tool layout (Codex / Cursor / Copilot / Gemini) exists
|
|
1664
|
+
* alongside Claude Code's .claude/:
|
|
1665
|
+
* - .agents/skills/ — the cross-tool Agent Skills location, mirrored from
|
|
1666
|
+
* .claude/skills so it stays current as the Hub OTA updates skills.
|
|
1667
|
+
* - AGENTS.md — a root instruction file derived from .claude/CLAUDE.md,
|
|
1668
|
+
* created only if one doesn't already exist (never clobbers user edits).
|
|
1669
|
+
* This is what retrofits an existing (Claude-only) project for Codex on the
|
|
1670
|
+
* next deploy / pull / `platform update`. Returns paths newly created, for
|
|
1671
|
+
* reporting. Best-effort: never throws.
|
|
1672
|
+
*/
|
|
1673
|
+
function ensureCodexLayout(targetDir) {
|
|
1674
|
+
const created = [];
|
|
1675
|
+
try {
|
|
1676
|
+
const claudeSkills = path.join(targetDir, ".claude", "skills");
|
|
1677
|
+
if (fs.existsSync(claudeSkills)) {
|
|
1678
|
+
const agentsSkills = path.join(targetDir, ".agents", "skills");
|
|
1679
|
+
const existed = fs.existsSync(agentsSkills);
|
|
1680
|
+
// Re-mirror every sync so new/updated skills propagate; only report the
|
|
1681
|
+
// first-time creation to keep steady-state syncs quiet.
|
|
1682
|
+
copyDirRecursive(claudeSkills, agentsSkills);
|
|
1683
|
+
if (!existed)
|
|
1684
|
+
created.push(".agents/skills/");
|
|
1685
|
+
}
|
|
1686
|
+
const agentsMdPath = path.join(targetDir, "AGENTS.md");
|
|
1687
|
+
const claudeMdPath = path.join(targetDir, ".claude", "CLAUDE.md");
|
|
1688
|
+
if (!fs.existsSync(agentsMdPath) && fs.existsSync(claudeMdPath)) {
|
|
1689
|
+
const agents = deriveAgentsMdFromClaudeMd(targetDir, fs.readFileSync(claudeMdPath, "utf-8"));
|
|
1690
|
+
if (agents) {
|
|
1691
|
+
fs.writeFileSync(agentsMdPath, agents);
|
|
1692
|
+
created.push("AGENTS.md");
|
|
1693
|
+
}
|
|
1694
|
+
}
|
|
1695
|
+
}
|
|
1696
|
+
catch {
|
|
1697
|
+
// Non-fatal — cross-tool layout is best-effort.
|
|
1698
|
+
}
|
|
1699
|
+
return created;
|
|
1700
|
+
}
|
|
1572
1701
|
function platformMetaPath(targetDir) {
|
|
1573
1702
|
return path.join(targetDir, ".claude", ".platform-meta.json");
|
|
1574
1703
|
}
|
|
@@ -1684,8 +1813,14 @@ async function syncPlatformRules(targetDir, baseUrl, apiKey) {
|
|
|
1684
1813
|
}
|
|
1685
1814
|
writePlatformMeta(targetDir, meta);
|
|
1686
1815
|
const claudeUpdated = syncClaudeMdImports(targetDir);
|
|
1687
|
-
|
|
1688
|
-
|
|
1816
|
+
const codexCreated = ensureCodexLayout(targetDir);
|
|
1817
|
+
if (written.length > 0 || claudeUpdated || codexCreated.length > 0) {
|
|
1818
|
+
const extras = [];
|
|
1819
|
+
if (claudeUpdated)
|
|
1820
|
+
extras.push("CLAUDE.md updated");
|
|
1821
|
+
if (codexCreated.length > 0)
|
|
1822
|
+
extras.push(codexCreated.join(" + ") + " added for Codex");
|
|
1823
|
+
console.log(` Platform rules synced (${written.length} file${written.length === 1 ? "" : "s"}${extras.length ? " + " + extras.join(" + ") : ""}).`);
|
|
1689
1824
|
}
|
|
1690
1825
|
if (preserved.length > 0) {
|
|
1691
1826
|
console.log(` Preserved ${preserved.length} locally-modified file${preserved.length === 1 ? "" : "s"} (Hub updates skipped):`);
|
|
@@ -1772,11 +1907,23 @@ async function platformUpdate(checkOnly = false, force = false) {
|
|
|
1772
1907
|
}
|
|
1773
1908
|
if (claudeWouldChange)
|
|
1774
1909
|
updated.push(".claude/CLAUDE.md (@imports would be added)");
|
|
1910
|
+
// Cross-tool (Codex/Cursor/Copilot/Gemini) layout a real run would create.
|
|
1911
|
+
if (!fs.existsSync(path.join(process.cwd(), "AGENTS.md")) &&
|
|
1912
|
+
fs.existsSync(path.join(process.cwd(), ".claude", "CLAUDE.md"))) {
|
|
1913
|
+
updated.push("AGENTS.md (would be created for Codex/cross-tool)");
|
|
1914
|
+
}
|
|
1915
|
+
if (fs.existsSync(path.join(process.cwd(), ".claude", "skills")) &&
|
|
1916
|
+
!fs.existsSync(path.join(process.cwd(), ".agents", "skills"))) {
|
|
1917
|
+
updated.push(".agents/skills/ (would be created for Codex/cross-tool)");
|
|
1918
|
+
}
|
|
1775
1919
|
}
|
|
1776
1920
|
else {
|
|
1777
1921
|
const claudeUpdated = syncClaudeMdImports(process.cwd());
|
|
1778
1922
|
if (claudeUpdated)
|
|
1779
1923
|
updated.push(".claude/CLAUDE.md (@imports updated)");
|
|
1924
|
+
for (const p of ensureCodexLayout(process.cwd())) {
|
|
1925
|
+
updated.push(`${p} (created for Codex/cross-tool)`);
|
|
1926
|
+
}
|
|
1780
1927
|
}
|
|
1781
1928
|
if (updated.length === 0 && preserved.length === 0) {
|
|
1782
1929
|
console.log(" Already up to date — no changes.\n");
|