refactor(linkedin-studio): M0-2 — storage.ts getDataRoot + external default (ANALYTICS_ROOT alias kept)
This commit is contained in:
parent
ce0f17077c
commit
843e8a3a65
2 changed files with 91 additions and 42 deletions
|
|
@ -1,19 +1,19 @@
|
||||||
import { readFileSync, writeFileSync, readdirSync, existsSync, mkdirSync } from "node:fs";
|
import { readFileSync, writeFileSync, readdirSync, existsSync, mkdirSync } from "node:fs";
|
||||||
import { join, resolve, dirname } from "node:path";
|
import { join, resolve, dirname } from "node:path";
|
||||||
import { fileURLToPath } from "node:url";
|
import { homedir } from "node:os";
|
||||||
import type { AnalyticsBatch, WeeklyReport, MonthlyReport, PostAnalytics } from "../models/types.js";
|
import type { AnalyticsBatch, WeeklyReport, MonthlyReport, PostAnalytics } from "../models/types.js";
|
||||||
|
|
||||||
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Walk up from `startDir` until a directory containing `.claude-plugin/plugin.json`
|
* 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
|
* is found. Returns that directory, or null if no marker is found before the
|
||||||
* filesystem root.
|
* filesystem root.
|
||||||
*
|
*
|
||||||
* Anchoring on the plugin marker keeps the analytics root correct by
|
* Anchoring on the plugin marker locates the plugin tree correctly by
|
||||||
* construction regardless of whether this module runs from `src/utils/`
|
* construction regardless of whether this module runs from `src/utils/`
|
||||||
* (under tsx) or `build/utils/` (compiled) — and survives a future source
|
* (under tsx) or `build/utils/` (compiled) — and survives a future source
|
||||||
* move that a hardcoded "../../../../" count would silently break.
|
* move that a hardcoded "../../../../" count would silently break. Since M0 it
|
||||||
|
* locates bundled read-only assets only; the per-user data root is external
|
||||||
|
* (getDataRoot), no longer derived from the plugin tree.
|
||||||
*/
|
*/
|
||||||
export function findPluginRoot(startDir: string): string | null {
|
export function findPluginRoot(startDir: string): string | null {
|
||||||
let dir = resolve(startDir);
|
let dir = resolve(startDir);
|
||||||
|
|
@ -33,20 +33,42 @@ export function findPluginRoot(startDir: string): string | null {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the analytics root directory from environment or default location.
|
* Resolve HOME with the codebase idiom, but never fall through to "" — that would
|
||||||
* Default is assets/analytics under the plugin root (the dir holding
|
* turn the absolute data path into a CWD-relative one. os.homedir() is the floor.
|
||||||
* .claude-plugin/plugin.json). The ANALYTICS_ROOT env override is the test seam.
|
*/
|
||||||
|
function resolveHome(): string {
|
||||||
|
return process.env.HOME || process.env.USERPROFILE || homedir();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* CANONICAL — twin of hooks/scripts/data-root.mjs:getDataRoot. MUST STAY IN SYNC:
|
||||||
|
* identical default (~/.claude/linkedin-studio) + identical LINKEDIN_STUDIO_DATA
|
||||||
|
* override semantics. A twin exists (not a shared import) because the runtimes
|
||||||
|
* differ: the hooks are plain .mjs (no tsx) and cannot import this TS module. The
|
||||||
|
* twin-consistency test (hooks/scripts/__tests__/data-root.test.mjs) guards the
|
||||||
|
* contract behaviorally.
|
||||||
|
*
|
||||||
|
* Per-user data root: ~/.claude/linkedin-studio/<subdir>, overridable via
|
||||||
|
* LINKEDIN_STUDIO_DATA (test + power-user seam, mirrors the state-file location).
|
||||||
|
*/
|
||||||
|
export function getDataRoot(subdir: string): string {
|
||||||
|
const dataBase =
|
||||||
|
process.env.LINKEDIN_STUDIO_DATA ||
|
||||||
|
join(resolveHome(), ".claude", "linkedin-studio");
|
||||||
|
return join(dataBase, subdir);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the analytics root directory — since M0 the external per-user dir
|
||||||
|
* (getDataRoot("analytics")). The deprecated ANALYTICS_ROOT env override still
|
||||||
|
* wins for back-compat (D4) so cli.ts and existing prose keep working through the
|
||||||
|
* migration. findPluginRoot is preserved (above) for bundled read-only assets.
|
||||||
*/
|
*/
|
||||||
export function getAnalyticsRoot(): string {
|
export function getAnalyticsRoot(): string {
|
||||||
if (process.env.ANALYTICS_ROOT) {
|
if (process.env.ANALYTICS_ROOT) {
|
||||||
return resolve(process.env.ANALYTICS_ROOT);
|
return resolve(process.env.ANALYTICS_ROOT);
|
||||||
}
|
}
|
||||||
|
return getDataRoot("analytics");
|
||||||
// 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");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
|
|
@ -3,25 +3,34 @@ import assert from "node:assert/strict";
|
||||||
import { mkdtempSync, rmSync, mkdirSync, writeFileSync, existsSync } from "node:fs";
|
import { mkdtempSync, rmSync, mkdirSync, writeFileSync, existsSync } from "node:fs";
|
||||||
import { join, resolve } from "node:path";
|
import { join, resolve } from "node:path";
|
||||||
import { tmpdir } from "node:os";
|
import { tmpdir } from "node:os";
|
||||||
import { getAnalyticsRoot, findPluginRoot } from "../src/utils/storage.js";
|
import { getAnalyticsRoot, getDataRoot, findPluginRoot } from "../src/utils/storage.js";
|
||||||
|
|
||||||
// Regression lock for the fresh-clone / foreign-CWD analytics root resolution.
|
// M0: the data root moved OUT of the plugin tree to the external per-user dir
|
||||||
// The root must anchor on the .claude-plugin/plugin.json marker (correct by
|
// (~/.claude/linkedin-studio), behind getDataRoot(). getAnalyticsRoot() is now a
|
||||||
// construction), NOT on a fragile count of "../" segments that silently breaks
|
// thin alias = getDataRoot("analytics") that still honors the deprecated
|
||||||
// if the source layout is ever moved.
|
// ANALYTICS_ROOT env for back-compat (D4). findPluginRoot stays — it now locates
|
||||||
describe("analytics root resolution", () => {
|
// bundled read-only assets only (commit 798484b, the fresh-clone crash fix).
|
||||||
|
//
|
||||||
|
// The default must be stubbed via HOME + LINKEDIN_STUDIO_DATA save/restore so it
|
||||||
|
// never couples to the dev's real $HOME. Mirrors the .mjs twin test
|
||||||
|
// (hooks/scripts/__tests__/data-root.test.mjs) — keep them in sync.
|
||||||
|
describe("data root resolution", () => {
|
||||||
let tempDir: string | undefined;
|
let tempDir: string | undefined;
|
||||||
const savedEnv = process.env.ANALYTICS_ROOT;
|
const saved = {
|
||||||
|
ANALYTICS_ROOT: process.env.ANALYTICS_ROOT,
|
||||||
|
LINKEDIN_STUDIO_DATA: process.env.LINKEDIN_STUDIO_DATA,
|
||||||
|
HOME: process.env.HOME,
|
||||||
|
USERPROFILE: process.env.USERPROFILE,
|
||||||
|
};
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
if (tempDir && existsSync(tempDir)) {
|
if (tempDir && existsSync(tempDir)) {
|
||||||
rmSync(tempDir, { recursive: true, force: true });
|
rmSync(tempDir, { recursive: true, force: true });
|
||||||
}
|
}
|
||||||
tempDir = undefined;
|
tempDir = undefined;
|
||||||
if (savedEnv === undefined) {
|
for (const [k, v] of Object.entries(saved)) {
|
||||||
delete process.env.ANALYTICS_ROOT;
|
if (v === undefined) delete process.env[k];
|
||||||
} else {
|
else process.env[k] = v;
|
||||||
process.env.ANALYTICS_ROOT = savedEnv;
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -46,32 +55,50 @@ describe("analytics root resolution", () => {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("getDataRoot", () => {
|
||||||
|
test("external default (HOME-stubbed): getDataRoot('analytics') = HOME/.claude/linkedin-studio/analytics", () => {
|
||||||
|
process.env.HOME = "/home/tester";
|
||||||
|
delete process.env.USERPROFILE;
|
||||||
|
delete process.env.LINKEDIN_STUDIO_DATA;
|
||||||
|
|
||||||
|
assert.equal(
|
||||||
|
getDataRoot("analytics"),
|
||||||
|
join("/home/tester", ".claude", "linkedin-studio", "analytics"),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("LINKEDIN_STUDIO_DATA override wins", () => {
|
||||||
|
process.env.LINKEDIN_STUDIO_DATA = "/tmp/lis-data";
|
||||||
|
|
||||||
|
assert.equal(getDataRoot("analytics"), join("/tmp/lis-data", "analytics"));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe("getAnalyticsRoot", () => {
|
describe("getAnalyticsRoot", () => {
|
||||||
test("honors ANALYTICS_ROOT override (resolved env path)", () => {
|
test("default (no env) now resolves to the EXTERNAL data dir, not in-plugin assets/analytics", () => {
|
||||||
|
process.env.HOME = "/home/tester";
|
||||||
|
delete process.env.USERPROFILE;
|
||||||
|
delete process.env.LINKEDIN_STUDIO_DATA;
|
||||||
|
delete process.env.ANALYTICS_ROOT;
|
||||||
|
|
||||||
|
assert.equal(
|
||||||
|
getAnalyticsRoot(),
|
||||||
|
join("/home/tester", ".claude", "linkedin-studio", "analytics"),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("honors deprecated ANALYTICS_ROOT alias (resolved env path) for back-compat", () => {
|
||||||
tempDir = mkdtempSync(join(tmpdir(), "analytics-root-"));
|
tempDir = mkdtempSync(join(tmpdir(), "analytics-root-"));
|
||||||
process.env.ANALYTICS_ROOT = tempDir;
|
process.env.ANALYTICS_ROOT = tempDir;
|
||||||
|
|
||||||
assert.equal(getAnalyticsRoot(), resolve(tempDir));
|
assert.equal(getAnalyticsRoot(), resolve(tempDir));
|
||||||
});
|
});
|
||||||
|
|
||||||
test("default (no env) anchors on the plugin dir, not scripts/analytics/assets", () => {
|
test("LINKEDIN_STUDIO_DATA drives the analytics root when ANALYTICS_ROOT is unset", () => {
|
||||||
delete process.env.ANALYTICS_ROOT;
|
delete process.env.ANALYTICS_ROOT;
|
||||||
|
process.env.LINKEDIN_STUDIO_DATA = "/tmp/lis-data";
|
||||||
|
|
||||||
const root = getAnalyticsRoot();
|
assert.equal(getAnalyticsRoot(), join("/tmp/lis-data", "analytics"));
|
||||||
const suffix = join("assets", "analytics");
|
|
||||||
assert.ok(root.endsWith(suffix), `expected to end with ${suffix}, got ${root}`);
|
|
||||||
|
|
||||||
// The parent of assets/analytics must be the real plugin root (holds the marker),
|
|
||||||
// proving the root is NOT scripts/analytics/assets/analytics.
|
|
||||||
const pluginRoot = root.slice(0, root.length - (suffix.length + 1));
|
|
||||||
assert.ok(
|
|
||||||
existsSync(join(pluginRoot, ".claude-plugin", "plugin.json")),
|
|
||||||
`plugin marker missing under resolved root parent: ${pluginRoot}`,
|
|
||||||
);
|
|
||||||
assert.ok(
|
|
||||||
!pluginRoot.endsWith(join("scripts", "analytics")),
|
|
||||||
`root wrongly anchored under scripts/analytics: ${pluginRoot}`,
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue