create-obsidian-arrow 0.1.8 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -24,10 +24,11 @@ pnpm dev
24
24
  > `public/app.css` is git-ignored and never bundled — it's Obsidian's proprietary
25
25
  > CSS, so each developer extracts it from their own install via `pnpm pull-css`.
26
26
 
27
- ### Install the bundled skills
27
+ ### Install the agent skills
28
28
 
29
- The scaffold ships agent skills and installs them via the [`skills`](https://github.com/vercel-labs/skills)
30
- CLI:
29
+ Skills aren't vendored into the scaffold `skills:install` pulls them from the
30
+ published repo (source of truth, always current) via the
31
+ [`skills`](https://github.com/vercel-labs/skills) CLI:
31
32
 
32
33
  ```sh
33
34
  pnpm skills:install --yes # non-interactive — installs all
@@ -68,8 +69,9 @@ node create-obsidian-arrow/index.mjs update ../my-app # update
68
69
 
69
70
  A full sandbox: client-only Vite + TS, `@arrow-js/core` + `@arrow-js/framework`
70
71
  (no SSR), `routeToPage` + Navigation-API router with an `/example` demo, Biome +
71
- husky pre-commit + `node:test` + GitHub Actions CI, bundled agent skills, and the
72
- `pull-css` script that extracts Obsidian's `app.css`.
72
+ husky pre-commit + `node:test` + GitHub Actions CI, a `skills:install` that pulls
73
+ the agent skills from the published repo, and the `pull-css` script that extracts
74
+ Obsidian's `app.css`.
73
75
 
74
76
  ## Maintaining the template
75
77
 
package/index.mjs CHANGED
@@ -27,17 +27,10 @@ const templateDir = path.join(here, "template");
27
27
 
28
28
  // Files/dirs the template owns and `update` may overwrite/merge. Everything else
29
29
  // (src/, public/, index.html, vite.config.ts, tsconfig.json, lockfile, .gitignore,
30
- // port-parity.json, …) is treated as user-owned and left alone.
31
- const MANAGED = [
32
- "scripts",
33
- "skills",
34
- "docs",
35
- ".github",
36
- ".husky",
37
- "biome.json",
38
- "AGENTS.md",
39
- "CLAUDE.md",
40
- ];
30
+ // port-parity.json, …) is treated as user-owned and left alone. Skills aren't
31
+ // here they're pulled from the published repo via `pnpm skills:update`, not
32
+ // vendored into the scaffold.
33
+ const MANAGED = ["scripts", "docs", ".github", ".husky", "biome.json", "AGENTS.md", "CLAUDE.md"];
41
34
 
42
35
  const argv = process.argv.slice(2);
43
36
  const dryRun = argv.includes("--dry-run");
@@ -182,7 +175,7 @@ function update(targetArg) {
182
175
  console.log(
183
176
  dryRun
184
177
  ? "\n(dry run — nothing written.)"
185
- : "\nLeft alone: src/, public/, index.html, vite.config.ts, tsconfig.json, .gitignore.\nRun `pnpm install` then `pnpm check`.\n"
178
+ : "\nLeft alone: src/, public/, index.html, vite.config.ts, tsconfig.json, .gitignore.\nRun `pnpm install` then `pnpm check`. Update skills separately with `pnpm skills:update`.\n"
186
179
  );
187
180
  }
188
181
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-obsidian-arrow",
3
- "version": "0.1.8",
3
+ "version": "0.2.1",
4
4
  "description": "Scaffold an Obsidian-styled Arrow.js UI sandbox (pnpm create obsidian-arrow <dir>).",
5
5
  "type": "module",
6
6
  "bin": {
@@ -13,8 +13,8 @@ This file is the hub — everything else is linked from here:
13
13
  obsidian-arrow-sandbox, arrow-js-obsidian-templates, arrow-js-obsidian-patterns,
14
14
  arrow-js-obsidian-porting (sandbox→plugin parity check), obsidian-arrow-maintenance
15
15
  (updating an existing project).
16
- - [`docs/prompts/agent-setup.md`](docs/prompts/agent-setup.md) — prompt for
17
- briefing a fresh agent (scaffold + orient).
16
+ - [`docs/prompts/`](docs/prompts/) — copy-paste agent prompts: `agent-setup.md`
17
+ (scaffold + orient) and `update-existing.md` (update tooling + skills, keep src).
18
18
 
19
19
  Design rationale (why `core`+`framework`, no SSR, how `app.css` is sourced) is
20
20
  summarized in "What this is (and isn't)" below and in the README.
@@ -41,8 +41,9 @@ pnpm dev # Vite + HMR
41
41
  `public/app.css` is **git-ignored** (Obsidian's proprietary CSS — not
42
42
  redistributed); run `pnpm pull-css` once before `pnpm dev`.
43
43
 
44
- Install the bundled skills: `pnpm skills:install --yes` (non-interactive, all
45
- skills) or `pnpm skills:install` (TUI); update with `pnpm skills:update`. Scope
44
+ Install the skills (pulled from the published repo, not vendored):
45
+ `pnpm skills:install --yes` (non-interactive, all skills) or `pnpm skills:install`
46
+ (TUI); update with `pnpm skills:update`. Scope
46
47
  flags: `--agent <name>`, `--project-dir=<path>`, `--global`. **Nested inside
47
48
  another repo?** Skills install cwd-relative — use `--project-dir=<outer-repo>` so
48
49
  they land where an agent at the outer repo looks. To refresh an existing
@@ -33,15 +33,18 @@ npx create-obsidian-arrow update # in the project (or: update <dir>)
33
33
  npx create-obsidian-arrow update --dry-run # preview first
34
34
  ```
35
35
 
36
- > **Nested in another repo?** If you scaffold inside an existing repo, bundled
37
- > skills install scoped to the scaffold folder (the `skills` CLI is cwd-relative).
38
- > To install them at the outer repo instead:
39
- > `pnpm skills:install --yes --project-dir=<outer-repo>` (or `--global` for
40
- > user-level). The scaffolder prints this hint when it detects nesting.
36
+ > **Nested in another repo?** If you scaffold inside an existing repo, skills
37
+ > install scoped to the scaffold folder (the `skills` CLI is cwd-relative), so the
38
+ > session/`skill://` registry which only indexes the **repo root** + global —
39
+ > won't see them. Install at the outer repo instead:
40
+ > `pnpm skills:install --yes --project-dir=<outer-repo>` (or, from the outer repo,
41
+ > `npx skills add kylebrodeur/obsidian-arrow-sandbox --all --yes`), then **reload
42
+ > the session**. The scaffolder prints this hint when it detects nesting.
41
43
 
42
44
  > This repo (the full sandbox) is **not** published to npm — only the
43
- > `create-obsidian-arrow/` initializer is. An agent-onboarding prompt lives in
44
- > [`docs/prompts/agent-setup.md`](docs/prompts/agent-setup.md).
45
+ > `create-obsidian-arrow/` initializer is. Copy-paste agent prompts live in
46
+ > [`docs/prompts/`](docs/prompts/): `agent-setup.md` (scaffold + orient) and
47
+ > `update-existing.md` (update tooling + skills, keeping `src/` intact).
45
48
 
46
49
  ## Quick start
47
50
 
@@ -81,10 +84,12 @@ Obsidian install.
81
84
  A husky `pre-commit` hook runs `lint-staged` (Biome on staged files) + a full
82
85
  typecheck. CI (`.github/workflows/ci.yml`) runs the `ci` script on push/PR.
83
86
 
84
- ## Bundled agent skills
87
+ ## Agent skills
85
88
 
86
- This repo ships [`skills`](https://github.com/vercel-labs/skills)-compatible skills
87
- under [`skills/`](skills/) — it doubles as a local skill marketplace:
89
+ This repo is the source of truth for five [`skills`](https://github.com/vercel-labs/skills)-compatible
90
+ skills under [`skills/`](skills/) — it's a skill marketplace. Scaffolds **don't
91
+ vendor copies**; they pull from this published repo, so installs are always
92
+ current.
88
93
 
89
94
  - `obsidian-arrow-sandbox` — running and using this sandbox, and porting to a plugin.
90
95
  - `arrow-js-obsidian-templates` — Arrow v1.0.6 template rules + footguns.
@@ -96,19 +101,23 @@ under [`skills/`](skills/) — it doubles as a local skill marketplace:
96
101
  - `obsidian-arrow-maintenance` — updating an existing project: `create-obsidian-arrow
97
102
  update`, `skills:update`, nesting/`--project-dir`, re-pull styling.
98
103
 
99
- Install them into your agent:
104
+ Install them into your agent (pulls from the published repo; the sandbox repo
105
+ itself uses its local `skills/`):
100
106
 
101
107
  ```sh
102
108
  pnpm skills:install # interactive picker (TUI) on a terminal
103
- pnpm skills:install --yes # non-interactive — installs ALL bundled skills
109
+ pnpm skills:install --yes # non-interactive — installs all skills
104
110
  pnpm skills:install --yes --agent claude-code # install for one agent only
105
111
  pnpm skills:install --yes --project-dir=<repo-root> # install into another project root
106
112
  pnpm skills:update # update an existing setup to the latest
107
113
  ```
108
114
 
115
+ Anywhere, with no project at all:
116
+ `npx skills add kylebrodeur/obsidian-arrow-sandbox --all --yes`.
117
+
109
118
  `postinstall` offers the picker automatically after `pnpm install`, but only in
110
119
  an interactive terminal — in CI / non-TTY it just prints how to install (never
111
- hangs). For agents/CI, use `--yes` (it runs `npx skills add . --all --yes`).
120
+ hangs). For agents/CI, use `--yes`.
112
121
  Scope flags: `--agent <name>` (one agent), `--project-dir=<path>` (install into a
113
122
  different project root, e.g. an outer repo a scaffold is nested in), `--global`
114
123
  (user-level, available everywhere). `pnpm skills:update` runs
@@ -32,8 +32,8 @@ Then:
32
32
  # or set OBSIDIAN_ASAR=<path>.
33
33
  pnpm dev # open the printed URL: / is the examples index, /example the demo.
34
34
  # The toolbar slider/presets + edge drag handle resize the panel.
35
- pnpm skills:install --yes # install ALL bundled agent skills non-interactively
36
- # (runs `npx skills add . --all --yes`) — this loads
35
+ pnpm skills:install --yes # install all agent skills non-interactively, pulled
36
+ # from the published repo (not vendored) — this loads
37
37
  # the domain knowledge. Drop --yes for an interactive picker.
38
38
  # NESTED inside another repo? Skills install cwd-relative,
39
39
  # so add --project-dir=<outer-repo> (or --global) to put them
@@ -0,0 +1,78 @@
1
+ # Update-existing-project prompt
2
+
3
+ Copy the block below and give it to a coding agent to bring an **existing**
4
+ Obsidian Arrow project fully up to date — tooling, skills, styling — **without
5
+ touching your source code**.
6
+
7
+ ---
8
+
9
+ ```text
10
+ Update this existing Obsidian Arrow project to the latest tooling and agent
11
+ skills. Do NOT change any of my source code.
12
+
13
+ HARD CONSTRAINTS
14
+ - Never modify src/, public/, index.html, vite.config.ts, tsconfig.json, or my
15
+ component/app code. Only managed tooling, skills, and docs may change.
16
+ - Preview before applying anything destructive; don't commit unless I ask.
17
+ - At the end, prove src/ was untouched (`git diff --stat` shows no src/ changes).
18
+
19
+ CONTEXT
20
+ Scaffolded from create-obsidian-arrow. Tooling + agent skills come from the
21
+ published repo kylebrodeur/obsidian-arrow-sandbox; skills install via the
22
+ `skills` CLI and are NOT vendored in the project.
23
+
24
+ STEPS (in order)
25
+
26
+ 1. Locate + nesting check
27
+ - Find the project root (has package.json + scripts/).
28
+ - Is it nested in a larger repo? Compare `git rev-parse --show-toplevel` to the
29
+ project dir. If the OUTER root differs, that's where skills must be installed
30
+ so the session's skill:// registry (repo-root + global only) can find them.
31
+
32
+ 2. Refresh tooling — preserves src/
33
+ - Preview: npx create-obsidian-arrow update --dry-run
34
+ - Apply: npx create-obsidian-arrow update
35
+ - Then: pnpm install
36
+ Refreshes scripts/, docs/, .github/, .husky/, biome.json, AGENTS.md, CLAUDE.md
37
+ and merges package.json scripts/deps. It never touches src/, public/, or configs.
38
+
39
+ 3. Reset agent skills to the current set
40
+ - Audit: npx skills list
41
+ - Remove any stale/old ones (older setups were missing some skills):
42
+ npx skills remove obsidian-arrow-sandbox arrow-js-obsidian-templates \
43
+ arrow-js-obsidian-patterns arrow-js-obsidian-porting obsidian-arrow-maintenance
44
+ - Install the current set from the published repo, AT THE RIGHT ROOT (the OUTER
45
+ repo root if nested):
46
+ npx skills add kylebrodeur/obsidian-arrow-sandbox --all --yes
47
+ (equivalently from the project: pnpm skills:install --yes
48
+ [--project-dir=<outer-repo>])
49
+ - Reload the agent session so skill:// resolves them.
50
+
51
+ 4. Refresh Obsidian styling
52
+ - pnpm pull-css (needs a local Obsidian; macOS auto-detected, else --path)
53
+
54
+ 5. Clean stray install artifacts
55
+ - git status — untracked .agents/ (now git-ignored) or stray skills-lock.json?
56
+ - Broken symlinks: find .agents ~/.claude/skills -type l ! -exec test -e {} \; -print
57
+
58
+ 6. Re-check porting parity (only if you keep a port-parity manifest)
59
+ - node scripts/component-hash.mjs --check port-parity.json
60
+
61
+ 7. Verify
62
+ - pnpm run ci (biome + typecheck + tests + build)
63
+ - pnpm dev and confirm /example renders with a clean console
64
+
65
+ REPORT
66
+ - What `update` changed, which skills are now installed and where, the pull-css
67
+ result, and `git diff --stat` confirming src/ is untouched.
68
+ ```
69
+
70
+ ---
71
+
72
+ ## Notes
73
+ - Skills are pulled from the published repo, so step 3 is location-independent for
74
+ the *source* — only the *install location* matters (run it at the repo root the
75
+ agent uses; reload after).
76
+ - `npx create-obsidian-arrow update` is **create-only-safe**: it refreshes managed
77
+ files and merges `package.json`, but never overwrites `src/`, `public/`,
78
+ `index.html`, or build configs. See the `obsidian-arrow-maintenance` skill.
@@ -37,7 +37,7 @@ From inside the scaffolded project:
37
37
 
38
38
  ```sh
39
39
  pnpm skills:install # interactive picker (TUI) on a terminal
40
- pnpm skills:install --yes # non-interactive — installs ALL bundled skills (for agents/CI)
40
+ pnpm skills:install --yes # non-interactive — installs all skills from the published repo (agents/CI)
41
41
  pnpm skills:update # update an already-installed setup to the latest
42
42
  ```
43
43
 
@@ -1,30 +1,33 @@
1
1
  #!/usr/bin/env node
2
2
  /**
3
- * Install / update the skills bundled in this repo (skills/*) via the `skills`
4
- * CLI (https://github.com/vercel-labs/skills). This repo is a local skills
5
- * marketplace: the source `.` resolves to skills/<name>/SKILL.md.
3
+ * Install / update the obsidian-arrow agent skills via the `skills` CLI
4
+ * (https://github.com/vercel-labs/skills).
5
+ *
6
+ * Skills are NOT vendored into scaffolds — they're pulled from the published
7
+ * repo (the source of truth), so installs are always current and don't depend on
8
+ * a bundled copy. The sandbox repo itself (which has a local `skills/` dir) uses
9
+ * that local copy instead, so you can test edits before publishing.
6
10
  *
7
11
  * Modes (auto-detected):
8
12
  * - Interactive terminal → opens the picker TUI (choose what to install).
9
- * - Non-interactive / CI / -y → installs ALL bundled skills, no prompts.
13
+ * - Non-interactive / CI / -y → installs ALL skills, no prompts.
10
14
  * - --update → updates an already-installed setup in place.
11
15
  *
12
16
  * Wiring:
13
17
  * - `postinstall` (auto on `pnpm install`): only acts in an interactive
14
18
  * terminal; in CI/non-TTY it prints how to install and exits 0 (never hangs).
15
- * - `pnpm skills:install` → picker (TUI) on a terminal, else installs all.
16
- * - `pnpm skills:install --yes` always non-interactive: install all skills.
17
- * - `pnpm skills:update` → update installed skills to the latest.
19
+ * - `pnpm skills:install [--yes]` → picker, or non-interactive install of all.
20
+ * - `pnpm skills:update` update installed skills to the latest.
18
21
  *
19
22
  * Flags / env:
23
+ * --source <ref> | SKILLS_SOURCE=<ref> where to pull skills from (default: the
24
+ * published repo; a local `skills/` dir is used if present)
20
25
  * --update / -u update installed skills (runs `skills update -y`)
21
- * --yes / -y non-interactive install of all bundled skills
22
- * --agent <name> | --agent=<name> install for one agent (e.g. claude-code)
23
- * instead of all detected agents
26
+ * --yes / -y non-interactive install of all skills
27
+ * --agent <name> | --agent=<name> install for one agent instead of all
24
28
  * --project-dir=<path> install into another project root (e.g. the outer repo
25
29
  * a scaffold is nested in); project-scoped there
26
- * --global / -g install at user level (~/.<agent>/…), available
27
- * everywhere; some agents don't support global
30
+ * --global / -g install at user level (~/.<agent>/…), everywhere
28
31
  * SKILLS_AGENT / SKILLS_PROJECT_DIR / SKILLS_GLOBAL env forms (for the auto
29
32
  * `postinstall` step, which can't take CLI args, and CI)
30
33
  * --dry-run / SKILLS_DRY_RUN=1 print the command instead of running it
@@ -35,8 +38,9 @@ import fs from "node:fs";
35
38
  import path from "node:path";
36
39
  import process from "node:process";
37
40
 
38
- const BUNDLED =
39
- "obsidian-arrow-sandbox, arrow-js-obsidian-templates, arrow-js-obsidian-patterns, arrow-js-obsidian-porting";
41
+ const REPO = "kylebrodeur/obsidian-arrow-sandbox";
42
+ const SKILLS =
43
+ "obsidian-arrow-sandbox, arrow-js-obsidian-templates, arrow-js-obsidian-patterns, arrow-js-obsidian-porting, obsidian-arrow-maintenance";
40
44
 
41
45
  const has = (flag) => process.argv.includes(flag);
42
46
  // Accepts both `--flag value` and `--flag=value`.
@@ -49,9 +53,7 @@ const flagValue = (flag) => {
49
53
  return i >= 0 ? process.argv[i + 1] : undefined;
50
54
  };
51
55
 
52
- /** Nearest ancestor *above* `dir` that is a git repo, or null. Used to warn
53
- * when this project is nested inside another repo (skills install per-project,
54
- * relative to cwd — not the outer root). */
56
+ /** Nearest ancestor *above* `dir` that is a git repo, or null. */
55
57
  function outerRepoAbove(dir) {
56
58
  let current = path.dirname(path.resolve(dir));
57
59
  const fsRoot = path.parse(current).root;
@@ -75,10 +77,14 @@ const isCI = Boolean(process.env.CI);
75
77
  const interactive = Boolean(process.stdout.isTTY && process.stdin.isTTY);
76
78
  const optedOut = process.env.SKIP_SKILLS_INSTALL === "1";
77
79
 
78
- // `skills add` installs project-scope relative to cwd. To install into another
79
- // root (e.g. the outer repo a scaffold is nested in), run the CLI there while
80
- // sourcing the skills from THIS folder by absolute path.
81
- const skillsSource = projectDir ? path.resolve(".") : ".";
80
+ // Pull from the published repo by default; prefer a local `skills/` dir if
81
+ // present (the sandbox repo itself); honor an explicit --source override.
82
+ const explicitSource = flagValue("--source") || process.env.SKILLS_SOURCE;
83
+ const hasLocalSkills = fs.existsSync(path.join(process.cwd(), "skills"));
84
+ const baseSource = explicitSource ?? (hasLocalSkills ? "." : REPO);
85
+ // A repo ref is cwd-independent; a local "." must be made absolute when
86
+ // --project-dir redirects cwd so it still resolves.
87
+ const source = projectDir && baseSource === "." ? path.resolve(".") : baseSource;
82
88
  const targetCwd = projectDir ? path.resolve(projectDir) : process.cwd();
83
89
 
84
90
  if (projectDir && !fs.existsSync(targetCwd)) {
@@ -101,7 +107,7 @@ function run(args, cwd = process.cwd()) {
101
107
  });
102
108
  if (result.error) {
103
109
  console.error(
104
- `[skills] could not run npx skills (${result.error.message}). Install manually: npx skills add . --all --yes`
110
+ `[skills] could not run npx skills (${result.error.message}). Install manually: npx skills add ${REPO} --all --yes`
105
111
  );
106
112
  process.exit(0); // never fail an install over an optional convenience step
107
113
  }
@@ -123,27 +129,24 @@ if (update) {
123
129
  // `pnpm install` in CI never hangs or installs things unprompted.
124
130
  if (!forced && (isCI || !interactive)) {
125
131
  console.log(
126
- `[skills] Bundled skills: ${BUNDLED}\n Install: pnpm skills:install (interactive picker on a terminal)\n pnpm skills:install --yes (non-interactive — installs all)\n Update: pnpm skills:update`
132
+ `[skills] Agent skills: ${SKILLS}\n Install: pnpm skills:install (interactive picker on a terminal)\n pnpm skills:install --yes (non-interactive — installs all)\n Update: pnpm skills:update`
127
133
  );
128
134
  process.exit(0);
129
135
  }
130
136
 
131
137
  // Non-interactive (CI/agent/no TTY, or --yes, or an agent/global target): install
132
- // ALL bundled skills with no prompts. Target one agent if asked, else all agents.
138
+ // ALL skills with no prompts. Target one agent if asked, else all agents.
133
139
  if (!interactive || yes || agent || global || projectDir) {
134
- console.log(`[skills] Installing all bundled skills non-interactively: ${BUNDLED}`);
140
+ console.log(`[skills] Installing all skills non-interactively from ${source}: ${SKILLS}`);
135
141
  const outer = global || projectDir ? null : outerRepoAbove(process.cwd());
136
142
  if (outer) {
137
143
  console.log(
138
- `[skills] note: this folder is nested inside ${outer}. Skills install here, scoped to THIS project. To install at the outer repo instead, re-run with --project-dir=${outer} (or --global for user-level).`
144
+ `[skills] note: this folder is nested inside ${outer}, and skills install scoped to cwd. For session/skill:// discovery they must live at the repo root install there with:\n pnpm skills:install --yes --project-dir=${outer}\n (or, from ${outer}: npx skills add ${REPO} --all --yes). Reload the session afterwards.`
139
145
  );
140
146
  }
141
147
  // `--all` is shorthand for `-s * -a * -y`; to target one agent we spell out
142
- // all-skills + that agent explicitly. `skillsSource` is absolute when
143
- // --project-dir redirects cwd elsewhere.
144
- const args = agent
145
- ? ["add", skillsSource, "-s", "*", "-a", agent, "-y"]
146
- : ["add", skillsSource, "--all"];
148
+ // all-skills + that agent explicitly.
149
+ const args = agent ? ["add", source, "-s", "*", "-a", agent, "-y"] : ["add", source, "--all"];
147
150
  if (global) {
148
151
  args.push("--global");
149
152
  }
@@ -151,5 +154,5 @@ if (!interactive || yes || agent || global || projectDir) {
151
154
  }
152
155
 
153
156
  // Interactive terminal: let the user pick in the TUI.
154
- console.log(`[skills] Opening the skills picker. Bundled: ${BUNDLED}`);
155
- run(["add", "."]);
157
+ console.log(`[skills] Opening the skills picker (source: ${source}). Skills: ${SKILLS}`);
158
+ run(["add", source]);
@@ -1,94 +0,0 @@
1
- ---
2
- name: arrow-js-obsidian-patterns
3
- description: Use when building reactive Obsidian plugin UI with @arrow-js/core beyond basic templates — icons (Obsidian uses Lucide; the data-icon sweep since setIcon can't run inside templates), CSS scoping/specificity against Obsidian's global rules, component mount/unmount lifecycle in an ItemView, and organizing shared reactive state. Complements arrow-js-obsidian-templates (syntax) with integration patterns.
4
- ---
5
-
6
- # Arrow.js + Obsidian integration patterns
7
-
8
- How to build real components (not just template syntax) for an Obsidian plugin
9
- view with `@arrow-js/core`. Pairs with **arrow-js-obsidian-templates** (the
10
- template rules + footguns).
11
-
12
- ## Mount / unmount lifecycle
13
-
14
- A view mounts a component imperatively, the same call the sandbox makes:
15
-
16
- ```ts
17
- import { html } from "@arrow-js/core"
18
- // in ItemView.onOpen():
19
- this.contentEl.empty()
20
- html`${MyComponent()}`(this.contentEl)
21
- ```
22
-
23
- - `html\`\`(container)` does **not** reliably return an unmount function — don't
24
- gate cleanup on its return value. Always `container.empty()` before remounting.
25
- - Re-rendering on route/state change: clear the container, then mount fresh.
26
- Reactive bindings (`${() => …}`) update in place — don't remount the whole tree
27
- on every state change; let Arrow patch the slots that read changed state.
28
-
29
- ## Shared reactive state
30
-
31
- One `reactive()` object is the single source of truth; components read it via
32
- getters so they stay in sync without prop-drilling.
33
-
34
- ```ts
35
- import { reactive } from "@arrow-js/core"
36
- export const state = reactive({ model: "", streaming: false, items: [] })
37
- // pass getters into components: Toggle(() => state.streaming, () => { ... })
38
- ```
39
-
40
- ## Icons (Obsidian uses Lucide)
41
-
42
- `setIcon(el, name)` needs a real DOM element, so it **cannot** be called inside
43
- an Arrow template expression (the element doesn't exist yet). Two options:
44
-
45
- 1. **In-plugin — the data-icon sweep.** Emit a placeholder, then sweep after
46
- mount:
47
-
48
- ```ts
49
- html`<span class="svg-icon" data-icon="copy"></span>`
50
- // after template(container) — scope the query to the container, not document:
51
- for (const el of Array.from(container.querySelectorAll<HTMLElement>("[data-icon]"))) {
52
- const name = el.getAttribute("data-icon")
53
- if (name) setIcon(el, name) // setIcon from "obsidian"
54
- }
55
- ```
56
- For sections that open after mount (dropdowns/popovers), run the sweep in a
57
- `nextTick(...)` after they render, scoped to that section's element.
58
-
59
- 2. **In the sandbox (no `obsidian` module).** Use a Lucide import or inline SVG,
60
- or plain text/emoji for chrome. The baseline uses text glyphs to stay
61
- shim-free; add a Lucide-backed `setIcon` shim (aliased as `obsidian`) when you
62
- port icon-using components in.
63
-
64
- ## CSS scoping & specificity (the big one)
65
-
66
- Obsidian's `app.css` applies global rules like
67
- `button:not(.clickable-icon) { background: var(--interactive-normal) }` at
68
- specificity **(0,1,1)**. A plain `.my-btn` selector is **(0,1,0)** and loses
69
- regardless of cascade order.
70
-
71
- ```css
72
- /* (0,1,0) — LOSES to Obsidian's global button rule */
73
- .my-action { background: var(--interactive-accent); }
74
-
75
- /* (0,2,1) — WINS, and can't leak */
76
- .my-panel button.my-action { background: var(--interactive-accent); }
77
- ```
78
-
79
- Rules: prefer Obsidian's own classes (`.setting-item`, `.clickable-icon`,
80
- `.workspace-leaf`, `.vertical-tab-*`, `.modal`) and `var(--…)` tokens first; only
81
- add custom CSS where there's no Obsidian class; always scope custom rules under a
82
- container class + element type.
83
-
84
- ## Common Obsidian layout classes
85
-
86
- - `.workspace-leaf` > `.workspace-leaf-content` — a view pane.
87
- - `.view-header` / `.view-content` — pane header + body.
88
- - `.setting-item` (+ `.setting-item-info` / `.setting-item-name` /
89
- `.setting-item-description` / `.setting-item-control`) — settings rows.
90
- - `.checkbox-container` (+ `.is-enabled`) — toggle.
91
- - `.clickable-icon` — icon button (escapes the global button background rule).
92
- - `.vertical-tab-header` / `.vertical-tab-nav-item` (+ `.is-active`) — tab nav.
93
- - `.modal-container` > `.modal` — modal/popover.
94
- - `.mod-cta` — primary call-to-action button.
@@ -1,92 +0,0 @@
1
- ---
2
- name: arrow-js-obsidian-porting
3
- description: Use when porting an Arrow component from the obsidian-arrow-sandbox into the real Obsidian plugin, or wiring a check that the plugin copy hasn't drifted from the sandbox source. Covers the content-addressed porting-parity tool (scripts/component-hash.mjs), the canonical-form rules, a port-parity manifest, and a husky/CI pre-commit hook. The sandbox is the source of truth; the plugin copy must match.
4
- ---
5
-
6
- # Porting parity (sandbox → Obsidian plugin)
7
-
8
- The sandbox is the **source of truth** for a component; the plugin gets a
9
- near-verbatim copy. The risk is **drift** — someone hand-edits the plugin copy,
10
- or a port silently diverges. This makes components **content-addressed**: a
11
- canonical hash of the component's portable body must match on both sides, the
12
- same idea as "never hand-edit generated artifacts."
13
-
14
- ## Write portable components
15
-
16
- So the body is byte-identical across sandbox and plugin and only the *mount site*
17
- differs:
18
-
19
- - Take data via **props / getters**, not hard-wired sources. The sandbox passes a
20
- stub (`loadStatus()`); the plugin passes the real source (rpc) — but that wiring
21
- lives at the **call site** (`main.ts` / `ItemView.onOpen()`), not in the
22
- component body.
23
- - Use Obsidian classes + `var(--…)` tokens (already the rule). Keep
24
- `obsidian`-API calls (`setIcon`, …) out of the component body where possible.
25
-
26
- ## The tool: `scripts/component-hash.mjs`
27
-
28
- Dependency-free Node; canonicalizer in `scripts/lib/canonical-source.mjs`.
29
-
30
- ```sh
31
- node scripts/component-hash.mjs <file> # print canonical hash
32
- node scripts/component-hash.mjs --verify <sandbox> <plugin> # PARITY OK | DRIFT (exit 0/1)
33
- node scripts/component-hash.mjs --check port-parity.json # check all; exit 1 on drift
34
- node scripts/component-hash.mjs --check port-parity.json --update # re-bless baselines
35
- ```
36
-
37
- **Canonical form** (define it so both sides agree by construction):
38
- - `import …;` statements are stripped (single- and multi-line) — imports are the
39
- main legit delta (stub vs real data, the `obsidian` API).
40
- - Line endings → `\n`, trailing whitespace trimmed, blank-line runs collapsed,
41
- leading/trailing blanks trimmed.
42
- - Comments are **kept** (they're part of the component; the sandbox owns them too).
43
- - SHA-256 of the result.
44
-
45
- So two copies that differ only in imports/formatting hash equal; any change to
46
- the body/template/comments shows as drift.
47
-
48
- ## Wiring the check in the plugin
49
-
50
- The hook lives in the **plugin** repo (that's where ported copies drift). Copy the
51
- two dependency-free files in (`scripts/lib/canonical-source.mjs`,
52
- `scripts/component-hash.mjs`) and add a manifest:
53
-
54
- ```jsonc
55
- // port-parity.json — paths resolve relative to THIS file
56
- {
57
- "entries": [
58
- {
59
- "plugin": "src/chat/arrow/SettingsPanel.ts",
60
- "sandbox": "../../arrow-ui/obsidian-arrow-sandbox/src/components/SettingsPanel.ts", // optional
61
- "hash": "<sha256>" // fallback when sandbox isn't checked out
62
- }
63
- ]
64
- }
65
- ```
66
-
67
- - If the `sandbox` path exists (sibling checkout), the check compares **plugin
68
- copy ↔ live sandbox source** — catches drift in both directions.
69
- - If it doesn't, it falls back to the recorded `hash` (catches hand-edits to the
70
- plugin copy). Record/refresh it with `--update` after an intentional re-port.
71
-
72
- husky `pre-commit` (and CI):
73
-
74
- ```sh
75
- node scripts/component-hash.mjs --check port-parity.json
76
- ```
77
-
78
- On drift the commit fails: *edit the sandbox component and re-port* (don't
79
- hand-edit the copy); use `--update` only to intentionally re-bless.
80
-
81
- ## What this does NOT do (by design)
82
-
83
- - **No runtime component-DOM hashing in Obsidian.** Source parity already
84
- guarantees same code → same Arrow DOM, and the sandbox vs real Obsidian DOM
85
- *intentionally* differ (icon stub vs `setIcon`, `MarkdownRenderer` nodes, live
86
- theme), so a runtime markup hash would be tautological *and* full of false
87
- mismatches. The real runtime risk is visual/CSS — verify that by **loading in
88
- Obsidian and looking**, not with a hash.
89
- - **Planned (not built): a styling-freshness check.** A small helper that hashes
90
- the *live Obsidian token set* and compares it to the sandbox's pulled `app.css`
91
- snapshot, to flag "re-run `pnpm pull-css`." That's the one runtime hash worth
92
- having — environment parity, not component parity. Add it when needed.
@@ -1,101 +0,0 @@
1
- ---
2
- name: arrow-js-obsidian-templates
3
- description: Use when writing @arrow-js/core (v1.0.6) html template literals — reactive vs static (wrap reactive reads in arrow functions), full-value attribute binding where returning false removes the attribute, .property and @event binding, keyed lists, and async component(fn, { fallback }) via boundary(); plus the footguns — no literal HTML comments and no partial attribute values (both throw Invalid HTML position at render), and @event handlers must type the param as Event not a narrowed subtype like MouseEvent (TS2345).
4
- ---
5
-
6
- # Arrow.js Templates (v1.0.6)
7
-
8
- Rules for writing `html\`\`` templates that render correctly in the browser and
9
- inside an Obsidian plugin. Several of these are hard runtime errors, not style.
10
-
11
- ## Reactive vs static — the core rule
12
-
13
- ```ts
14
- import { html, reactive } from "@arrow-js/core"
15
- const data = reactive({ count: 0 })
16
-
17
- html`<span>${data.count}</span>` // ❌ renders ONCE — never updates
18
- html`<span>${() => data.count}</span>` // ✅ tracked — updates only this slot
19
- ```
20
-
21
- `${value}` is read once at mount. `${() => value}` is tracked: Arrow records the
22
- reactive reads inside the function and re-runs **only that slot** when they
23
- change. Forgetting `() =>` is the #1 "why isn't it updating" bug.
24
-
25
- ## Attributes
26
-
27
- An attribute expression must be the **entire** attribute value:
28
-
29
- ```ts
30
- html`<div class="${() => (active() ? "tab is-active" : "tab")}">` // ✅
31
- html`<div class="tab ${() => active() ? "is-active" : ""}">` // ❌ THROWS
32
- ```
33
-
34
- Partial values (`"static ${…}"`) are not registered as placeholders and throw
35
- `Invalid HTML position`. Build the full string in one expression.
36
-
37
- Returning `false` from an attribute expression **removes** the attribute (vs `""`
38
- which keeps it present-but-empty) — the clean way to toggle `disabled`/`hidden`:
39
-
40
- ```ts
41
- html`<button disabled="${() => !canSubmit()}">Save</button>`
42
- ```
43
-
44
- ## Properties, events, lists
45
-
46
- ```ts
47
- html`<input .value="${() => data.text}" />` // .prop = IDL property
48
- html`<button @click="${() => data.count++}">+</button>` // @event
49
- html`${() => data.items.map((i) => html`<li>${i.text}</li>`.key(i.id))}` // keyed list
50
- ```
51
-
52
- Keyed lists preserve DOM across reorders; mutate item fields in place for
53
- fine-grained updates instead of replacing the whole array.
54
-
55
- ## Async sections
56
-
57
- ```ts
58
- import { boundary } from "@arrow-js/framework"
59
- const Card = component(
60
- async () => { const d = await load(); return html`<div>${d.label}</div>` },
61
- { fallback: html`<div>Loading…</div>` } // shown while pending
62
- )
63
- html`${boundary(Card())}`
64
- ```
65
-
66
- Works client-side with no SSR. `boundary()` only takes `{ idPrefix }`; the
67
- visible loading state comes from the async component's `fallback` option.
68
-
69
- ## Event handler typing
70
-
71
- An `@event` handler must be assignable to `(e: Event) => void`. A handler typed
72
- with a narrowed subtype fails (parameter contravariance) — `tsc` reports
73
- `TS2345 … not assignable to 'ArrowExpression'`. Type the param `Event` and
74
- narrow inside; no-arg handlers are always fine.
75
-
76
- ```ts
77
- // ❌ TS2345 — narrowed param
78
- html`<div @mousedown="${(e: MouseEvent) => resize(e)}">`
79
- // ✅ widen, narrow inside
80
- html`<div @mousedown="${(e: Event) => resize(e as MouseEvent)}">`
81
- ```
82
-
83
- (document.addEventListener handlers are unaffected — they're DOM-lib typed.)
84
-
85
- ## Hard footguns
86
-
87
- Render-time (pass `tsc`, fail only at render — always verify in a browser):
88
-
89
- 1. **No literal HTML comments inside templates.** Arrow uses HTML comments as
90
- expression-slot markers, so a literal `<!-- … -->` inflates the slot count and
91
- throws `Invalid HTML position`. Use JS `//` comments outside the template.
92
- 2. **No partial attribute values** — the expression must be the whole value
93
- (see Attributes above), else `Invalid HTML position`.
94
-
95
- Type-time (caught by `tsc`):
96
-
97
- 3. **No narrowed `Event` subtype in `@event` handlers** (see Event handler
98
- typing above).
99
-
100
- CI guards all three: `test/template-footguns.test.mjs` scans for `<!--` and for
101
- inline handlers typed with a narrowed Event subtype; `tsc` covers the rest.
@@ -1,69 +0,0 @@
1
- ---
2
- name: obsidian-arrow-maintenance
3
- description: Use when updating or maintaining an EXISTING scaffolded obsidian-arrow project — refresh the managed tooling with create-obsidian-arrow update, update installed agent skills with pnpm skills:update, fix skills scoping when the project is nested inside another repo (--project-dir / --global), and re-pull Obsidian styling. Only tooling/skills/docs are refreshed; your src is preserved.
4
- ---
5
-
6
- # Maintaining an existing Obsidian Arrow project
7
-
8
- How to bring an already-scaffolded project up to date. The scaffolder is
9
- create-only (it refuses a non-empty dir), so updates split into three tracks:
10
- **tooling files**, **agent skills**, and **styling**. None of these touch your
11
- `src/`.
12
-
13
- ## 1. Refresh the tooling (scripts, skills files, docs, CI, config)
14
-
15
- ```sh
16
- npx create-obsidian-arrow update # in the project root (or: update <dir>)
17
- npx create-obsidian-arrow update --dry-run # preview what would change first
18
- ```
19
-
20
- Refreshes the **managed** files from the latest template — `scripts/`, `skills/`,
21
- `docs/`, `.github/`, `.husky/`, `biome.json`, `AGENTS.md`, `CLAUDE.md` — and
22
- **merges** `package.json` scripts + any missing deps. It **never** touches `src/`,
23
- `public/`, `index.html`, `vite.config.ts`, `tsconfig.json`, or `.gitignore`. After
24
- it runs: `pnpm install && pnpm check`.
25
-
26
- ## 2. Update the installed agent skills
27
-
28
- ```sh
29
- pnpm skills:update # = npx skills update -y (updates installed skills in place)
30
- ```
31
-
32
- Or reinstall the latest straight from the published repo (works from anywhere,
33
- even if the project predates the skill scripts):
34
-
35
- ```sh
36
- npx skills add kylebrodeur/obsidian-arrow-sandbox --all --yes
37
- ```
38
-
39
- ## 3. Nested inside another repo? Fix skills scoping
40
-
41
- The `skills` CLI installs project-scope **relative to cwd**. If this project sits
42
- inside a larger repo and your agent runs from the **outer** repo, skills installed
43
- here won't be found. Install them where the agent looks:
44
-
45
- ```sh
46
- pnpm skills:install --yes --project-dir=<outer-repo> # project-scoped at the outer root
47
- pnpm skills:install --yes --global # or user-level (all projects)
48
- ```
49
-
50
- (`SKILLS_PROJECT_DIR` / `SKILLS_GLOBAL` env forms drive the auto `postinstall`
51
- step, which takes no CLI args.)
52
-
53
- ## 4. Re-pull Obsidian styling after Obsidian updates
54
-
55
- `public/app.css` is a local snapshot (git-ignored). If Obsidian updated or the
56
- sandbox renders stale, refresh it:
57
-
58
- ```sh
59
- pnpm pull-css
60
- ```
61
-
62
- ## 5. Re-check porting parity
63
-
64
- After updating, re-run the porting-parity check so plugin copies still match the
65
- sandbox source (see the **arrow-js-obsidian-porting** skill):
66
-
67
- ```sh
68
- node scripts/component-hash.mjs --check port-parity.json
69
- ```
@@ -1,77 +0,0 @@
1
- ---
2
- name: obsidian-arrow-sandbox
3
- description: Use when prototyping or building Arrow.js UI for an Obsidian plugin in the obsidian-arrow-sandbox project — covers running the sandbox, pulling Obsidian's real app.css from the local install, the dev/verify workflow, CSS scoping, and porting a finished component into a plugin's ItemView with near-zero refactoring.
4
- ---
5
-
6
- # Obsidian Arrow Sandbox
7
-
8
- A client-only Vite + TypeScript environment for building [Arrow.js](https://arrow-js.com/)
9
- UI that drops into an Obsidian plugin. Components render against Obsidian's real
10
- `app.css`, so what you see in the browser is what you get in a plugin view.
11
-
12
- ## Mental model
13
-
14
- - **Client-only, no SSR.** Components use `@arrow-js/core` (`reactive`, `html`,
15
- `component`, `watch`) plus `@arrow-js/framework` (`boundary` for async
16
- sections), mounted via `template(container)` — the exact call an Obsidian
17
- `ItemView.onOpen()` makes. Do **not** add `@arrow-js/ssr` or `@arrow-js/hydrate`;
18
- an Obsidian plugin has no server and nothing to hydrate.
19
- - **Styling is Obsidian's, not yours.** `index.html` puts Obsidian body classes
20
- (`theme-dark mod-macos …`) on `<body>` and loads the extracted `app.css`, which
21
- defines every `var(--…)` token and semantic class (`.setting-item`,
22
- `.clickable-icon`, `.vertical-tab-*`).
23
-
24
- ## Run it
25
-
26
- ```sh
27
- pnpm install
28
- pnpm pull-css # extract Obsidian's app.css (macOS auto-detect)
29
- pnpm dev # Vite + HMR; open the printed URL
30
- ```
31
-
32
- `pnpm pull-css` reads `app.css` out of `obsidian.asar`. macOS is auto-detected;
33
- elsewhere pass `--path <obsidian.asar|app.css>` or set `OBSIDIAN_ASAR=<path>`.
34
- `public/app.css` is **git-ignored** (Obsidian's proprietary CSS — not
35
- redistributed), so run `pnpm pull-css` once before `pnpm dev`.
36
-
37
- ## Install the bundled skills
38
-
39
- ```sh
40
- pnpm skills:install --yes # non-interactive: install ALL bundled skills (agents/CI)
41
- pnpm skills:install # interactive picker on a terminal
42
- pnpm skills:update # update an already-installed setup
43
- ```
44
-
45
- Scope flags: `--agent <name>`, `--project-dir=<path>` (install into another repo
46
- root — use this when the project is **nested** inside a larger repo), `--global`.
47
- To update an existing project's tooling later, see the **obsidian-arrow-maintenance**
48
- skill (`npx create-obsidian-arrow update`).
49
-
50
- ## Build a component
51
-
52
- Add `src/components/MyThing.ts` exporting an Arrow `component()`, then mount it
53
- from `src/main.ts`. Use Obsidian classes + `var(--…)` tokens first; add custom
54
- CSS only when there's no Obsidian class, and scope it under a container class +
55
- element type (e.g. `.oas-frame button.oas-x`) so it beats Obsidian's global
56
- `button:not(.clickable-icon)` rule. Sandbox-only chrome lives in
57
- `src/sandbox/sandbox.css`.
58
-
59
- For the template-writing rules and Arrow's hard footguns, use the companion
60
- skill **arrow-js-obsidian-templates**.
61
-
62
- ## Verify before claiming done
63
-
64
- ```sh
65
- pnpm typecheck && pnpm test && pnpm lint # or: pnpm check
66
- ```
67
-
68
- Then open the `pnpm dev` URL and confirm the console is clean and the component
69
- looks like a real Obsidian pane. Arrow's footguns only surface at render, so
70
- typecheck passing is **not** proof a component works.
71
-
72
- ## Port into the plugin
73
-
74
- Copy the component file into the plugin's view directory and mount it from
75
- `ItemView.onOpen()` via `template(this.contentEl)`. If it uses `boundary()` /
76
- async components, add `@arrow-js/framework` to the plugin and the side-effect
77
- `import '@arrow-js/framework'`. Leave sandbox chrome (`src/sandbox/*`) behind.