Skip to content

Commit 43aa0c2

Browse files
Bundle DreamNode git-init template into the plugin, not the InterBrain repo
Root cause of "ENOENT .git/udd" on fresh installs: the template path keyed off <vault>/InterBrain/src/features/dreamnode/DreamNode-template/ — but on a fresh install the daemon clones the InterBrain DreamNode from `main`, which is docs-only and has NO `src/` tree. The template never resolved, so every DreamNode creation (including the auto-created sender Dreamer node on invite-accept) failed. Fix: the template now ships INSIDE the plugin install dir. - assemble-plugin.mjs stages src/features/dreamnode/DreamNode-template/ → repo-root DreamNode-template/ (gitignored, like main.js) - copy-plugin-resources.mjs carries it into the Tauri resource bundle - install_managed copies it into <vault>/.obsidian/plugins/<id>/ DreamNode-template/ (new copy_dir_recursive helper) - ensure_plugin_health treats a managed dir missing the template as unhealthy → re-stages it (so older installs self-heal on next daemon startup) - git-dreamnode-service.ts templatePath → <pluginDir>/DreamNode-template Now DreamNode creation works on any fresh install regardless of which branch the InterBrain DreamNode repo was cloned from. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent bb18af3 commit 43aa0c2

5 files changed

Lines changed: 105 additions & 16 deletions

File tree

.gitignore

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,10 @@ dist
105105
/main.js
106106
*.zip
107107
/styles.css
108+
# Staged copy of the DreamNode git-init template — assemble-plugin.mjs
109+
# copies it here from src/features/dreamnode/DreamNode-template/ (the
110+
# source of truth) so it gets bundled into the plugin install dir.
111+
/DreamNode-template/
108112

109113
# macOS
110114
.DS_Store

desktop/scripts/copy-plugin-resources.mjs

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
*
88
* Run automatically before `tauri build` and `vite build`.
99
*/
10-
import { copyFileSync, existsSync, mkdirSync } from 'node:fs';
10+
import { copyFileSync, existsSync, mkdirSync, cpSync } from 'node:fs';
1111
import { dirname, join } from 'node:path';
1212
import { fileURLToPath } from 'node:url';
1313

@@ -24,6 +24,16 @@ const items = [
2424
['theme/interbrain.css', 'interbrain.css'],
2525
];
2626

27+
// Directories to copy wholesale (sourceRelativeToRepo, dstNameInResourceDir).
28+
const dirItems = [
29+
// The DreamNode git-init template. assemble-plugin.mjs stages it at the
30+
// repo root; we carry it into the Tauri resource bundle so the installed
31+
// plugin can find it at <pluginDir>/DreamNode-template. Must NOT depend
32+
// on the InterBrain DreamNode repo — that's cloned from `main` on fresh
33+
// installs and has no `src/` tree.
34+
['DreamNode-template', 'DreamNode-template'],
35+
];
36+
2737
mkdirSync(resourceDir, { recursive: true });
2838

2939
const missing = [];
@@ -38,6 +48,17 @@ for (const [rel, dstName] of items) {
3848
console.log(`copied ${rel}${dst}`);
3949
}
4050

51+
for (const [rel, dstName] of dirItems) {
52+
const src = join(repoRoot, rel);
53+
if (!existsSync(src)) {
54+
missing.push(rel);
55+
continue;
56+
}
57+
const dst = join(resourceDir, dstName);
58+
cpSync(src, dst, { recursive: true });
59+
console.log(`copied dir ${rel}${dst}`);
60+
}
61+
4162
if (missing.length > 0) {
4263
console.error(`\nMissing plugin files at repo root: ${missing.join(', ')}`);
4364
console.error('Run `npm run build:plugin` from the repo root first.');

desktop/src-tauri/src/vaults.rs

Lines changed: 45 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -211,6 +211,23 @@ pub fn install_managed(vault_path: &Path, bundled_dir: &Path) -> Result<()> {
211211
fs::copy(bundled_dir.join(file), staging.join(file))?;
212212
}
213213

214+
// Carry the DreamNode git-init template into the plugin install dir.
215+
// The plugin reads it from <pluginDir>/DreamNode-template at runtime
216+
// for `git init --template=…`. Best-effort: older bundles may not
217+
// ship it, in which case DreamNode creation would fail with a clear
218+
// error — but every rc.21+ build includes it.
219+
let template_src = bundled_dir.join("DreamNode-template");
220+
if template_src.is_dir() {
221+
copy_dir_recursive(&template_src, &staging.join("DreamNode-template"))
222+
.with_context(|| "stage DreamNode-template into plugin dir")?;
223+
} else {
224+
tracing::warn!(
225+
target: "vaults",
226+
path = %template_src.display(),
227+
"bundled DreamNode-template missing — DreamNode creation will fail until rebuilt"
228+
);
229+
}
230+
214231
// Now replace the live target — only AFTER staging is fully populated.
215232
if target.exists() {
216233
remove_path(&target)?;
@@ -304,6 +321,27 @@ fn remove_path(p: &Path) -> Result<()> {
304321
Ok(())
305322
}
306323

324+
/// Recursively copy a directory tree. Used to stage the bundled
325+
/// DreamNode-template into a vault's plugin dir. Plain file/dir copy —
326+
/// no symlink following, no special handling; the template is just a
327+
/// few small files (udd, hooks/, LICENSE, README).
328+
fn copy_dir_recursive(src: &Path, dst: &Path) -> Result<()> {
329+
fs::create_dir_all(dst)
330+
.with_context(|| format!("create_dir_all {}", dst.display()))?;
331+
for entry in fs::read_dir(src).with_context(|| format!("read_dir {}", src.display()))? {
332+
let entry = entry?;
333+
let from = entry.path();
334+
let to = dst.join(entry.file_name());
335+
if entry.file_type()?.is_dir() {
336+
copy_dir_recursive(&from, &to)?;
337+
} else {
338+
fs::copy(&from, &to)
339+
.with_context(|| format!("copy {} -> {}", from.display(), to.display()))?;
340+
}
341+
}
342+
Ok(())
343+
}
344+
307345
/// Clone the InterBrain repo into the vault at `<vault>/InterBrain` so the
308346
/// plugin source code lives alongside the user's data — the canonical
309347
/// "plugin code is a DreamNode" pattern. Idempotent: clones if missing,
@@ -449,8 +487,13 @@ pub fn ensure_plugin_health(
449487
true
450488
}
451489
} else if meta.is_dir() {
452-
// Regular dir must contain manifest + main.js.
453-
!plugin_dir.join("manifest.json").exists() || !plugin_dir.join("main.js").exists()
490+
// Regular dir must contain manifest + main.js + the bundled
491+
// DreamNode-template (added rc.21+; an older managed install
492+
// won't have it and needs a re-stage so DreamNode creation
493+
// works).
494+
!plugin_dir.join("manifest.json").exists()
495+
|| !plugin_dir.join("main.js").exists()
496+
|| !plugin_dir.join("DreamNode-template").is_dir()
454497
} else {
455498
true
456499
}

scripts/assemble-plugin.mjs

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
* Replaces the POSIX `cp ... && cat ... > ...` pipeline that broke on
99
* Windows. Cross-platform, idempotent.
1010
*/
11-
import { copyFileSync, readFileSync, writeFileSync, existsSync } from 'node:fs';
11+
import { copyFileSync, readFileSync, writeFileSync, existsSync, cpSync } from 'node:fs';
1212
import { dirname, join } from 'node:path';
1313
import { fileURLToPath } from 'node:url';
1414

@@ -33,3 +33,20 @@ const base = existsSync(baseCss) ? readFileSync(baseCss, 'utf8') : '';
3333
const main = existsSync(distMainCss) ? readFileSync(distMainCss, 'utf8') : '';
3434
writeFileSync(outStyles, base + main, 'utf8');
3535
console.log(`wrote ${outStyles} (${base.length} + ${main.length} bytes)`);
36+
37+
// Bundle the DreamNode git-init template into the plugin install dir.
38+
// `git init --template=<dir>` seeds every new DreamNode's .git/ with the
39+
// `udd` placeholder + hooks. It MUST travel with the plugin itself —
40+
// keying it off the InterBrain DreamNode repo (as we did before) breaks
41+
// on fresh installs: the daemon clones that repo from `main`, which is
42+
// docs-only and has no `src/` tree at all. The plugin reads this copy
43+
// from its own install dir via `<pluginDir>/DreamNode-template`.
44+
const templateSrc = join(repoRoot, 'src', 'features', 'dreamnode', 'DreamNode-template');
45+
const templateDst = join(repoRoot, 'DreamNode-template');
46+
if (existsSync(templateSrc)) {
47+
cpSync(templateSrc, templateDst, { recursive: true });
48+
console.log(`copied ${templateSrc}${templateDst}`);
49+
} else {
50+
console.error(`assemble-plugin: missing template dir ${templateSrc}`);
51+
process.exit(1);
52+
}

src/features/dreamnode/services/git-dreamnode-service.ts

Lines changed: 16 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -58,24 +58,28 @@ export class GitDreamNodeService {
5858
}
5959

6060
this.vaultPath = vaultPath;
61-
62-
// Template lives in the InterBrain DreamNode that the daemon clones
63-
// into every registered vault. Path: <vault>/InterBrain/src/features/
64-
// dreamnode/DreamNode-template/. Used by `git init --template=…` to
65-
// seed each new DreamNode's `.git/` directory with `udd` + hooks +
66-
// README.md placeholders.
61+
62+
// The DreamNode git-init template ships INSIDE the plugin install dir
63+
// (<vault>/.obsidian/plugins/<id>/DreamNode-template/). It's bundled
64+
// there by assemble-plugin.mjs + copy-plugin-resources.mjs. Used by
65+
// `git init --template=…` to seed each new DreamNode's `.git/` with
66+
// the `udd` placeholder + hooks.
67+
//
68+
// It must NOT key off the InterBrain DreamNode repo: on a fresh
69+
// install the daemon clones that repo from `main`, which is docs-only
70+
// and has no `src/` tree — so the template would never resolve.
6771
if (this.vaultPath) {
6872
this.templatePath = path.join(
69-
this.vaultPath, 'InterBrain', 'src', 'features', 'dreamnode', 'DreamNode-template'
73+
this.vaultPath, '.obsidian', 'plugins', plugin.manifest.id, 'DreamNode-template'
7074
);
7175
} else {
72-
// Pre-onload edge case (vault path not resolvable yet) — fall back to
73-
// a repo-relative path that only works in dev. Production loads the
74-
// adapter basePath synchronously so this branch never fires.
75-
this.templatePath = './InterBrain/src/features/dreamnode/DreamNode-template';
76+
// Pre-onload edge case (vault path not resolvable yet) — fall back
77+
// to a repo-relative path that only works in dev. Production loads
78+
// the adapter basePath synchronously so this branch never fires.
79+
this.templatePath = './src/features/dreamnode/DreamNode-template';
7680
console.warn('GitDreamNodeService: Could not determine vault path, using fallback template path:', this.templatePath);
7781
}
78-
82+
7983
}
8084

8185
/**

0 commit comments

Comments
 (0)