create-obsidian-arrow 0.1.5 → 0.1.8

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,44 @@ 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
- Local dev of the initializer itself (from the sandbox repo, before publishing):
27
+ ### Install the bundled skills
28
+
29
+ The scaffold ships agent skills and installs them via the [`skills`](https://github.com/vercel-labs/skills)
30
+ CLI:
31
+
32
+ ```sh
33
+ pnpm skills:install --yes # non-interactive — installs all
34
+ pnpm skills:install # interactive picker
35
+ pnpm skills:install --yes --project-dir=<root> # install at another repo root (nesting)
36
+ pnpm skills:update # update an existing setup
37
+ ```
38
+
39
+ If you scaffold **inside an existing repo**, skills install scoped to the
40
+ scaffold folder (the CLI is cwd-relative); use `--project-dir=<outer-repo>` (or
41
+ `--global`) to install where an agent at the outer repo looks. The scaffolder
42
+ prints this hint when it detects nesting.
43
+
44
+ ## Update an existing project
45
+
46
+ The scaffolder is create-only, but `update` refreshes an existing project's
47
+ **managed** tooling (`scripts/`, `skills/`, `docs/`, `.github/`, `.husky/`,
48
+ `biome.json`, agent guides) and merges new `package.json` scripts/deps — it never
49
+ touches your `src/`, `public/`, `index.html`, or build configs:
50
+
51
+ ```sh
52
+ npx create-obsidian-arrow update # in the project (or: update <dir>)
53
+ npx create-obsidian-arrow update --dry-run # preview
54
+ ```
55
+
56
+ Then `pnpm install && pnpm check`.
57
+
58
+ ## Local dev of the initializer
59
+
60
+ From the sandbox repo, before publishing:
28
61
 
29
62
  ```sh
30
- node create-obsidian-arrow/index.mjs ../my-app
63
+ node create-obsidian-arrow/index.mjs ../my-app # scaffold
64
+ node create-obsidian-arrow/index.mjs update ../my-app # update
31
65
  ```
32
66
 
33
67
  ## What you get
package/index.mjs CHANGED
@@ -1,13 +1,21 @@
1
1
  #!/usr/bin/env node
2
2
  /**
3
- * create-obsidian-arrow — scaffold a new Obsidian-styled Arrow.js UI sandbox.
3
+ * create-obsidian-arrow — scaffold or update an Obsidian-styled Arrow.js sandbox.
4
4
  *
5
- * pnpm create obsidian-arrow my-app # once published
6
- * node create-obsidian-arrow/index.mjs my-app # locally, before publishing
5
+ * create-obsidian-arrow <dir> scaffold a new project into <dir>
6
+ * create-obsidian-arrow update [dir] refresh an existing project's tooling
7
+ * (default dir: cwd) — preserves your code
7
8
  *
8
- * Copies the vendored template/ into <dir>, restores .gitignore (npm strips it
9
- * from packages, so it's vendored as _gitignore), rewrites the project name,
10
- * and runs `git init`. The template is a full, verified sandbox — see template/.
9
+ * pnpm create obsidian-arrow my-app
10
+ * node create-obsidian-arrow/index.mjs my-app # locally
11
+ * node create-obsidian-arrow/index.mjs update ./my-app
12
+ *
13
+ * Scaffold copies the vendored template/, restores .gitignore (vendored as
14
+ * _gitignore), names the project, and runs `git init`.
15
+ *
16
+ * Update refreshes only the *managed* tooling files from the template and merges
17
+ * package.json scripts + missing deps — it never touches your src/, public/,
18
+ * index.html, or the core build configs. Use --dry-run to preview.
11
19
  */
12
20
  import { spawnSync } from "node:child_process";
13
21
  import fs from "node:fs";
@@ -17,60 +25,172 @@ import { fileURLToPath } from "node:url";
17
25
  const here = path.dirname(fileURLToPath(import.meta.url));
18
26
  const templateDir = path.join(here, "template");
19
27
 
28
+ // Files/dirs the template owns and `update` may overwrite/merge. Everything else
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
+ ];
41
+
42
+ const argv = process.argv.slice(2);
43
+ const dryRun = argv.includes("--dry-run");
44
+
20
45
  function fail(message) {
21
46
  console.error(`create-obsidian-arrow: ${message}`);
22
47
  process.exit(1);
23
48
  }
24
49
 
25
- const targetArg = process.argv[2];
26
- if (!targetArg) {
27
- fail("usage: create-obsidian-arrow <directory>");
50
+ if (!fs.existsSync(templateDir)) {
51
+ fail("template/ is missing — run scripts/sync-template.mjs to build it.");
28
52
  }
29
53
 
30
- const destRoot = path.resolve(process.cwd(), targetArg);
31
- const appName = path.basename(destRoot);
54
+ /** Nearest ancestor *above* `dir` that is a git repo, or null. */
55
+ function outerRepoAbove(dir) {
56
+ let current = path.dirname(path.resolve(dir));
57
+ const fsRoot = path.parse(current).root;
58
+ while (current && current !== fsRoot) {
59
+ if (fs.existsSync(path.join(current, ".git"))) {
60
+ return current;
61
+ }
62
+ current = path.dirname(current);
63
+ }
64
+ return null;
65
+ }
32
66
 
33
- if (fs.existsSync(destRoot) && fs.readdirSync(destRoot).length > 0) {
34
- fail(`target "${targetArg}" already exists and is not empty.`);
67
+ /** Recursively copy src→dest; returns the list of written file paths (relative
68
+ * to `base`). Honors --dry-run (records but doesn't write). */
69
+ function copyTree(src, dest, base, written) {
70
+ const stat = fs.statSync(src);
71
+ if (stat.isDirectory()) {
72
+ if (!dryRun) {
73
+ fs.mkdirSync(dest, { recursive: true });
74
+ }
75
+ for (const entry of fs.readdirSync(src)) {
76
+ copyTree(path.join(src, entry), path.join(dest, entry), base, written);
77
+ }
78
+ return;
79
+ }
80
+ written.push(path.relative(base, dest));
81
+ if (!dryRun) {
82
+ fs.mkdirSync(path.dirname(dest), { recursive: true });
83
+ fs.copyFileSync(src, dest);
84
+ }
35
85
  }
36
- if (!fs.existsSync(templateDir)) {
37
- fail("template/ is missing — run scripts/sync-template.mjs to build it.");
86
+
87
+ function scaffold(targetArg) {
88
+ const destRoot = path.resolve(process.cwd(), targetArg);
89
+ const appName = path.basename(destRoot);
90
+
91
+ if (fs.existsSync(destRoot) && fs.readdirSync(destRoot).length > 0) {
92
+ fail(`target "${targetArg}" already exists and is not empty (use \`update\` to refresh it).`);
93
+ }
94
+
95
+ const written = [];
96
+ copyTree(templateDir, destRoot, destRoot, written);
97
+ // Restore .gitignore (npm omits .gitignore from packages; vendored as _gitignore).
98
+ const vendoredIgnore = path.join(destRoot, "_gitignore");
99
+ if (fs.existsSync(vendoredIgnore)) {
100
+ fs.renameSync(vendoredIgnore, path.join(destRoot, ".gitignore"));
101
+ }
102
+
103
+ // Personalize the project name via targeted replace (keeps Biome formatting).
104
+ const pkgPath = path.join(destRoot, "package.json");
105
+ const renamed = fs
106
+ .readFileSync(pkgPath, "utf8")
107
+ .replace(/("name":\s*)"[^"]*"/, (_m, prefix) => `${prefix}${JSON.stringify(appName)}`);
108
+ fs.writeFileSync(pkgPath, renamed);
109
+
110
+ spawnSync("git", ["init", "-q"], { cwd: destRoot, stdio: "ignore" });
111
+
112
+ console.log(`\nScaffolded ${appName} in ${path.relative(process.cwd(), destRoot) || "."}\n`);
113
+ console.log("Next steps:");
114
+ console.log(` cd ${targetArg}`);
115
+ console.log(" pnpm install");
116
+ console.log(" pnpm pull-css # extract Obsidian's app.css (macOS auto-detect)");
117
+ console.log(" pnpm dev\n");
118
+
119
+ const outer = outerRepoAbove(destRoot);
120
+ if (outer) {
121
+ console.log(`Note: this project is nested inside the repo at ${outer}.`);
122
+ console.log(" Bundled skills install scoped to THIS project. To install them at the");
123
+ console.log(` outer repo instead: pnpm skills:install --yes --project-dir=${outer}\n`);
124
+ }
125
+ }
126
+
127
+ /** Merge template package.json scripts + missing deps into the target's,
128
+ * preserving name/version/identity and existing dep versions. */
129
+ function mergePackageJson(targetPkgPath) {
130
+ const tpl = JSON.parse(fs.readFileSync(path.join(templateDir, "package.json"), "utf8"));
131
+ const pkg = JSON.parse(fs.readFileSync(targetPkgPath, "utf8"));
132
+ const changes = [];
133
+
134
+ pkg.scripts ??= {};
135
+ for (const [name, cmd] of Object.entries(tpl.scripts ?? {})) {
136
+ if (pkg.scripts[name] !== cmd) {
137
+ changes.push(`script ${name}`);
138
+ pkg.scripts[name] = cmd;
139
+ }
140
+ }
141
+ for (const field of ["dependencies", "devDependencies"]) {
142
+ pkg[field] ??= {};
143
+ for (const [name, version] of Object.entries(tpl[field] ?? {})) {
144
+ if (!(name in pkg[field])) {
145
+ changes.push(`${field}: ${name}`);
146
+ pkg[field][name] = version;
147
+ }
148
+ }
149
+ }
150
+
151
+ if (changes.length > 0 && !dryRun) {
152
+ fs.writeFileSync(targetPkgPath, `${JSON.stringify(pkg, null, "\t")}\n`);
153
+ }
154
+ return changes;
38
155
  }
39
156
 
40
- function copyDir(src, dest) {
41
- fs.mkdirSync(dest, { recursive: true });
42
- for (const entry of fs.readdirSync(src, { withFileTypes: true })) {
43
- const srcPath = path.join(src, entry.name);
44
- // Vendored as _gitignore because npm omits .gitignore from published tarballs.
45
- const destName = entry.name === "_gitignore" ? ".gitignore" : entry.name;
46
- const destPath = path.join(dest, destName);
47
- if (entry.isDirectory()) {
48
- copyDir(srcPath, destPath);
49
- } else {
50
- fs.copyFileSync(srcPath, destPath);
157
+ function update(targetArg) {
158
+ const root = path.resolve(process.cwd(), targetArg ?? ".");
159
+ if (!fs.existsSync(path.join(root, "package.json"))) {
160
+ fail(`no package.json in ${root} — is this a scaffolded project?`);
161
+ }
162
+
163
+ const written = [];
164
+ for (const name of MANAGED) {
165
+ const src = path.join(templateDir, name);
166
+ if (fs.existsSync(src)) {
167
+ copyTree(src, path.join(root, name), root, written);
51
168
  }
52
169
  }
170
+ const pkgChanges = mergePackageJson(path.join(root, "package.json"));
171
+
172
+ const verb = dryRun ? "Would refresh" : "Refreshed";
173
+ console.log(
174
+ `${verb} ${written.length} managed file(s) in ${path.relative(process.cwd(), root) || "."}:`
175
+ );
176
+ for (const file of written) {
177
+ console.log(` ${file}`);
178
+ }
179
+ if (pkgChanges.length > 0) {
180
+ console.log(`package.json: ${dryRun ? "would update" : "updated"} ${pkgChanges.join(", ")}`);
181
+ }
182
+ console.log(
183
+ dryRun
184
+ ? "\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"
186
+ );
53
187
  }
54
188
 
55
- copyDir(templateDir, destRoot);
56
-
57
- // Personalize the project name. Use a targeted replace (not JSON.parse +
58
- // stringify) so the template's existing Biome formatting stays byte-identical
59
- // and the fresh project passes `pnpm lint` out of the box.
60
- const pkgPath = path.join(destRoot, "package.json");
61
- const pkgText = fs.readFileSync(pkgPath, "utf8");
62
- const renamed = pkgText.replace(
63
- /("name":\s*)"[^"]*"/,
64
- (_match, prefix) => `${prefix}${JSON.stringify(appName)}`
65
- );
66
- fs.writeFileSync(pkgPath, renamed);
67
-
68
- // Initialize a git repo (best-effort; ignore if git is unavailable).
69
- spawnSync("git", ["init", "-q"], { cwd: destRoot, stdio: "ignore" });
70
-
71
- console.log(`\nScaffolded ${appName} in ${path.relative(process.cwd(), destRoot) || "."}\n`);
72
- console.log("Next steps:");
73
- console.log(` cd ${targetArg}`);
74
- console.log(" pnpm install");
75
- console.log(" pnpm pull-css # extract Obsidian's app.css (macOS auto-detect)");
76
- console.log(" pnpm dev\n");
189
+ const command = argv[0];
190
+ if (command === "update") {
191
+ update(argv.find((a) => a !== "update" && !a.startsWith("--")));
192
+ } else if (!command || command.startsWith("--")) {
193
+ fail("usage: create-obsidian-arrow <directory> | update [directory]");
194
+ } else {
195
+ scaffold(command);
196
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-obsidian-arrow",
3
- "version": "0.1.5",
3
+ "version": "0.1.8",
4
4
  "description": "Scaffold an Obsidian-styled Arrow.js UI sandbox (pnpm create obsidian-arrow <dir>).",
5
5
  "type": "module",
6
6
  "bin": {
@@ -11,7 +11,8 @@ This file is the hub — everything else is linked from here:
11
11
  - [`docs/workflow.md`](docs/workflow.md) — fresh-machine → running workflow.
12
12
  - [`skills/`](skills/) — installable domain skills (`pnpm skills:install`):
13
13
  obsidian-arrow-sandbox, arrow-js-obsidian-templates, arrow-js-obsidian-patterns,
14
- arrow-js-obsidian-porting (sandbox→plugin parity check).
14
+ arrow-js-obsidian-porting (sandbox→plugin parity check), obsidian-arrow-maintenance
15
+ (updating an existing project).
15
16
  - [`docs/prompts/agent-setup.md`](docs/prompts/agent-setup.md) — prompt for
16
17
  briefing a fresh agent (scaffold + orient).
17
18
 
@@ -40,6 +41,14 @@ pnpm dev # Vite + HMR
40
41
  `public/app.css` is **git-ignored** (Obsidian's proprietary CSS — not
41
42
  redistributed); run `pnpm pull-css` once before `pnpm dev`.
42
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
46
+ flags: `--agent <name>`, `--project-dir=<path>`, `--global`. **Nested inside
47
+ another repo?** Skills install cwd-relative — use `--project-dir=<outer-repo>` so
48
+ they land where an agent at the outer repo looks. To refresh an existing
49
+ project's tooling, run `npx create-obsidian-arrow update` (see the
50
+ obsidian-arrow-maintenance skill).
51
+
43
52
  ## Arrow v1.0.6 footguns — READ BEFORE WRITING TEMPLATES
44
53
 
45
54
  These are hard runtime errors, not style nits. They are encoded in CI
@@ -24,6 +24,21 @@ Then `cd my-app && pnpm install && pnpm pull-css && pnpm dev`. A freshly
24
24
  scaffolded project passes `pnpm run ci` out of the box. The initializer's
25
25
  template is generated from this repo (`pnpm create:sync`), so it never drifts.
26
26
 
27
+ **Update an existing project's tooling** (refreshes the managed files — scripts,
28
+ skills, docs, agent guides, CI, `biome.json` — and merges new `package.json`
29
+ scripts/deps; never touches `src/`, `public/`, `index.html`, or build configs):
30
+
31
+ ```sh
32
+ npx create-obsidian-arrow update # in the project (or: update <dir>)
33
+ npx create-obsidian-arrow update --dry-run # preview first
34
+ ```
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.
41
+
27
42
  > This repo (the full sandbox) is **not** published to npm — only the
28
43
  > `create-obsidian-arrow/` initializer is. An agent-onboarding prompt lives in
29
44
  > [`docs/prompts/agent-setup.md`](docs/prompts/agent-setup.md).
@@ -78,23 +93,28 @@ under [`skills/`](skills/) — it doubles as a local skill marketplace:
78
93
  - `arrow-js-obsidian-porting` — content-addressed porting parity: the
79
94
  `component-hash` tool + a husky/CI check that the plugin copy hasn't drifted
80
95
  from the sandbox source.
96
+ - `obsidian-arrow-maintenance` — updating an existing project: `create-obsidian-arrow
97
+ update`, `skills:update`, nesting/`--project-dir`, re-pull styling.
81
98
 
82
99
  Install them into your agent:
83
100
 
84
101
  ```sh
85
- pnpm skills:install # interactive picker (TUI) on a terminal
86
- pnpm skills:install --yes # non-interactive — installs ALL bundled skills
87
- pnpm skills:install --yes --agent claude-code # install for one agent only
88
- pnpm skills:update # update an existing setup to the latest
102
+ pnpm skills:install # interactive picker (TUI) on a terminal
103
+ pnpm skills:install --yes # non-interactive — installs ALL bundled skills
104
+ pnpm skills:install --yes --agent claude-code # install for one agent only
105
+ pnpm skills:install --yes --project-dir=<repo-root> # install into another project root
106
+ pnpm skills:update # update an existing setup to the latest
89
107
  ```
90
108
 
91
109
  `postinstall` offers the picker automatically after `pnpm install`, but only in
92
110
  an interactive terminal — in CI / non-TTY it just prints how to install (never
93
- hangs). For agents/CI, use `--yes` (it runs `npx skills add . --all --yes`);
94
- `--agent <name>` (or `--agent=<name>`) targets one agent instead of all.
95
- `pnpm skills:update` runs `npx skills update -y`. The auto `postinstall` step
96
- takes no CLI args, so use `SKILLS_AGENT=<name>` (and `SKIP_SKILLS_INSTALL=1` to
97
- opt out) to influence *that* path.
111
+ hangs). For agents/CI, use `--yes` (it runs `npx skills add . --all --yes`).
112
+ Scope flags: `--agent <name>` (one agent), `--project-dir=<path>` (install into a
113
+ different project root, e.g. an outer repo a scaffold is nested in), `--global`
114
+ (user-level, available everywhere). `pnpm skills:update` runs
115
+ `npx skills update -y`. The auto `postinstall` step takes no CLI args, so the env
116
+ forms `SKILLS_AGENT` / `SKILLS_PROJECT_DIR` / `SKILLS_GLOBAL` (and
117
+ `SKIP_SKILLS_INSTALL=1`) influence *that* path.
98
118
 
99
119
  ## Porting a component into the plugin
100
120
 
@@ -3,6 +3,10 @@ dist
3
3
  .DS_Store
4
4
  *.tgz
5
5
 
6
+ # `skills` install artifacts (regenerated by `pnpm skills:install`); the source
7
+ # of truth is skills/. (skills-lock.json is intentionally kept tracked.)
8
+ .agents/
9
+
6
10
  # Obsidian's app.css is proprietary — extract it locally with `pnpm pull-css`,
7
11
  # don't commit/redistribute it.
8
12
  public/app.css
@@ -35,13 +35,18 @@ Then:
35
35
  pnpm skills:install --yes # install ALL bundled agent skills non-interactively
36
36
  # (runs `npx skills add . --all --yes`) — this loads
37
37
  # the domain knowledge. Drop --yes for an interactive picker.
38
+ # NESTED inside another repo? Skills install cwd-relative,
39
+ # so add --project-dir=<outer-repo> (or --global) to put them
40
+ # where an agent at the outer repo will find them.
38
41
 
39
42
  READ FIRST
40
43
  - AGENTS.md (root) — operating guide + docs map (links everything below).
41
44
  - docs/workflow.md — fresh-machine → running workflow.
42
45
  - skills/*/SKILL.md — obsidian-arrow-sandbox (workflow), arrow-js-obsidian-
43
46
  templates (template syntax + footguns), arrow-js-obsidian-patterns (icons via
44
- Lucide/data-icon sweep, CSS scoping, mount/unmount lifecycle, reactive state).
47
+ Lucide/data-icon sweep, CSS scoping, mount/unmount lifecycle, reactive state),
48
+ arrow-js-obsidian-porting (sandbox→plugin parity check),
49
+ obsidian-arrow-maintenance (updating an existing project).
45
50
 
46
51
  ARROW v1.0.6 FOOTGUNS — do not relearn these the hard way:
47
52
  1. NO literal HTML comments inside html`` templates — Arrow treats HTML comments
@@ -76,7 +81,12 @@ PORTING TO A PLUGIN
76
81
  Copy the component file into the plugin's view dir and mount from
77
82
  ItemView.onOpen() via `template(this.contentEl)`. If it uses boundary()/async
78
83
  components, add @arrow-js/framework to the plugin and the side-effect
79
- `import '@arrow-js/framework'`. Leave src/sandbox/* behind.
84
+ `import '@arrow-js/framework'`. Leave src/sandbox/* behind. Guard against drift
85
+ with the porting-parity check (see the arrow-js-obsidian-porting skill).
86
+
87
+ MAINTENANCE (existing project)
88
+ Refresh tooling later with `npx create-obsidian-arrow update` (preserves src/),
89
+ update skills with `pnpm skills:update`. See the obsidian-arrow-maintenance skill.
80
90
 
81
91
  Start by scaffolding, running setup steps, then read AGENTS.md and confirm
82
92
  `pnpm dev` renders /example correctly. Report what you see.
@@ -44,6 +44,13 @@ pnpm skills:update # update an already-installed setup to the latest
44
44
  Then point the agent at [`AGENTS.md`](../AGENTS.md), or brief a fresh agent with
45
45
  [`docs/prompts/agent-setup.md`](prompts/agent-setup.md).
46
46
 
47
+ **Nested inside another repo?** Skills install scoped to the current folder (the
48
+ `skills` CLI is cwd-relative). To install them at the outer repo instead:
49
+ `pnpm skills:install --yes --project-dir=<outer-repo>` (or `--global`).
50
+
51
+ **Refresh an existing project's tooling** (scripts, skills, docs, CI — never your
52
+ `src/`): `npx create-obsidian-arrow update` (add `--dry-run` to preview).
53
+
47
54
  ## Build → verify → port loop
48
55
 
49
56
  ```sh
@@ -21,12 +21,18 @@
21
21
  * --yes / -y non-interactive install of all bundled skills
22
22
  * --agent <name> | --agent=<name> install for one agent (e.g. claude-code)
23
23
  * instead of all detected agents
24
- * SKILLS_AGENT=<name> same as --agent, for the auto `postinstall` step
25
- * (which can't take CLI args) and CI
24
+ * --project-dir=<path> install into another project root (e.g. the outer repo
25
+ * 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
28
+ * SKILLS_AGENT / SKILLS_PROJECT_DIR / SKILLS_GLOBAL env forms (for the auto
29
+ * `postinstall` step, which can't take CLI args, and CI)
26
30
  * --dry-run / SKILLS_DRY_RUN=1 print the command instead of running it
27
31
  * SKIP_SKILLS_INSTALL=1 opt out of the postinstall auto-step
28
32
  */
29
33
  import { spawnSync } from "node:child_process";
34
+ import fs from "node:fs";
35
+ import path from "node:path";
30
36
  import process from "node:process";
31
37
 
32
38
  const BUNDLED =
@@ -43,23 +49,53 @@ const flagValue = (flag) => {
43
49
  return i >= 0 ? process.argv[i + 1] : undefined;
44
50
  };
45
51
 
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). */
55
+ function outerRepoAbove(dir) {
56
+ let current = path.dirname(path.resolve(dir));
57
+ const fsRoot = path.parse(current).root;
58
+ while (current && current !== fsRoot) {
59
+ if (fs.existsSync(path.join(current, ".git"))) {
60
+ return current;
61
+ }
62
+ current = path.dirname(current);
63
+ }
64
+ return null;
65
+ }
66
+
46
67
  const forced = has("--force"); // set by `pnpm skills:install`
47
68
  const update = has("--update") || has("-u");
48
69
  const yes = has("--yes") || has("-y");
70
+ const global = has("--global") || has("-g") || process.env.SKILLS_GLOBAL === "1";
49
71
  const agent = flagValue("--agent") || process.env.SKILLS_AGENT;
72
+ const projectDir = flagValue("--project-dir") || process.env.SKILLS_PROJECT_DIR;
50
73
  const dryRun = has("--dry-run") || process.env.SKILLS_DRY_RUN === "1";
51
74
  const isCI = Boolean(process.env.CI);
52
75
  const interactive = Boolean(process.stdout.isTTY && process.stdin.isTTY);
53
76
  const optedOut = process.env.SKIP_SKILLS_INSTALL === "1";
54
77
 
55
- function run(args) {
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(".") : ".";
82
+ const targetCwd = projectDir ? path.resolve(projectDir) : process.cwd();
83
+
84
+ if (projectDir && !fs.existsSync(targetCwd)) {
85
+ console.error(`[skills] --project-dir not found: ${targetCwd}`);
86
+ process.exit(1);
87
+ }
88
+
89
+ function run(args, cwd = process.cwd()) {
90
+ const where = cwd === process.cwd() ? "" : ` (in ${cwd})`;
56
91
  const pretty = ["npx", "skills", ...args].join(" ");
57
92
  if (dryRun) {
58
- console.log(`[skills] (dry-run) would run: ${pretty}`);
93
+ console.log(`[skills] (dry-run) would run: ${pretty}${where}`);
59
94
  process.exit(0);
60
95
  }
61
- console.log(`[skills] ${pretty}`);
96
+ console.log(`[skills] ${pretty}${where}`);
62
97
  const result = spawnSync("npx", ["--yes", "skills", ...args], {
98
+ cwd,
63
99
  stdio: "inherit",
64
100
  shell: process.platform === "win32",
65
101
  });
@@ -92,16 +128,26 @@ if (!forced && (isCI || !interactive)) {
92
128
  process.exit(0);
93
129
  }
94
130
 
95
- // Non-interactive (CI/agent/no TTY, or --yes, or an agent target): install ALL
96
- // bundled skills with no prompts. Target one agent if asked, else all agents.
97
- if (!interactive || yes || agent) {
131
+ // 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.
133
+ if (!interactive || yes || agent || global || projectDir) {
98
134
  console.log(`[skills] Installing all bundled skills non-interactively: ${BUNDLED}`);
99
- if (agent) {
100
- // `--all` is shorthand for `-s * -a * -y`, so to target one agent we
101
- // spell out all-skills + that agent explicitly.
102
- run(["add", ".", "-s", "*", "-a", agent, "-y"]);
135
+ const outer = global || projectDir ? null : outerRepoAbove(process.cwd());
136
+ if (outer) {
137
+ 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).`
139
+ );
140
+ }
141
+ // `--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"];
147
+ if (global) {
148
+ args.push("--global");
103
149
  }
104
- run(["add", ".", "--all"]);
150
+ run(args, targetCwd);
105
151
  }
106
152
 
107
153
  // Interactive terminal: let the user pick in the TUI.
@@ -0,0 +1,69 @@
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
+ ```
@@ -34,6 +34,19 @@ elsewhere pass `--path <obsidian.asar|app.css>` or set `OBSIDIAN_ASAR=<path>`.
34
34
  `public/app.css` is **git-ignored** (Obsidian's proprietary CSS — not
35
35
  redistributed), so run `pnpm pull-css` once before `pnpm dev`.
36
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
+
37
50
  ## Build a component
38
51
 
39
52
  Add `src/components/MyThing.ts` exporting an Arrow `component()`, then mount it