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 +36 -2
- package/index.mjs +168 -48
- package/package.json +1 -1
- package/template/AGENTS.md +10 -1
- package/template/README.md +29 -9
- package/template/_gitignore +4 -0
- package/template/docs/prompts/agent-setup.md +12 -2
- package/template/docs/workflow.md +7 -0
- package/template/scripts/install-skills.mjs +59 -13
- package/template/skills/obsidian-arrow-maintenance/SKILL.md +69 -0
- package/template/skills/obsidian-arrow-sandbox/SKILL.md +13 -0
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
|
-
|
|
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
|
|
3
|
+
* create-obsidian-arrow — scaffold or update an Obsidian-styled Arrow.js sandbox.
|
|
4
4
|
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
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
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
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
|
-
|
|
26
|
-
|
|
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
|
-
|
|
31
|
-
|
|
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
|
-
|
|
34
|
-
|
|
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
|
-
|
|
37
|
-
|
|
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
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
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
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
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
package/template/AGENTS.md
CHANGED
|
@@ -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
|
package/template/README.md
CHANGED
|
@@ -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
|
|
86
|
-
pnpm skills:install --yes
|
|
87
|
-
pnpm skills:install --yes --agent claude-code
|
|
88
|
-
pnpm skills:
|
|
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>` (
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
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
|
|
package/template/_gitignore
CHANGED
|
@@ -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
|
-
*
|
|
25
|
-
*
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
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(
|
|
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
|