mirror of
https://github.com/mderman/context_nine_obsidian_plugin.git
synced 2026-07-22 07:24:23 +00:00
Simplify vault cockpit commands
This commit is contained in:
parent
9c2daa353f
commit
6da8ebf421
7 changed files with 263 additions and 254 deletions
|
|
@ -28,9 +28,9 @@ Context Nine is desktop-only. Vault Command Center and background sync commands
|
|||
|
||||
## Vault Command Center
|
||||
|
||||
Vault Command Center runs the local `vault` command dispatcher without leaving Obsidian. Default buttons include refresh, sync, context, content schedules, attachment dry run/apply, and profile sync.
|
||||
Vault Command Center shows local command output in the right sidebar and keeps a Git Commit action close at hand. Day-to-day vault commands live in the Obsidian command palette and are loaded from the vault command metadata file.
|
||||
|
||||
Normal click runs a command directly. `Cmd+Click` opens an arguments modal and appends parsed arguments to the base command. Output streams live into the panel with stdout, stderr, status, exit code, and timestamps visible after completion.
|
||||
Output streams live into the panel with stdout, stderr, status, exit code, and timestamps visible after completion.
|
||||
|
||||
## Development
|
||||
|
||||
|
|
|
|||
108
src/main.ts
108
src/main.ts
|
|
@ -1,4 +1,4 @@
|
|||
import { Notice, Plugin, PluginSettingTab, Setting, TFile, type WorkspaceLeaf } from "obsidian";
|
||||
import { Modal, Notice, Plugin, PluginSettingTab, Setting, TFile, type App, type WorkspaceLeaf } from "obsidian";
|
||||
import { spawn, type ChildProcessWithoutNullStreams } from "child_process";
|
||||
import { join } from "path";
|
||||
import { AttachmentRouter, noticeRouteResult } from "./attachment-router";
|
||||
|
|
@ -9,6 +9,10 @@ import { VAULT_COCKPIT_VIEW_TYPE, VaultCockpitView } from "./vault-cockpit";
|
|||
import { TaskContextRouterService } from "./task-context-router";
|
||||
import { TaskNotesUxService } from "./tasknotes-ux";
|
||||
import { TaskNotesModalUiService } from "./tasknotes-modal-ui";
|
||||
import {
|
||||
loadVaultCommandMetadata,
|
||||
type VaultCommandDefinition,
|
||||
} from "./vault-command-metadata";
|
||||
|
||||
export default class ContextNinePlugin extends Plugin {
|
||||
settings: MasterPluginSettings;
|
||||
|
|
@ -52,6 +56,8 @@ export default class ContextNinePlugin extends Plugin {
|
|||
},
|
||||
});
|
||||
|
||||
await this.registerVaultPaletteCommands();
|
||||
|
||||
this.addCommand({
|
||||
id: "focus-main-pane-1",
|
||||
name: "Focus first main pane",
|
||||
|
|
@ -204,22 +210,62 @@ export default class ContextNinePlugin extends Plugin {
|
|||
}
|
||||
|
||||
getVaultCommand(command: string, cwd: string): string {
|
||||
return command === "vault" ? join(cwd, "master/system/scripts/vault.py") : command;
|
||||
return command === "vault" ? join(cwd, "_master/system/scripts/vault.py") : command;
|
||||
}
|
||||
|
||||
async openVaultCockpit(): Promise<void> {
|
||||
await this.getVaultCockpitView();
|
||||
}
|
||||
|
||||
async getVaultCockpitView(): Promise<VaultCockpitView | null> {
|
||||
const existing = this.app.workspace.getLeavesOfType(VAULT_COCKPIT_VIEW_TYPE)[0];
|
||||
if (existing) {
|
||||
await this.app.workspace.revealLeaf(existing);
|
||||
return;
|
||||
return existing.view instanceof VaultCockpitView ? existing.view : null;
|
||||
}
|
||||
const leaf = this.app.workspace.getRightLeaf(false);
|
||||
if (!leaf) {
|
||||
new Notice("Could not open the right sidebar.");
|
||||
return;
|
||||
return null;
|
||||
}
|
||||
await leaf.setViewState({ type: VAULT_COCKPIT_VIEW_TYPE, active: true });
|
||||
await this.app.workspace.revealLeaf(leaf);
|
||||
return leaf.view instanceof VaultCockpitView ? leaf.view : null;
|
||||
}
|
||||
|
||||
private async registerVaultPaletteCommands(): Promise<void> {
|
||||
const result = await loadVaultCommandMetadata(this.app);
|
||||
if (result.warning) {
|
||||
console.warn(`[Context Nine] ${result.warning}`);
|
||||
}
|
||||
for (const command of result.commands.filter((item) => item.palette)) {
|
||||
this.addCommand({
|
||||
id: `vault-${command.id}`,
|
||||
name: `Vault ${command.label}`,
|
||||
callback: () => {
|
||||
void this.runPaletteVaultCommand(command);
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private async runPaletteVaultCommand(command: VaultCommandDefinition): Promise<void> {
|
||||
if (command.promptArgs?.length) {
|
||||
new VaultPromptArgsModal(this.app, command, (extraArgs) => {
|
||||
void this.runVaultCommandInCockpit(command, extraArgs);
|
||||
}).open();
|
||||
return;
|
||||
}
|
||||
await this.runVaultCommandInCockpit(command);
|
||||
}
|
||||
|
||||
private async runVaultCommandInCockpit(command: VaultCommandDefinition, extraArgs: string[] = []): Promise<void> {
|
||||
const view = await this.getVaultCockpitView();
|
||||
if (!view) {
|
||||
new Notice("Could not open the vault command center.");
|
||||
return;
|
||||
}
|
||||
view.runCommand(command, extraArgs);
|
||||
}
|
||||
|
||||
private async focusMainPane(index: number, createIfMissing = false): Promise<void> {
|
||||
|
|
@ -369,6 +415,60 @@ export default class ContextNinePlugin extends Plugin {
|
|||
}
|
||||
}
|
||||
|
||||
class VaultPromptArgsModal extends Modal {
|
||||
private values: string[];
|
||||
|
||||
constructor(
|
||||
app: App,
|
||||
private readonly command: VaultCommandDefinition,
|
||||
private readonly onSubmit: (args: string[]) => void
|
||||
) {
|
||||
super(app);
|
||||
this.values = new Array(command.promptArgs?.length ?? 0).fill("");
|
||||
}
|
||||
|
||||
onOpen(): void {
|
||||
this.contentEl.empty();
|
||||
this.containerEl.addClass("omp-modal");
|
||||
this.titleEl.setText(this.command.label);
|
||||
|
||||
const promptArgs = this.command.promptArgs ?? [];
|
||||
promptArgs.forEach((promptArg, index) => {
|
||||
new Setting(this.contentEl).setName(promptArg.label).addText((text) => {
|
||||
text.setPlaceholder(promptArg.placeholder ?? "").onChange((value) => {
|
||||
this.values[index] = value.trim();
|
||||
});
|
||||
text.inputEl.addEventListener("keydown", (event) => {
|
||||
if (event.key === "Enter") {
|
||||
event.preventDefault();
|
||||
this.submit();
|
||||
}
|
||||
});
|
||||
if (index === 0) {
|
||||
window.setTimeout(() => text.inputEl.focus(), 0);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
const buttons = this.contentEl.createDiv({ cls: "omp-button-row" });
|
||||
buttons.createEl("button", { text: "Cancel" }).addEventListener("click", () => this.close());
|
||||
const run = buttons.createEl("button", { text: "Run" });
|
||||
run.addClass("mod-cta");
|
||||
run.addEventListener("click", () => this.submit());
|
||||
}
|
||||
|
||||
private submit(): void {
|
||||
const promptArgs = this.command.promptArgs ?? [];
|
||||
const missing = promptArgs.find((_, index) => !this.values[index]);
|
||||
if (missing) {
|
||||
new Notice(`${missing.label} required.`);
|
||||
return;
|
||||
}
|
||||
this.close();
|
||||
this.onSubmit(this.values);
|
||||
}
|
||||
}
|
||||
|
||||
class ObsidianMasterSettingTab extends PluginSettingTab {
|
||||
constructor(private readonly plugin: ContextNinePlugin) {
|
||||
super(plugin.app, plugin);
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
import { ItemView, Modal, Notice, Setting, type App, type IconName, type WorkspaceLeaf } from "obsidian";
|
||||
import { ItemView, Notice, type IconName, type WorkspaceLeaf } from "obsidian";
|
||||
import type ContextNinePlugin from "./main";
|
||||
import { parseAdditionalArgs } from "./vault-args";
|
||||
import {
|
||||
FALLBACK_VAULT_COMMANDS,
|
||||
loadVaultCommandMetadata,
|
||||
|
|
@ -18,14 +17,12 @@ interface LogEntry {
|
|||
export class VaultCockpitView extends ItemView {
|
||||
private readonly runner = new VaultCommandRunner();
|
||||
private commands: VaultCommandDefinition[] = FALLBACK_VAULT_COMMANDS;
|
||||
private metadataWarning: string | null = null;
|
||||
private status: VaultRunStatus = "idle";
|
||||
private activeCommandId: string | null = null;
|
||||
private actionsExpanded = false;
|
||||
private outputExpanded = false;
|
||||
private logEntries: LogEntry[] = [];
|
||||
private statusEl!: HTMLElement;
|
||||
private warningEl!: HTMLElement;
|
||||
private logContainerEl!: HTMLElement;
|
||||
private logEl: HTMLElement | null = null;
|
||||
private buttons = new Map<string, HTMLButtonElement>();
|
||||
|
|
@ -52,9 +49,8 @@ export class VaultCockpitView extends ItemView {
|
|||
async onOpen(): Promise<void> {
|
||||
const result = await loadVaultCommandMetadata(this.app);
|
||||
this.commands = result.commands;
|
||||
this.metadataWarning = result.warning ?? null;
|
||||
if (this.metadataWarning) {
|
||||
this.logEntries.push({ stream: "system", text: `Warning: ${this.metadataWarning}\n` });
|
||||
if (result.warning) {
|
||||
this.logEntries.push({ stream: "system", text: `Warning: ${result.warning}\n` });
|
||||
}
|
||||
this.render();
|
||||
}
|
||||
|
|
@ -80,28 +76,12 @@ export class VaultCockpitView extends ItemView {
|
|||
this.statusEl = primaryRow.createDiv({ cls: "omp-vault-cockpit-status", text: labelForStatus(this.status) });
|
||||
this.statusEl.dataset.status = this.status;
|
||||
|
||||
const commitButton = primaryRow.createEl("button", {
|
||||
cls: "omp-vault-cockpit-actions-toggle",
|
||||
attr: {
|
||||
"aria-label": "Git commit",
|
||||
title: "Git commit",
|
||||
},
|
||||
});
|
||||
commitButton.createSpan({
|
||||
cls: "omp-vault-cockpit-actions-toggle-icon",
|
||||
text: "✓",
|
||||
});
|
||||
commitButton.addEventListener("click", () => {
|
||||
this.runGitCommit();
|
||||
});
|
||||
this.buttons.set("git-commit", commitButton);
|
||||
|
||||
const actionsToggle = primaryRow.createEl("button", {
|
||||
cls: "omp-vault-cockpit-actions-toggle",
|
||||
attr: {
|
||||
"aria-label": this.actionsExpanded ? "Hide vault actions" : "Show vault actions",
|
||||
"aria-label": this.actionsExpanded ? "Hide console controls" : "Show console controls",
|
||||
"aria-expanded": String(this.actionsExpanded),
|
||||
title: this.actionsExpanded ? "Hide vault actions" : "Show vault actions",
|
||||
title: this.actionsExpanded ? "Hide console controls" : "Show console controls",
|
||||
},
|
||||
});
|
||||
actionsToggle.createSpan({
|
||||
|
|
@ -114,12 +94,12 @@ export class VaultCockpitView extends ItemView {
|
|||
});
|
||||
|
||||
if (this.actionsExpanded) {
|
||||
this.renderCommandSections(containerEl);
|
||||
this.logContainerEl = containerEl.createDiv({ cls: "omp-vault-cockpit-output" });
|
||||
this.renderOutput();
|
||||
} else {
|
||||
this.logEl = null;
|
||||
}
|
||||
|
||||
this.logContainerEl = containerEl.createDiv({ cls: "omp-vault-cockpit-output" });
|
||||
this.renderStatus();
|
||||
this.renderOutput();
|
||||
}
|
||||
|
||||
private createCommandButton(parent: HTMLElement, command: VaultCommandDefinition, extraClass = ""): HTMLButtonElement {
|
||||
|
|
@ -131,18 +111,16 @@ export class VaultCockpitView extends ItemView {
|
|||
"aria-label": command.description,
|
||||
},
|
||||
});
|
||||
button.addEventListener("click", (event) => {
|
||||
if (event.metaKey) {
|
||||
new VaultArgsModal(this.app, command, (extraArgs) => {
|
||||
this.runCommand(command, extraArgs);
|
||||
}).open();
|
||||
return;
|
||||
}
|
||||
button.addEventListener("click", () => {
|
||||
this.runCommand(command);
|
||||
});
|
||||
return button;
|
||||
}
|
||||
|
||||
private findCommand(id: string): VaultCommandDefinition | undefined {
|
||||
return this.commands.find((command) => command.id === id);
|
||||
}
|
||||
|
||||
private runGitCommit(): void {
|
||||
if (this.runner.running) {
|
||||
new Notice("A vault command is already running.");
|
||||
|
|
@ -189,61 +167,7 @@ export class VaultCockpitView extends ItemView {
|
|||
}
|
||||
}
|
||||
|
||||
private renderCommandSections(containerEl: HTMLElement): void {
|
||||
const sections = [
|
||||
{
|
||||
title: "Maintenance",
|
||||
commands: ["sync", "context", "content"],
|
||||
},
|
||||
{
|
||||
title: "Attachments",
|
||||
commands: ["attachments-dry-run", "attachments-apply"],
|
||||
},
|
||||
{
|
||||
title: "System",
|
||||
commands: ["profile"],
|
||||
},
|
||||
];
|
||||
|
||||
const commandList = containerEl.createDiv({ cls: "omp-vault-cockpit-actions" });
|
||||
const primaryCommand = this.findCommand("refresh") ?? this.commands[0];
|
||||
const renderedIds = new Set(primaryCommand ? [primaryCommand.id] : []);
|
||||
|
||||
for (const section of sections) {
|
||||
const commands = section.commands.map((id) => this.findCommand(id)).filter((command): command is VaultCommandDefinition => Boolean(command));
|
||||
if (commands.length === 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const sectionEl = commandList.createDiv({ cls: "omp-vault-cockpit-section" });
|
||||
sectionEl.createDiv({ cls: "omp-vault-cockpit-section-title", text: section.title });
|
||||
const grid = sectionEl.createDiv({ cls: "omp-vault-cockpit-grid" });
|
||||
for (const command of commands) {
|
||||
const button = this.createCommandButton(grid, command);
|
||||
this.buttons.set(command.id, button);
|
||||
renderedIds.add(command.id);
|
||||
}
|
||||
}
|
||||
|
||||
const uncategorized = this.commands.filter((command) => !renderedIds.has(command.id));
|
||||
if (uncategorized.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const sectionEl = commandList.createDiv({ cls: "omp-vault-cockpit-section" });
|
||||
sectionEl.createDiv({ cls: "omp-vault-cockpit-section-title", text: "Other" });
|
||||
const grid = sectionEl.createDiv({ cls: "omp-vault-cockpit-grid" });
|
||||
for (const command of uncategorized) {
|
||||
const button = this.createCommandButton(grid, command);
|
||||
this.buttons.set(command.id, button);
|
||||
}
|
||||
}
|
||||
|
||||
private findCommand(id: string): VaultCommandDefinition | undefined {
|
||||
return this.commands.find((command) => command.id === id);
|
||||
}
|
||||
|
||||
private runCommand(command: VaultCommandDefinition, extraArgs: string[] = []): void {
|
||||
runCommand(command: VaultCommandDefinition, extraArgs: string[] = []): void {
|
||||
if (this.runner.running) {
|
||||
new Notice("A vault command is already running.");
|
||||
return;
|
||||
|
|
@ -315,7 +239,6 @@ export class VaultCockpitView extends ItemView {
|
|||
for (const [id, button] of this.buttons) {
|
||||
button.disabled = this.status === "running" && id === this.activeCommandId;
|
||||
}
|
||||
this.renderOutput();
|
||||
}
|
||||
|
||||
private renderOutput(): void {
|
||||
|
|
@ -323,15 +246,10 @@ export class VaultCockpitView extends ItemView {
|
|||
return;
|
||||
}
|
||||
this.logContainerEl.empty();
|
||||
if (!this.actionsExpanded) {
|
||||
this.logEl = null;
|
||||
return;
|
||||
}
|
||||
|
||||
const row = this.logContainerEl.createDiv({ cls: "omp-vault-cockpit-output-row" });
|
||||
const toggle = row.createEl("button", {
|
||||
cls: "omp-vault-cockpit-output-toggle",
|
||||
text: `${this.outputExpanded ? "Hide" : "Show"} Output`,
|
||||
text: `${this.outputExpanded ? "Hide" : "Show"} Console Output`,
|
||||
attr: {
|
||||
"aria-expanded": String(this.outputExpanded),
|
||||
},
|
||||
|
|
@ -341,12 +259,13 @@ export class VaultCockpitView extends ItemView {
|
|||
this.renderOutput();
|
||||
});
|
||||
|
||||
if (this.metadataWarning) {
|
||||
this.warningEl = this.logContainerEl.createDiv({
|
||||
cls: "omp-vault-cockpit-warning",
|
||||
text: this.metadataWarning,
|
||||
});
|
||||
}
|
||||
const commitButton = row.createEl("button", { cls: "omp-vault-cockpit-git-commit", text: "Git Commit" });
|
||||
commitButton.disabled = this.status === "running";
|
||||
commitButton.addEventListener("click", () => this.runGitCommit());
|
||||
this.buttons.set("git-commit", commitButton);
|
||||
|
||||
this.statusEl = row.createDiv({ cls: "omp-vault-cockpit-status", text: labelForStatus(this.status) });
|
||||
this.statusEl.dataset.status = this.status;
|
||||
|
||||
if (!this.outputExpanded) {
|
||||
this.logEl = null;
|
||||
|
|
@ -373,55 +292,6 @@ export class VaultCockpitView extends ItemView {
|
|||
}
|
||||
}
|
||||
|
||||
class VaultArgsModal extends Modal {
|
||||
private value = "";
|
||||
|
||||
constructor(
|
||||
app: App,
|
||||
private readonly command: VaultCommandDefinition,
|
||||
private readonly onSubmit: (extraArgs: string[]) => void
|
||||
) {
|
||||
super(app);
|
||||
}
|
||||
|
||||
onOpen(): void {
|
||||
this.contentEl.empty();
|
||||
this.containerEl.addClass("omp-modal");
|
||||
this.titleEl.setText(`${this.command.label} arguments`);
|
||||
this.contentEl.createEl("p", {
|
||||
text: `Base command: vault ${this.command.args.join(" ")}`,
|
||||
});
|
||||
|
||||
new Setting(this.contentEl).setName("Additional arguments").addText((text) => {
|
||||
text.setPlaceholder("--flag value").onChange((value) => {
|
||||
this.value = value;
|
||||
});
|
||||
text.inputEl.addEventListener("keydown", (event) => {
|
||||
if (event.key === "Enter") {
|
||||
event.preventDefault();
|
||||
this.submit();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
const buttons = this.contentEl.createDiv({ cls: "omp-button-row" });
|
||||
buttons.createEl("button", { text: "Cancel" }).addEventListener("click", () => this.close());
|
||||
const run = buttons.createEl("button", { text: "Run" });
|
||||
run.addClass("mod-cta");
|
||||
run.addEventListener("click", () => this.submit());
|
||||
}
|
||||
|
||||
private submit(): void {
|
||||
const parsed = parseAdditionalArgs(this.value);
|
||||
if (parsed.error) {
|
||||
new Notice(parsed.error);
|
||||
return;
|
||||
}
|
||||
this.close();
|
||||
this.onSubmit(parsed.args);
|
||||
}
|
||||
}
|
||||
|
||||
function labelForStatus(status: VaultRunStatus): string {
|
||||
if (status === "idle") return "Idle";
|
||||
if (status === "running") return "Running";
|
||||
|
|
|
|||
|
|
@ -6,6 +6,15 @@ export interface VaultCommandDefinition {
|
|||
description: string;
|
||||
args: string[];
|
||||
aliases?: string[];
|
||||
cockpit?: boolean;
|
||||
palette?: boolean;
|
||||
promptArgs?: VaultCommandPromptArg[];
|
||||
}
|
||||
|
||||
export interface VaultCommandPromptArg {
|
||||
label: string;
|
||||
placeholder?: string;
|
||||
argName?: string;
|
||||
}
|
||||
|
||||
export interface VaultCommandLoadResult {
|
||||
|
|
@ -13,7 +22,8 @@ export interface VaultCommandLoadResult {
|
|||
warning?: string;
|
||||
}
|
||||
|
||||
export const VAULT_COMMAND_METADATA_PATH = "master/system/scripts/vault-commands.json";
|
||||
export const VAULT_COMMAND_METADATA_PATH = "_master/system/scripts/vault-commands.json";
|
||||
const LEGACY_VAULT_COMMAND_METADATA_PATH = "master/system/scripts/vault-commands.json";
|
||||
|
||||
export const FALLBACK_VAULT_COMMANDS: VaultCommandDefinition[] = [
|
||||
{
|
||||
|
|
@ -21,6 +31,22 @@ export const FALLBACK_VAULT_COMMANDS: VaultCommandDefinition[] = [
|
|||
label: "Refresh",
|
||||
description: "Ingest configured Apple Notes, then regenerate agent context.",
|
||||
args: ["refresh"],
|
||||
palette: true,
|
||||
},
|
||||
{
|
||||
id: "folder-register",
|
||||
label: "Folder Register",
|
||||
description: "Register an existing context folder and regenerate vault wiring.",
|
||||
args: ["folder", "register"],
|
||||
palette: true,
|
||||
promptArgs: [{ label: "Context folder", placeholder: "impression", argName: "name" }],
|
||||
},
|
||||
{
|
||||
id: "upgrade",
|
||||
label: "Upgrade",
|
||||
description: "Apply public bootstrap vault updates.",
|
||||
args: ["upgrade", "--apply"],
|
||||
palette: true,
|
||||
},
|
||||
{
|
||||
id: "sync",
|
||||
|
|
@ -106,10 +132,15 @@ export async function loadVaultCommandMetadata(app: App): Promise<VaultCommandLo
|
|||
const json = await app.vault.adapter.read(VAULT_COMMAND_METADATA_PATH);
|
||||
return parseVaultCommandMetadata(json);
|
||||
} catch (error) {
|
||||
return {
|
||||
commands: FALLBACK_VAULT_COMMANDS,
|
||||
warning: `Could not read ${VAULT_COMMAND_METADATA_PATH}: ${messageForError(error)}`,
|
||||
};
|
||||
try {
|
||||
const json = await app.vault.adapter.read(LEGACY_VAULT_COMMAND_METADATA_PATH);
|
||||
return parseVaultCommandMetadata(json);
|
||||
} catch {
|
||||
return {
|
||||
commands: FALLBACK_VAULT_COMMANDS,
|
||||
warning: `Could not read ${VAULT_COMMAND_METADATA_PATH}: ${messageForError(error)}`,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -132,6 +163,18 @@ function normalizeCommand(value: unknown): VaultCommandDefinition | null {
|
|||
) {
|
||||
return null;
|
||||
}
|
||||
if (value.cockpit !== undefined && typeof value.cockpit !== "boolean") {
|
||||
return null;
|
||||
}
|
||||
if (value.palette !== undefined && typeof value.palette !== "boolean") {
|
||||
return null;
|
||||
}
|
||||
if (
|
||||
value.promptArgs !== undefined &&
|
||||
(!Array.isArray(value.promptArgs) || !value.promptArgs.every(isPromptArg))
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
id: value.id,
|
||||
|
|
@ -139,6 +182,9 @@ function normalizeCommand(value: unknown): VaultCommandDefinition | null {
|
|||
description: value.description,
|
||||
args: value.args,
|
||||
aliases: value.aliases,
|
||||
cockpit: value.cockpit,
|
||||
palette: value.palette,
|
||||
promptArgs: value.promptArgs,
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -146,6 +192,19 @@ function isRecord(value: unknown): value is Record<string, unknown> {
|
|||
return typeof value === "object" && value !== null;
|
||||
}
|
||||
|
||||
function isPromptArg(value: unknown): value is VaultCommandPromptArg {
|
||||
if (!isRecord(value) || typeof value.label !== "string") {
|
||||
return false;
|
||||
}
|
||||
if (value.placeholder !== undefined && typeof value.placeholder !== "string") {
|
||||
return false;
|
||||
}
|
||||
if (value.argName !== undefined && typeof value.argName !== "string") {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
function messageForError(error: unknown): string {
|
||||
return error instanceof Error ? error.message : String(error);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -44,7 +44,7 @@ export class VaultCommandRunner {
|
|||
}
|
||||
|
||||
const startedAt = new Date();
|
||||
const resolvedCommand = command === "vault" ? join(cwd, "master/system/scripts/vault.py") : command;
|
||||
const resolvedCommand = command === "vault" ? join(cwd, "_master/system/scripts/vault.py") : command;
|
||||
events.onStart({ spec, command: resolvedCommand, cwd, startedAt });
|
||||
|
||||
const child = spawn(resolvedCommand, spec.args, {
|
||||
|
|
|
|||
88
styles.css
88
styles.css
|
|
@ -37,25 +37,6 @@
|
|||
padding: 8px;
|
||||
}
|
||||
|
||||
.omp-vault-cockpit-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.omp-vault-cockpit-header h3 {
|
||||
margin: 0;
|
||||
font-size: var(--font-ui-medium);
|
||||
}
|
||||
|
||||
.omp-vault-cockpit-primary-row {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr) auto 28px 28px;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.omp-vault-cockpit-status {
|
||||
border: 1px solid var(--background-modifier-border);
|
||||
border-radius: 999px;
|
||||
|
|
@ -76,6 +57,13 @@
|
|||
color: var(--text-error);
|
||||
}
|
||||
|
||||
.omp-vault-cockpit-primary-row {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr) auto 28px;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.omp-vault-cockpit-actions-toggle {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
|
|
@ -94,70 +82,32 @@
|
|||
line-height: 1;
|
||||
}
|
||||
|
||||
.omp-vault-cockpit-actions {
|
||||
display: grid;
|
||||
flex: 0 1 auto;
|
||||
gap: 10px;
|
||||
min-height: 0;
|
||||
max-height: 40%;
|
||||
overflow-y: auto;
|
||||
padding-top: 2px;
|
||||
}
|
||||
|
||||
.omp-vault-cockpit-section {
|
||||
display: grid;
|
||||
gap: 5px;
|
||||
}
|
||||
|
||||
.omp-vault-cockpit-section-title {
|
||||
color: var(--text-muted);
|
||||
font-size: var(--font-ui-smaller);
|
||||
font-weight: var(--font-semibold);
|
||||
}
|
||||
|
||||
.omp-vault-cockpit-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(92px, 1fr));
|
||||
gap: 5px;
|
||||
}
|
||||
|
||||
.omp-vault-cockpit-command {
|
||||
min-height: 26px;
|
||||
min-height: 30px;
|
||||
padding: 3px 7px;
|
||||
border-color: var(--background-modifier-border);
|
||||
background-color: var(--background-modifier-border);
|
||||
border-color: color-mix(in srgb, var(--background-primary) 82%, black);
|
||||
background-color: color-mix(in srgb, var(--background-primary) 88%, black);
|
||||
color: var(--text-normal);
|
||||
font-size: var(--font-ui-smaller);
|
||||
font-weight: var(--font-semibold);
|
||||
line-height: 1.2;
|
||||
white-space: normal;
|
||||
}
|
||||
|
||||
.omp-vault-cockpit-command.omp-vault-cockpit-refresh {
|
||||
min-height: 30px;
|
||||
border-color: color-mix(in srgb, var(--background-primary) 82%, black);
|
||||
background-color: color-mix(in srgb, var(--background-primary) 88%, black);
|
||||
color: var(--text-normal);
|
||||
font-weight: var(--font-semibold);
|
||||
}
|
||||
|
||||
.omp-vault-cockpit-command.omp-vault-cockpit-refresh:hover {
|
||||
.omp-vault-cockpit-command:hover {
|
||||
border-color: color-mix(in srgb, var(--background-primary) 72%, black);
|
||||
background-color: color-mix(in srgb, var(--background-primary) 80%, black);
|
||||
color: var(--text-normal);
|
||||
}
|
||||
|
||||
.omp-vault-cockpit-command:hover {
|
||||
background-color: var(--background-modifier-border-hover);
|
||||
}
|
||||
|
||||
.omp-vault-cockpit-output {
|
||||
display: flex;
|
||||
flex: 1 1 auto;
|
||||
flex: 1 1 min-content;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
min-height: 0;
|
||||
margin-top: 2px;
|
||||
overflow: hidden;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.omp-vault-cockpit-output-row {
|
||||
|
|
@ -173,6 +123,12 @@
|
|||
font-size: var(--font-ui-smaller);
|
||||
}
|
||||
|
||||
.omp-vault-cockpit-git-commit {
|
||||
min-height: 24px;
|
||||
padding: 2px 7px;
|
||||
font-size: var(--font-ui-smaller);
|
||||
}
|
||||
|
||||
.omp-vault-cockpit-log-actions {
|
||||
display: flex;
|
||||
gap: 6px;
|
||||
|
|
@ -192,8 +148,8 @@
|
|||
}
|
||||
|
||||
.omp-vault-cockpit-log {
|
||||
flex: 1 1 auto;
|
||||
min-height: 0;
|
||||
flex: 1 1 0;
|
||||
min-height: 120px;
|
||||
overflow: auto;
|
||||
border: 1px solid var(--background-modifier-border);
|
||||
border-radius: 6px;
|
||||
|
|
|
|||
|
|
@ -1,45 +1,69 @@
|
|||
import { describe, expect, it } from "vitest";
|
||||
import { FALLBACK_VAULT_COMMANDS, parseVaultCommandMetadata } from "../src/vault-command-metadata";
|
||||
import { parseVaultCommandMetadata } from "../src/vault-command-metadata";
|
||||
|
||||
describe("vault command metadata", () => {
|
||||
it("loads valid command metadata", () => {
|
||||
it("keeps legacy command metadata compatible", () => {
|
||||
const result = parseVaultCommandMetadata(
|
||||
JSON.stringify([
|
||||
{
|
||||
id: "refresh",
|
||||
label: "Refresh",
|
||||
description: "Refresh everything.",
|
||||
description: "Run refresh",
|
||||
args: ["refresh"],
|
||||
aliases: ["r"],
|
||||
},
|
||||
])
|
||||
);
|
||||
|
||||
expect(result.warning).toBeUndefined();
|
||||
expect(result.commands).toEqual([
|
||||
{
|
||||
id: "refresh",
|
||||
label: "Refresh",
|
||||
description: "Refresh everything.",
|
||||
args: ["refresh"],
|
||||
aliases: ["r"],
|
||||
},
|
||||
]);
|
||||
expect(result.commands[0]).toMatchObject({
|
||||
id: "refresh",
|
||||
palette: undefined,
|
||||
promptArgs: undefined,
|
||||
});
|
||||
});
|
||||
|
||||
it("falls back when JSON cannot be parsed", () => {
|
||||
const result = parseVaultCommandMetadata("{nope");
|
||||
|
||||
expect(result.commands).toBe(FALLBACK_VAULT_COMMANDS);
|
||||
expect(result.warning).toContain("Could not parse");
|
||||
});
|
||||
|
||||
it("falls back when command args are not an array of strings", () => {
|
||||
it("parses palette flags and prompt args", () => {
|
||||
const result = parseVaultCommandMetadata(
|
||||
JSON.stringify([{ id: "refresh", label: "Refresh", description: "Refresh", args: "refresh" }])
|
||||
JSON.stringify([
|
||||
{
|
||||
id: "folder-register",
|
||||
label: "Folder Register",
|
||||
description: "Register a folder",
|
||||
args: ["folder", "register"],
|
||||
cockpit: false,
|
||||
palette: true,
|
||||
promptArgs: [
|
||||
{
|
||||
label: "Context folder",
|
||||
placeholder: "impression",
|
||||
argName: "name",
|
||||
},
|
||||
],
|
||||
},
|
||||
])
|
||||
);
|
||||
|
||||
expect(result.warning).toBeUndefined();
|
||||
expect(result.commands[0]).toMatchObject({
|
||||
cockpit: false,
|
||||
palette: true,
|
||||
promptArgs: [{ label: "Context folder", placeholder: "impression", argName: "name" }],
|
||||
});
|
||||
});
|
||||
|
||||
it("rejects malformed prompt args", () => {
|
||||
const result = parseVaultCommandMetadata(
|
||||
JSON.stringify([
|
||||
{
|
||||
id: "folder-register",
|
||||
label: "Folder Register",
|
||||
description: "Register a folder",
|
||||
args: ["folder", "register"],
|
||||
promptArgs: [{ placeholder: "impression" }],
|
||||
},
|
||||
])
|
||||
);
|
||||
|
||||
expect(result.commands).toBe(FALLBACK_VAULT_COMMANDS);
|
||||
expect(result.warning).toContain("invalid command");
|
||||
});
|
||||
});
|
||||
|
|
|
|||
Loading…
Reference in a new issue