fix(linkedin-studio): anchor analytics root on plugin marker + surface npm install

Wave 2 / Step 4 of the remediation plan (docs/remediation/plan.md).

PRIMARY (the real fresh-clone failure):
- scripts/analytics/node_modules is gitignored, so a fresh clone has neither
  tsx nor csv-parse. Surface an idempotent `npm install --silent` prerequisite
  at point-of-use in report.md (Step 1b) and import.md (Step 4).

DEVIATION FROM PLAN (correction-in-scope, to satisfy the plan's own Verify gate):
- The plan assumed prepending `npm install` was sufficient. Verified it is NOT:
  the commands invoke the CLI with an absolute src/cli.ts path but from the
  user's arbitrary CWD, and `node --import tsx` resolves the `tsx` specifier
  relative to CWD, not the script. There is no global tsx, so the call still
  fails with ERR_MODULE_NOT_FOUND from any CWD other than scripts/analytics.
- Complete fix: invoke the locally-installed tsx by its absolute
  node_modules/.bin/tsx path in all CLI calls (report.md x10, import.md x3), so
  they resolve from any working directory once the install above has run.
  Verified: 0 ERR_MODULE_NOT_FOUND running `report` from /tmp.

SECONDARY (latent correctness / hardening):
- Add findPluginRoot(): walks up to the dir holding .claude-plugin/plugin.json
  and anchors getAnalyticsRoot() on it, falling back to the legacy 4-up count.
  MEASURED that ../../../../ already resolved to the plugin root from BOTH
  src/utils and build/utils (both 4 levels deep), so the plan's "src-vs-build
  depth miscalibration" premise was false — this is correct-by-construction
  hardening (survives a future source move), not a live-bug fix.
- Reconcile cli.ts usage/help text: `node build/cli.js` -> `node --import tsx
  src/cli.ts` (the real runtime).
- Fix report.md troubleshooting: "Verify tsx is available" -> the actual
  install command on ERR_MODULE_NOT_FOUND.

Test-first: scripts/analytics/tests/storage-root.test.ts (red on missing
findPluginRoot export, green after). Full suite 106/106, tsc --noEmit clean,
structural lint 0 failed.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Kjell Tore Guttormsen 2026-05-29 21:18:33 +02:00
commit 798484bf0c
5 changed files with 159 additions and 29 deletions

View file

@ -27,11 +27,11 @@ function printUsage() {
LinkedIn Analytics CLI
Usage:
node build/cli.js import <filename> Import a CSV export
node build/cli.js report [--week W] Generate weekly report
node build/cli.js report --month YYYY-MM Generate monthly report with MoM comparison
node build/cli.js trends [--period P] [--metric M] Show trends and alerts
node build/cli.js heatmap Day-of-week performance matrix
node --import tsx src/cli.ts import <filename> Import a CSV export
node --import tsx src/cli.ts report [--week W] Generate weekly report
node --import tsx src/cli.ts report --month YYYY-MM Generate monthly report with MoM comparison
node --import tsx src/cli.ts trends [--period P] [--metric M] Show trends and alerts
node --import tsx src/cli.ts heatmap Day-of-week performance matrix
Options:
--week W ISO week (e.g., 2026-W05), defaults to current week
@ -39,9 +39,9 @@ Options:
--metric M Metric to analyze: "impressions" | "reactions" | "comments" | "shares" | "clicks" | "engagementRate" (default: "impressions")
Examples:
node build/cli.js import linkedin-export-2026-01-20.csv
node build/cli.js report --week 2026-W04
node build/cli.js trends --period quarter --metric engagementRate
node --import tsx src/cli.ts import linkedin-export-2026-01-20.csv
node --import tsx src/cli.ts report --week 2026-W04
node --import tsx src/cli.ts trends --period quarter --metric engagementRate
`);
}
@ -50,7 +50,7 @@ async function handleImport(root: string, args: string[]) {
if (!filename) {
console.error("Error: Missing filename argument");
console.error("Usage: node build/cli.js import <filename>");
console.error("Usage: node --import tsx src/cli.ts import <filename>");
process.exit(1);
}

View file

@ -6,18 +6,46 @@ import type { AnalyticsBatch, WeeklyReport, MonthlyReport, PostAnalytics } from
const __dirname = dirname(fileURLToPath(import.meta.url));
/**
* Get the analytics root directory from environment or default location
* Default is assets/analytics relative to the plugin root
* Walk up from `startDir` until a directory containing `.claude-plugin/plugin.json`
* is found. Returns that directory, or null if no marker is found before the
* filesystem root.
*
* Anchoring on the plugin marker keeps the analytics root correct by
* construction regardless of whether this module runs from `src/utils/`
* (under tsx) or `build/utils/` (compiled) and survives a future source
* move that a hardcoded "../../../../" count would silently break.
*/
export function findPluginRoot(startDir: string): string | null {
let dir = resolve(startDir);
// dirname() of the filesystem root returns the root itself; stop when it
// no longer changes.
for (;;) {
if (existsSync(join(dir, ".claude-plugin", "plugin.json"))) {
return dir;
}
const parent = dirname(dir);
if (parent === dir) {
return null;
}
dir = parent;
}
}
/**
* Get the analytics root directory from environment or default location.
* Default is assets/analytics under the plugin root (the dir holding
* .claude-plugin/plugin.json). The ANALYTICS_ROOT env override is the test seam.
*/
export function getAnalyticsRoot(): string {
if (process.env.ANALYTICS_ROOT) {
return resolve(process.env.ANALYTICS_ROOT);
}
// Build output is at: scripts/analytics/build/utils/storage.js
// Plugin root is 4 levels up: ../../../../
// Then assets/analytics from there
const pluginRoot = resolve(__dirname, "../../../../");
// Anchor on the .claude-plugin/plugin.json marker. Fall back to the legacy
// 4-levels-up count (scripts/analytics/{src,build}/utils -> plugin root) only
// if no marker is found (e.g. an unusual extraction without the manifest).
const pluginRoot = findPluginRoot(__dirname) ?? resolve(__dirname, "../../../../");
return join(pluginRoot, "assets", "analytics");
}