mirror of
https://github.com/blamouche/obsidian-any-ai-code.git
synced 2026-07-22 17:20:23 +00:00
Install the official Obsidian ESLint plugin (with eslint v9 and
@typescript-eslint/parser) and add an eslint.config.mjs flat config
that wires the recommended ruleset to TS files only, with a small
override turning off hardcoded-config-path for the tests directory
(those literal `.obsidian` strings are unit-test fixtures, not real
Obsidian configuration usage). Add an `npm run lint` script and call
it from the CI workflow before tests so guideline violations surface
on every push and PR.
Resolved every reported violation:
- UI text sentence case: button labels @Active file/@Active folder ->
@active file/@active folder; "(no runtime configured)" placeholder
capitalised; ribbon tooltip "Open Any AI CLI" -> "Open AI CLI panel";
command-list placeholders, descriptions, and the runtimes-section
empty state reworded to drop ambiguous mid-sentence acronyms (PTY,
CLIs, Node) and quoted button names.
- commands/no-plugin-name-in-command-name: command palette name
"Open Any AI CLI" -> "Open panel" (Obsidian shows the plugin name
next to the command, repeating it is redundant).
- prefer-active-doc on globalThis: replace the inline crypto fallback
in runtime-utils.ts with `import { randomUUID } from "node:crypto"`.
- no-unsupported-api on Workspace.revealLeaf: bump manifest.json
minAppVersion to 1.7.2 (the version that introduced revealLeaf) and
await the call. versions.json maps 0.1.38 -> 1.7.2 while older
entries stay at 1.5.0 so existing downloads keep resolving.
Lint, build, tsc and the 27 vitest cases are all clean.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
259 lines
6.9 KiB
TypeScript
259 lines
6.9 KiB
TypeScript
import { randomUUID } from "node:crypto";
|
|
|
|
export interface LaunchSpec {
|
|
file: string;
|
|
args: string[];
|
|
}
|
|
|
|
export interface PathApi {
|
|
join(...parts: string[]): string;
|
|
}
|
|
|
|
export interface CliRuntimeConfig {
|
|
id: string;
|
|
name: string;
|
|
command: string;
|
|
}
|
|
|
|
export interface LegacyRuntimeSettings {
|
|
command?: unknown;
|
|
codexCommand?: unknown;
|
|
runtime?: unknown;
|
|
runtimes?: unknown;
|
|
selectedRuntimeId?: unknown;
|
|
}
|
|
|
|
export function formatActiveFileMention(fileName: string): string {
|
|
return `@${fileName.trim()} `;
|
|
}
|
|
|
|
export function formatActiveFolderMention(filePath: string): string {
|
|
const trimmed = filePath.trim();
|
|
if (!trimmed) {
|
|
return "@./ ";
|
|
}
|
|
const lastSlash = trimmed.lastIndexOf("/");
|
|
if (lastSlash <= 0) {
|
|
return "@./ ";
|
|
}
|
|
return `@${trimmed.slice(0, lastSlash)}/ `;
|
|
}
|
|
|
|
export function isCodexLikeCommand(command: string | undefined | null): boolean {
|
|
if (typeof command !== "string") {
|
|
return false;
|
|
}
|
|
const trimmed = command.trim();
|
|
if (!trimmed) {
|
|
return false;
|
|
}
|
|
return trimmed === "codex" || /^codex(\s|$)/.test(trimmed);
|
|
}
|
|
|
|
export function migrateRuntimeSettings(
|
|
raw: LegacyRuntimeSettings | null | undefined,
|
|
defaults: CliRuntimeConfig[],
|
|
generateId: () => string = defaultGenerateRuntimeId
|
|
): { runtimes: CliRuntimeConfig[]; selectedRuntimeId: string } {
|
|
const fallbackDefaults = defaults.length > 0
|
|
? defaults.map((d) => ({ ...d }))
|
|
: [{ id: generateId(), name: "Default", command: "" }];
|
|
|
|
if (raw && Array.isArray(raw.runtimes)) {
|
|
const sanitized = sanitizeRuntimes(raw.runtimes, generateId);
|
|
if (sanitized.length > 0) {
|
|
const selected =
|
|
typeof raw.selectedRuntimeId === "string" &&
|
|
sanitized.some((r) => r.id === raw.selectedRuntimeId)
|
|
? raw.selectedRuntimeId
|
|
: sanitized[0].id;
|
|
return { runtimes: sanitized, selectedRuntimeId: selected };
|
|
}
|
|
}
|
|
|
|
const runtimes = fallbackDefaults;
|
|
if (raw && typeof raw.command === "string" && raw.command.trim()) {
|
|
const claude = runtimes.find((r) => r.id === "claude");
|
|
if (claude) {
|
|
claude.command = raw.command.trim();
|
|
}
|
|
}
|
|
if (raw && typeof raw.codexCommand === "string" && raw.codexCommand.trim()) {
|
|
const codex = runtimes.find((r) => r.id === "codex");
|
|
if (codex) {
|
|
codex.command = raw.codexCommand.trim();
|
|
}
|
|
}
|
|
|
|
const legacyRuntime =
|
|
typeof raw?.runtime === "string" && (raw.runtime === "claude" || raw.runtime === "codex")
|
|
? raw.runtime
|
|
: undefined;
|
|
const selectedRuntimeId =
|
|
legacyRuntime && runtimes.some((r) => r.id === legacyRuntime)
|
|
? legacyRuntime
|
|
: runtimes[0].id;
|
|
|
|
return { runtimes, selectedRuntimeId };
|
|
}
|
|
|
|
function sanitizeRuntimes(
|
|
raw: unknown[],
|
|
generateId: () => string
|
|
): CliRuntimeConfig[] {
|
|
const result: CliRuntimeConfig[] = [];
|
|
const seenIds = new Set<string>();
|
|
for (const entry of raw) {
|
|
if (!entry || typeof entry !== "object") continue;
|
|
const candidate = entry as Partial<CliRuntimeConfig>;
|
|
const name = typeof candidate.name === "string" ? candidate.name : "";
|
|
const command = typeof candidate.command === "string" ? candidate.command : "";
|
|
let id = typeof candidate.id === "string" && candidate.id.trim() ? candidate.id : generateId();
|
|
while (seenIds.has(id)) {
|
|
id = generateId();
|
|
}
|
|
seenIds.add(id);
|
|
result.push({ id, name, command });
|
|
}
|
|
return result;
|
|
}
|
|
|
|
export function defaultGenerateRuntimeId(): string {
|
|
return randomUUID();
|
|
}
|
|
|
|
export function resolvePluginDir(
|
|
pluginDir: string | undefined,
|
|
vaultBasePath: string | undefined,
|
|
pathApi: { isAbsolute(p: string): boolean; resolve(...parts: string[]): string }
|
|
): string | undefined {
|
|
if (!pluginDir) {
|
|
return undefined;
|
|
}
|
|
if (pathApi.isAbsolute(pluginDir)) {
|
|
return pluginDir;
|
|
}
|
|
if (!vaultBasePath) {
|
|
return undefined;
|
|
}
|
|
return pathApi.resolve(vaultBasePath, pluginDir);
|
|
}
|
|
|
|
export function mergePathEntries(
|
|
currentPath: string | undefined,
|
|
extras: string[],
|
|
platform: NodeJS.Platform
|
|
): string {
|
|
const delimiter = platform === "win32" ? ";" : ":";
|
|
const existing = (currentPath || "").split(delimiter).filter(Boolean);
|
|
const set = new Set(existing);
|
|
|
|
for (const entry of extras) {
|
|
if (!entry || set.has(entry)) {
|
|
continue;
|
|
}
|
|
set.add(entry);
|
|
existing.push(entry);
|
|
}
|
|
|
|
return existing.join(delimiter);
|
|
}
|
|
|
|
export function getLaunchSpecs(
|
|
command: string,
|
|
platform: NodeJS.Platform,
|
|
env: NodeJS.ProcessEnv,
|
|
existsSync: (path: string) => boolean
|
|
): LaunchSpec[] {
|
|
if (platform === "win32") {
|
|
const comspec = env.ComSpec || env.COMSPEC || "C:\\Windows\\System32\\cmd.exe";
|
|
return [{ file: comspec, args: ["/d", "/s", "/c", command] }];
|
|
}
|
|
|
|
const candidates = [env.SHELL, "/bin/zsh", "/bin/bash", "/bin/sh"].filter(
|
|
(value): value is string => Boolean(value)
|
|
);
|
|
|
|
const launches: LaunchSpec[] = [];
|
|
for (const shell of Array.from(new Set(candidates))) {
|
|
if (!existsSync(shell)) {
|
|
continue;
|
|
}
|
|
|
|
if (shell.endsWith("/sh")) {
|
|
launches.push({ file: shell, args: ["-c", command] });
|
|
} else {
|
|
launches.push({ file: shell, args: ["-lc", command] });
|
|
}
|
|
}
|
|
|
|
if (launches.length === 0) {
|
|
launches.push({ file: "/bin/sh", args: ["-c", command] });
|
|
}
|
|
|
|
return launches;
|
|
}
|
|
|
|
export function resolveExecutableInPath(
|
|
executable: string,
|
|
pathValue: string | undefined,
|
|
platform: NodeJS.Platform,
|
|
existsSync: (path: string) => boolean,
|
|
pathApi: PathApi
|
|
): string | undefined {
|
|
if (!pathValue) {
|
|
return undefined;
|
|
}
|
|
const delimiter = platform === "win32" ? ";" : ":";
|
|
const candidates = pathValue.split(delimiter).filter(Boolean);
|
|
const fileName = platform === "win32" ? `${executable}.exe` : executable;
|
|
|
|
for (const dir of candidates) {
|
|
const fullPath = pathApi.join(dir, fileName);
|
|
if (existsSync(fullPath)) {
|
|
return fullPath;
|
|
}
|
|
}
|
|
return undefined;
|
|
}
|
|
|
|
export function detectNodeExecutable(
|
|
configuredValue: string | undefined,
|
|
platform: NodeJS.Platform,
|
|
env: NodeJS.ProcessEnv,
|
|
existsSync: (path: string) => boolean,
|
|
pathApi: PathApi
|
|
): string {
|
|
const configured = configuredValue?.trim();
|
|
if (configured && configured.toLowerCase() !== "auto") {
|
|
return configured;
|
|
}
|
|
|
|
const fromPath = resolveExecutableInPath("node", env.PATH, platform, existsSync, pathApi);
|
|
if (fromPath) {
|
|
return fromPath;
|
|
}
|
|
|
|
const home = env.HOME || env.USERPROFILE || "";
|
|
const defaults =
|
|
platform === "win32"
|
|
? [
|
|
"C:\\Program Files\\nodejs\\node.exe",
|
|
"C:\\Program Files (x86)\\nodejs\\node.exe"
|
|
]
|
|
: [
|
|
"/opt/homebrew/bin/node",
|
|
"/usr/local/bin/node",
|
|
"/usr/bin/node",
|
|
home ? pathApi.join(home, ".volta", "bin", "node") : "",
|
|
home ? pathApi.join(home, ".nvm", "current", "bin", "node") : ""
|
|
];
|
|
|
|
for (const candidate of defaults) {
|
|
if (candidate && existsSync(candidate)) {
|
|
return candidate;
|
|
}
|
|
}
|
|
|
|
return "node";
|
|
}
|