@overscore/cli 0.13.16 → 0.13.18

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.
Files changed (2) hide show
  1. package/dist/index.js +202 -2
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -1155,6 +1155,7 @@ async function listDashboards() {
1155
1155
  }
1156
1156
  const data = (await res.json());
1157
1157
  console.log(`\n ${data.project} (${data.project_slug})\n`);
1158
+ console.log(" Showing one project — run 'overscore projects' to list them all.\n");
1158
1159
  if (data.dashboards.length === 0) {
1159
1160
  console.log(" No dashboards yet.\n");
1160
1161
  return;
@@ -1173,6 +1174,65 @@ async function listDashboards() {
1173
1174
  console.log();
1174
1175
  }
1175
1176
  }
1177
+ // Account-level project list. Uses the CLI DEVICE token (ocli_) rather than a
1178
+ // project-scoped API key, so it can enumerate EVERY project on the account —
1179
+ // including empty ones a project key could never see.
1180
+ async function listProjects() {
1181
+ const configPath = path.join(process.env.HOME || "", ".overscore", "config");
1182
+ const readToken = () => {
1183
+ const cfg = fs.existsSync(configPath)
1184
+ ? parseEnv(fs.readFileSync(configPath, "utf-8"))
1185
+ : {};
1186
+ return cfg.OVERSCORE_DEVICE_TOKEN;
1187
+ };
1188
+ let deviceToken = readToken();
1189
+ if (!deviceToken) {
1190
+ console.log("\n No session found. Launching browser login...\n");
1191
+ await authLogin();
1192
+ deviceToken = readToken();
1193
+ if (!deviceToken) {
1194
+ console.error("\n Could not authenticate.\n");
1195
+ process.exit(1);
1196
+ }
1197
+ }
1198
+ for (let attempt = 0; attempt < 2; attempt++) {
1199
+ const res = await fetch(`${HUB_URL}/api/projects`, {
1200
+ headers: { Authorization: `Bearer ${deviceToken}` },
1201
+ });
1202
+ if (res.ok) {
1203
+ const { projects } = (await res.json());
1204
+ printProjects(projects);
1205
+ return;
1206
+ }
1207
+ // Device token expired — re-auth once, then retry.
1208
+ if (res.status === 401 && attempt === 0) {
1209
+ console.log("\n Session expired. Launching browser login...\n");
1210
+ await authLogin();
1211
+ deviceToken = readToken();
1212
+ if (!deviceToken)
1213
+ break;
1214
+ continue;
1215
+ }
1216
+ const body = (await res.json().catch(() => ({})));
1217
+ console.error(`\n Error: ${body.error || `HTTP ${res.status}`}\n`);
1218
+ process.exit(1);
1219
+ }
1220
+ console.error("\n Could not authenticate.\n");
1221
+ process.exit(1);
1222
+ }
1223
+ function printProjects(projects) {
1224
+ if (!projects.length) {
1225
+ console.log("\n No projects yet.\n\n Create one: npx create-overscore --dashboard <name> --project <slug>\n");
1226
+ return;
1227
+ }
1228
+ console.log(`\n Your projects (${projects.length})\n`);
1229
+ for (const p of projects) {
1230
+ const count = p.dashboard_count === 1 ? "1 dashboard" : `${p.dashboard_count} dashboards`;
1231
+ console.log(` ${p.name}`);
1232
+ console.log(` slug: ${p.slug} · ${count} · ${p.role}`);
1233
+ console.log();
1234
+ }
1235
+ }
1176
1236
  function collectFiles(dir, prefix) {
1177
1237
  const files = [];
1178
1238
  for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
@@ -1581,6 +1641,123 @@ function syncClaudeMdImports(targetDir) {
1581
1641
  fs.writeFileSync(claudeMdPath, updated);
1582
1642
  return true;
1583
1643
  }
1644
+ /** Recursively copy a directory (files + subdirs), overwriting at the destination. */
1645
+ function copyDirRecursive(src, dest) {
1646
+ fs.mkdirSync(dest, { recursive: true });
1647
+ for (const entry of fs.readdirSync(src, { withFileTypes: true })) {
1648
+ const s = path.join(src, entry.name);
1649
+ const d = path.join(dest, entry.name);
1650
+ if (entry.isDirectory())
1651
+ copyDirRecursive(s, d);
1652
+ else if (entry.isFile())
1653
+ fs.copyFileSync(s, d);
1654
+ }
1655
+ }
1656
+ /**
1657
+ * Derive a cross-tool AGENTS.md from an existing .claude/CLAUDE.md. Codex,
1658
+ * Cursor, Copilot, and Gemini read AGENTS.md but do NOT resolve Claude Code's
1659
+ * `@import` lines, so the leading @import block becomes prose "read when
1660
+ * relevant" pointers, a reference-guides list of .claude/rules/*.md is added,
1661
+ * and the rest of the CLAUDE.md body is kept verbatim. Returns null if the
1662
+ * CLAUDE.md doesn't look like an Overscore artifact.
1663
+ */
1664
+ function deriveAgentsMdFromClaudeMd(targetDir, content) {
1665
+ const lines = content.split("\n");
1666
+ const headingIdx = lines.findIndex((l) => /^#\s+Overscore\s+(Dashboard|Analysis)/i.test(l));
1667
+ if (headingIdx === -1)
1668
+ return null;
1669
+ // The @import lines follow the heading (blank lines allowed between); the
1670
+ // first other non-blank line begins the body.
1671
+ const importTargets = [];
1672
+ let bodyStart = headingIdx + 1;
1673
+ for (let i = headingIdx + 1; i < lines.length; i++) {
1674
+ const t = lines[i].trim();
1675
+ if (t === "") {
1676
+ bodyStart = i + 1;
1677
+ continue;
1678
+ }
1679
+ if (t.startsWith("@")) {
1680
+ importTargets.push(t.slice(1));
1681
+ bodyStart = i + 1;
1682
+ continue;
1683
+ }
1684
+ bodyStart = i;
1685
+ break;
1686
+ }
1687
+ const body = lines.slice(bodyStart).join("\n").replace(/^\n+/, "");
1688
+ const memBullets = importTargets.length
1689
+ ? importTargets.map((p) => `- **${p}** — load when relevant to your task.`).join("\n")
1690
+ : "- (this project has no shared context files yet)";
1691
+ // Reference guides: every .claude/rules/*.md except the per-artifact memory
1692
+ // file (already listed above as a memory pointer).
1693
+ const memBasenames = new Set(importTargets.map((p) => path.basename(p)));
1694
+ const rulesDir = path.join(targetDir, ".claude", "rules");
1695
+ let refBlock = "";
1696
+ if (fs.existsSync(rulesDir)) {
1697
+ const ruleFiles = fs
1698
+ .readdirSync(rulesDir)
1699
+ .filter((f) => f.endsWith(".md") &&
1700
+ !memBasenames.has(f) &&
1701
+ f !== "this-dashboard.md" &&
1702
+ f !== "this-analysis.md")
1703
+ .sort();
1704
+ if (ruleFiles.length > 0) {
1705
+ refBlock =
1706
+ "\n## Reference guides — read the relevant one when the task calls for it\n\n" +
1707
+ "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" +
1708
+ ruleFiles.map((f) => `- **.claude/rules/${f}**`).join("\n") +
1709
+ "\n";
1710
+ }
1711
+ }
1712
+ return `${lines[headingIdx]}
1713
+
1714
+ ## Project instructions and memory
1715
+
1716
+ 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:
1717
+
1718
+ ${memBullets}
1719
+ ${refBlock}
1720
+ ${body}`;
1721
+ }
1722
+ /**
1723
+ * Ensure the cross-tool layout (Codex / Cursor / Copilot / Gemini) exists
1724
+ * alongside Claude Code's .claude/:
1725
+ * - .agents/skills/ — the cross-tool Agent Skills location, mirrored from
1726
+ * .claude/skills so it stays current as the Hub OTA updates skills.
1727
+ * - AGENTS.md — a root instruction file derived from .claude/CLAUDE.md,
1728
+ * created only if one doesn't already exist (never clobbers user edits).
1729
+ * This is what retrofits an existing (Claude-only) project for Codex on the
1730
+ * next deploy / pull / `platform update`. Returns paths newly created, for
1731
+ * reporting. Best-effort: never throws.
1732
+ */
1733
+ function ensureCodexLayout(targetDir) {
1734
+ const created = [];
1735
+ try {
1736
+ const claudeSkills = path.join(targetDir, ".claude", "skills");
1737
+ if (fs.existsSync(claudeSkills)) {
1738
+ const agentsSkills = path.join(targetDir, ".agents", "skills");
1739
+ const existed = fs.existsSync(agentsSkills);
1740
+ // Re-mirror every sync so new/updated skills propagate; only report the
1741
+ // first-time creation to keep steady-state syncs quiet.
1742
+ copyDirRecursive(claudeSkills, agentsSkills);
1743
+ if (!existed)
1744
+ created.push(".agents/skills/");
1745
+ }
1746
+ const agentsMdPath = path.join(targetDir, "AGENTS.md");
1747
+ const claudeMdPath = path.join(targetDir, ".claude", "CLAUDE.md");
1748
+ if (!fs.existsSync(agentsMdPath) && fs.existsSync(claudeMdPath)) {
1749
+ const agents = deriveAgentsMdFromClaudeMd(targetDir, fs.readFileSync(claudeMdPath, "utf-8"));
1750
+ if (agents) {
1751
+ fs.writeFileSync(agentsMdPath, agents);
1752
+ created.push("AGENTS.md");
1753
+ }
1754
+ }
1755
+ }
1756
+ catch {
1757
+ // Non-fatal — cross-tool layout is best-effort.
1758
+ }
1759
+ return created;
1760
+ }
1584
1761
  function platformMetaPath(targetDir) {
1585
1762
  return path.join(targetDir, ".claude", ".platform-meta.json");
1586
1763
  }
@@ -1696,8 +1873,14 @@ async function syncPlatformRules(targetDir, baseUrl, apiKey) {
1696
1873
  }
1697
1874
  writePlatformMeta(targetDir, meta);
1698
1875
  const claudeUpdated = syncClaudeMdImports(targetDir);
1699
- if (written.length > 0 || claudeUpdated) {
1700
- console.log(` Platform rules synced (${written.length} file${written.length === 1 ? "" : "s"}${claudeUpdated ? " + CLAUDE.md updated" : ""}).`);
1876
+ const codexCreated = ensureCodexLayout(targetDir);
1877
+ if (written.length > 0 || claudeUpdated || codexCreated.length > 0) {
1878
+ const extras = [];
1879
+ if (claudeUpdated)
1880
+ extras.push("CLAUDE.md updated");
1881
+ if (codexCreated.length > 0)
1882
+ extras.push(codexCreated.join(" + ") + " added for Codex");
1883
+ console.log(` Platform rules synced (${written.length} file${written.length === 1 ? "" : "s"}${extras.length ? " + " + extras.join(" + ") : ""}).`);
1701
1884
  }
1702
1885
  if (preserved.length > 0) {
1703
1886
  console.log(` Preserved ${preserved.length} locally-modified file${preserved.length === 1 ? "" : "s"} (Hub updates skipped):`);
@@ -1784,11 +1967,23 @@ async function platformUpdate(checkOnly = false, force = false) {
1784
1967
  }
1785
1968
  if (claudeWouldChange)
1786
1969
  updated.push(".claude/CLAUDE.md (@imports would be added)");
1970
+ // Cross-tool (Codex/Cursor/Copilot/Gemini) layout a real run would create.
1971
+ if (!fs.existsSync(path.join(process.cwd(), "AGENTS.md")) &&
1972
+ fs.existsSync(path.join(process.cwd(), ".claude", "CLAUDE.md"))) {
1973
+ updated.push("AGENTS.md (would be created for Codex/cross-tool)");
1974
+ }
1975
+ if (fs.existsSync(path.join(process.cwd(), ".claude", "skills")) &&
1976
+ !fs.existsSync(path.join(process.cwd(), ".agents", "skills"))) {
1977
+ updated.push(".agents/skills/ (would be created for Codex/cross-tool)");
1978
+ }
1787
1979
  }
1788
1980
  else {
1789
1981
  const claudeUpdated = syncClaudeMdImports(process.cwd());
1790
1982
  if (claudeUpdated)
1791
1983
  updated.push(".claude/CLAUDE.md (@imports updated)");
1984
+ for (const p of ensureCodexLayout(process.cwd())) {
1985
+ updated.push(`${p} (created for Codex/cross-tool)`);
1986
+ }
1792
1987
  }
1793
1988
  if (updated.length === 0 && preserved.length === 0) {
1794
1989
  console.log(" Already up to date — no changes.\n");
@@ -2917,6 +3112,9 @@ else if (command === "versions") {
2917
3112
  else if (command === "list") {
2918
3113
  listDashboards();
2919
3114
  }
3115
+ else if (command === "projects") {
3116
+ listProjects();
3117
+ }
2920
3118
  else if (command === "install-skills") {
2921
3119
  installSkills();
2922
3120
  }
@@ -3081,6 +3279,7 @@ else {
3081
3279
  auth logout Remove saved credentials
3082
3280
  dev Start the dev server (injects auth automatically)
3083
3281
  list List all dashboards in the project
3282
+ projects List every project on your account
3084
3283
  deploy Build and deploy the dashboard
3085
3284
  pull <slug> Pull source code from the hub
3086
3285
  versions List deploy history
@@ -3093,6 +3292,7 @@ else {
3093
3292
  npx @overscore/cli auth login
3094
3293
  npx @overscore/cli dev
3095
3294
  npx @overscore/cli list
3295
+ npx @overscore/cli projects
3096
3296
  npx @overscore/cli deploy --message "description"
3097
3297
  npx @overscore/cli pull <slug>
3098
3298
  npx @overscore/cli pull <slug> --version 3
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@overscore/cli",
3
- "version": "0.13.16",
3
+ "version": "0.13.18",
4
4
  "description": "CLI for deploying Overscore dashboards and publishing analyses",
5
5
  "bin": {
6
6
  "overscore": "dist/index.js"