mirror of
https://github.com/sotashimozono/obsidian-remote-ssh.git
synced 2026-07-22 06:52:07 +00:00
fix(plugin): guard shadow bootstrap against self-install when Connect is clicked inside the shadow window (1.1.7-beta.0)
This commit is contained in:
parent
f47bf18c77
commit
550740dfd7
7 changed files with 63 additions and 8 deletions
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"id": "remote-ssh",
|
||||
"name": "Remote SSH",
|
||||
"version": "1.1.6",
|
||||
"version": "1.1.7-beta.0",
|
||||
"minAppVersion": "1.5.0",
|
||||
"description": "Edit remote vaults over SSH/SFTP — VS Code Remote-SSH style.",
|
||||
"author": "souta shimozono",
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"id": "remote-ssh",
|
||||
"name": "Remote SSH",
|
||||
"version": "1.1.6",
|
||||
"version": "1.1.7-beta.0",
|
||||
"minAppVersion": "1.5.0",
|
||||
"description": "Edit remote vaults over SSH/SFTP — VS Code Remote-SSH style.",
|
||||
"author": "souta shimozono",
|
||||
|
|
|
|||
4
plugin/package-lock.json
generated
4
plugin/package-lock.json
generated
|
|
@ -1,12 +1,12 @@
|
|||
{
|
||||
"name": "obsidian-remote-ssh",
|
||||
"version": "1.1.6",
|
||||
"version": "1.1.7-beta.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "obsidian-remote-ssh",
|
||||
"version": "1.1.6",
|
||||
"version": "1.1.7-beta.0",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@xterm/addon-fit": "^0.11.0",
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "obsidian-remote-ssh",
|
||||
"version": "1.1.6",
|
||||
"version": "1.1.7-beta.0",
|
||||
"description": "VS Code Remote SSH-like experience for Obsidian",
|
||||
"main": "main.js",
|
||||
"scripts": {
|
||||
|
|
|
|||
|
|
@ -1043,6 +1043,18 @@ export default class RemoteSshPlugin extends Plugin {
|
|||
* shadow window (Phase 4).
|
||||
*/
|
||||
async openShadowVaultFor(profile: SshProfile): Promise<void> {
|
||||
// Connect clicked from INSIDE the shadow window for this same
|
||||
// profile (Settings row button / connect modal). This vault IS the
|
||||
// shadow — there is no second window to spawn, and re-running the
|
||||
// bootstrap from here would make installPlugin's rm+symlink cycle
|
||||
// target its own files (src == dst), turning main.js/manifest.json
|
||||
// into self-referential symlinks that brick the install on the
|
||||
// next start. Reconnect in place instead.
|
||||
if (this.settings.autoConnectProfileId === profile.id) {
|
||||
await this.runAutoConnect('reconnect');
|
||||
return;
|
||||
}
|
||||
|
||||
// Source dir: where this running plugin lives, so the shadow
|
||||
// vault's plugin install symlinks the same bundle.
|
||||
const sourcePluginDir = this.pluginDir();
|
||||
|
|
|
|||
|
|
@ -60,8 +60,12 @@ export interface BootstrapResult {
|
|||
* surfaces the one-time "restart Obsidian" notice.
|
||||
*/
|
||||
migrated: boolean;
|
||||
/** How the plugin source landed in the shadow vault. */
|
||||
pluginInstallMethod: 'symlink' | 'copy';
|
||||
/**
|
||||
* How the plugin source landed in the shadow vault. `in-place` means
|
||||
* source and target were the same directory (bootstrap re-run from
|
||||
* inside the shadow window) so nothing needed installing.
|
||||
*/
|
||||
pluginInstallMethod: 'symlink' | 'copy' | 'in-place';
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -1164,7 +1168,7 @@ export class ShadowVaultBootstrap {
|
|||
* writes the per-vault data.json into pluginDir as a real file,
|
||||
* leaving the source vault's data.json untouched.
|
||||
*/
|
||||
private installPlugin(pluginDir: string): 'symlink' | 'copy' {
|
||||
private installPlugin(pluginDir: string): 'symlink' | 'copy' | 'in-place' {
|
||||
// If pluginDir is a stale whole-dir symlink from an older build
|
||||
// (or a previous run of this same code on an older version),
|
||||
// unlink it — DO NOT rmSync, that would follow the link and
|
||||
|
|
@ -1178,6 +1182,23 @@ export class ShadowVaultBootstrap {
|
|||
// Doesn't exist yet, fine.
|
||||
}
|
||||
|
||||
// Self-install guard: when the bootstrap runs from INSIDE the
|
||||
// shadow window it targets, sourcePluginDir IS pluginDir. The
|
||||
// rm+symlink cycle below would then delete each real plugin file
|
||||
// and replace it with a symlink pointing at its own path — a
|
||||
// self-referential link Obsidian can't resolve, so the plugin
|
||||
// "disappears" on the next start. The bundle is already here;
|
||||
// there is nothing to install.
|
||||
try {
|
||||
if (fs.realpathSync(this.sourcePluginDir) === fs.realpathSync(pluginDir)) {
|
||||
return 'in-place';
|
||||
}
|
||||
} catch {
|
||||
// Either side unresolvable (e.g. pluginDir doesn't exist yet, or
|
||||
// the stale whole-dir symlink was just unlinked) → not the same
|
||||
// dir; proceed with a normal install.
|
||||
}
|
||||
|
||||
fs.mkdirSync(pluginDir, { recursive: true });
|
||||
|
||||
const sharedFiles = ['main.js', 'manifest.json', 'styles.css'];
|
||||
|
|
|
|||
|
|
@ -438,6 +438,28 @@ describe('ShadowVaultBootstrap.bootstrap', () => {
|
|||
expect(stat.isDirectory()).toBe(true);
|
||||
});
|
||||
|
||||
it('regression: re-bootstrap from INSIDE the shadow window (source == target plugin dir) must not self-symlink the plugin files', async () => {
|
||||
// First bootstrap from a normal source vault.
|
||||
const r1 = new ShadowVaultBootstrap(scratch.baseDir, scratch.sourceDir, new ObsidianRegistry(scratch.configPath));
|
||||
const profile = makeProfile({ id: 'p1', name: 'P', remotePath: '/p1' });
|
||||
const first = await r1.bootstrap(profile, [profile]);
|
||||
|
||||
// Simulate the plugin now RUNNING inside that shadow window with
|
||||
// the user clicking Connect again: the running plugin's own dir
|
||||
// (the bootstrap source) IS the shadow's plugin dir. Before the
|
||||
// self-install guard, installPlugin rm'd each real file and left a
|
||||
// symlink pointing at its own path — the plugin then failed to
|
||||
// load ("disappeared") on the next Obsidian start.
|
||||
const r2 = new ShadowVaultBootstrap(scratch.baseDir, first.layout.pluginDir, new ObsidianRegistry(scratch.configPath));
|
||||
const second = await r2.bootstrap(profile, [profile]);
|
||||
|
||||
expect(second.layout.pluginDir).toBe(first.layout.pluginDir);
|
||||
expect(second.pluginInstallMethod).toBe('in-place');
|
||||
// main.js must still resolve to the staged bundle content.
|
||||
const mainJs = path.join(second.layout.pluginDir, 'main.js');
|
||||
expect(fs.readFileSync(mainJs, 'utf-8')).toContain('fake bundled plugin');
|
||||
});
|
||||
|
||||
it('writes [remote-ssh] only into shadow community-plugins.json on first bootstrap (does not auto-install source plugins)', async () => {
|
||||
fs.writeFileSync(
|
||||
path.join(scratch.sourceConfigDir, 'community-plugins.json'),
|
||||
|
|
|
|||
Loading…
Reference in a new issue