create-obsidian-arrow 0.5.1 → 0.5.2
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 +7 -7
- package/cli/create.mjs +65 -0
- package/cli/detect-pm.mjs +20 -0
- package/cli/lib.mjs +117 -0
- package/cli/refresh.mjs +65 -0
- package/index.mjs +47 -204
- package/package.json +11 -2
- package/template/.husky/pre-commit +3 -2
- package/template/AGENTS.md +57 -12
- package/template/README.md +66 -31
- package/template/_gitignore +4 -1
- package/template/biome.json +7 -1
- package/template/docs/prompts/agent-setup.md +22 -20
- package/template/docs/prompts/update-existing.md +3 -3
- package/template/docs/workflow.md +11 -7
- package/template/package.json +15 -14
- package/template/src/components/DiffViewer/DiffViewer.css +41 -0
- package/template/src/components/DiffViewer/DiffViewer.ts +55 -0
- package/template/src/components/EmptyState/EmptyState.css +5 -5
- package/template/src/components/EmptyState/EmptyState.ts +5 -9
- package/template/src/utilities.css +158 -0
- package/template/src/views/DiffViewer/DiffViewerView.css +42 -0
- package/template/src/views/DiffViewer/DiffViewerView.ts +53 -0
- package/template/src/views/ExampleView/ExampleView.ts +92 -0
- package/template/stories/components/ComponentShell.stories.ts +28 -0
- package/template/stories/components/EmptyState.stories.ts +1 -0
- package/template/stories/components/LoadingState.stories.ts +1 -0
- package/template/stories/components/Toggle.stories.ts +50 -0
- package/template/stories/views/DiffViewer/DiffViewer.stories.ts +94 -0
- package/template/stories/views/EditorView.stories.ts +55 -0
- package/template/stories/views/ExampleView/ExampleView.stories.ts +15 -0
- package/template/stories/views/PanelView.stories.ts +61 -0
- package/template/stories/views/SettingsPanel/SettingsPanel.stories.ts +14 -0
- package/template/test/css-structure.test.mjs +112 -0
- package/template/test/template-footguns.test.mjs +85 -6
- package/template/test/viewer-stories.test.mjs +12 -0
- package/template/tools/router/client.ts +26 -4
- package/template/tools/router/routeToPage.ts +29 -13
- package/template/tools/sandbox/frame.ts +7 -27
- package/template/tools/sandbox/home.ts +6 -11
- package/template/tools/sandbox/layout.ts +24 -2
- package/template/tools/sandbox/sandbox.css +188 -226
- package/template/tools/sandbox/shell.ts +2 -2
- package/template/tools/sandbox/toolbar.ts +20 -9
- package/template/tools/viewer/ClassesPage.ts +7 -7
- package/template/tools/viewer/ComponentsIndex.ts +3 -3
- package/template/tools/viewer/StoryPage.ts +53 -40
- package/template/tools/viewer/TokensPage.ts +10 -10
- package/template/tools/viewer/ViewsIndex.ts +66 -0
- package/template/tools/viewer/discovery.ts +2 -0
- package/template/tools/viewer/obsidian-classes.ts +1 -1
- package/template/tools/viewer/sidebar.ts +27 -38
- package/template/tools/viewer/stories.ts +16 -2
- package/template/.github/workflows/ci.yml +0 -36
- package/template/pnpm-lock.yaml +0 -1608
- package/template/scripts/create-component.mjs +0 -101
- package/template/scripts/create-view.mjs +0 -75
- package/template/src/components/DiffViewer.ts +0 -42
- package/template/stories/DiffViewer.stories.ts +0 -75
- package/template/stories/SettingsPanel.stories.ts +0 -11
- package/template/stories/Toggle.stories.ts +0 -28
package/README.md
CHANGED
|
@@ -42,9 +42,9 @@ scaffold folder (the CLI is cwd-relative); use `--project-dir=<outer-repo>` (or
|
|
|
42
42
|
`--global`) to install where an agent at the outer repo looks. The scaffolder
|
|
43
43
|
prints this hint when it detects nesting.
|
|
44
44
|
|
|
45
|
-
##
|
|
45
|
+
## Refresh an existing project
|
|
46
46
|
|
|
47
|
-
The scaffolder is create-
|
|
47
|
+
The scaffolder is create-first, but `refresh` refreshes an existing project's
|
|
48
48
|
**managed** files (`scripts/`, `docs/`, `.github/`, `.husky/`, `biome.json`,
|
|
49
49
|
agent guides, `tools/viewer/`, `tools/router/`, `tools/sandbox/`, `src/main.ts`,
|
|
50
50
|
`src/utilities.css`, `test/`) and merges new `package.json` scripts/deps —
|
|
@@ -52,19 +52,19 @@ it never touches `src/components/`, `stories/`, `public/`, `index.html`, or
|
|
|
52
52
|
build configs:
|
|
53
53
|
|
|
54
54
|
```sh
|
|
55
|
-
npx create-obsidian-arrow
|
|
56
|
-
npx create-obsidian-arrow
|
|
55
|
+
npx create-obsidian-arrow refresh # in the project (or: refresh <dir>)
|
|
56
|
+
npx create-obsidian-arrow refresh --dry-run # preview
|
|
57
57
|
```
|
|
58
58
|
|
|
59
|
-
Then
|
|
59
|
+
Then install with your package manager and run the `check` script.
|
|
60
60
|
|
|
61
61
|
## Local dev of the initializer
|
|
62
62
|
|
|
63
63
|
From the sandbox repo, before publishing:
|
|
64
64
|
|
|
65
65
|
```sh
|
|
66
|
-
node create-obsidian-arrow/index.mjs ../my-app
|
|
67
|
-
node create-obsidian-arrow/index.mjs
|
|
66
|
+
node create-obsidian-arrow/index.mjs create ../my-app # scaffold
|
|
67
|
+
node create-obsidian-arrow/index.mjs refresh ../my-app # refresh
|
|
68
68
|
```
|
|
69
69
|
|
|
70
70
|
## What you get
|
package/cli/create.mjs
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { spawnSync } from "node:child_process";
|
|
2
|
+
import fs from "node:fs";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import chalk from "chalk";
|
|
5
|
+
import { copyTree, ensureTemplate, fail, outerRepoAbove, templateDir } from "./lib.mjs";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Scaffold a new project into `targetArg`.
|
|
9
|
+
*
|
|
10
|
+
* Copies the vendored template/, restores .gitignore (vendored as _gitignore),
|
|
11
|
+
* names the project after the target dir, and runs `git init`.
|
|
12
|
+
*/
|
|
13
|
+
export function create(targetArg, opts = {}) {
|
|
14
|
+
ensureTemplate();
|
|
15
|
+
const destRoot = path.resolve(process.cwd(), targetArg);
|
|
16
|
+
const appName = path.basename(destRoot);
|
|
17
|
+
|
|
18
|
+
if (fs.existsSync(destRoot) && fs.readdirSync(destRoot).length > 0) {
|
|
19
|
+
fail(`target "${targetArg}" already exists and is not empty (use \`refresh\` to refresh it).`);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const written = [];
|
|
23
|
+
copyTree(templateDir, destRoot, destRoot, written, false);
|
|
24
|
+
// Restore .gitignore (npm omits .gitignore from packages; vendored as _gitignore).
|
|
25
|
+
const vendoredIgnore = path.join(destRoot, "_gitignore");
|
|
26
|
+
if (fs.existsSync(vendoredIgnore)) {
|
|
27
|
+
fs.renameSync(vendoredIgnore, path.join(destRoot, ".gitignore"));
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// Personalize the project name via targeted replace (keeps Biome formatting).
|
|
31
|
+
const pkgPath = path.join(destRoot, "package.json");
|
|
32
|
+
const renamed = fs
|
|
33
|
+
.readFileSync(pkgPath, "utf8")
|
|
34
|
+
.replace(/("name":\s*)"[^"]*"/, (_m, prefix) => `${prefix}${JSON.stringify(appName)}`);
|
|
35
|
+
fs.writeFileSync(pkgPath, renamed);
|
|
36
|
+
|
|
37
|
+
spawnSync("git", ["init", "-q"], { cwd: destRoot, stdio: "ignore" });
|
|
38
|
+
|
|
39
|
+
if (opts.json) {
|
|
40
|
+
process.stdout.write(
|
|
41
|
+
`${JSON.stringify({ action: "create", dir: destRoot, written, pkgChanges: [`name: ${appName}`] })}\n`
|
|
42
|
+
);
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
console.log(
|
|
47
|
+
`\n${chalk.green("Scaffolded")} ${chalk.bold(appName)} in ${path.relative(process.cwd(), destRoot) || "."}\n`
|
|
48
|
+
);
|
|
49
|
+
// A fresh scaffold has no lockfile yet — stay package-manager-neutral.
|
|
50
|
+
console.log("Next steps:");
|
|
51
|
+
console.log(chalk.cyan(` cd ${targetArg}`));
|
|
52
|
+
console.log(` ${chalk.dim("# install with your package manager (pnpm / npm / bun / yarn)")}`);
|
|
53
|
+
console.log(chalk.cyan(" <pm> install"));
|
|
54
|
+
console.log(
|
|
55
|
+
` ${chalk.cyan("<pm> run pull-css")} ${chalk.dim("# extract Obsidian's app.css (macOS auto-detect)")}`
|
|
56
|
+
);
|
|
57
|
+
console.log(chalk.cyan(" <pm> run dev\n"));
|
|
58
|
+
|
|
59
|
+
const outer = outerRepoAbove(destRoot);
|
|
60
|
+
if (outer) {
|
|
61
|
+
console.log(chalk.yellow(`Note: this project is nested inside the repo at ${outer}.`));
|
|
62
|
+
console.log(" Bundled skills install scoped to THIS project. To install them at the");
|
|
63
|
+
console.log(` outer repo instead: <pm> run skills:install -- --yes --project-dir=${outer}\n`);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
|
|
4
|
+
const LOCK = [
|
|
5
|
+
["bun.lockb", "bun"],
|
|
6
|
+
["pnpm-lock.yaml", "pnpm"],
|
|
7
|
+
["yarn.lock", "yarn"],
|
|
8
|
+
["package-lock.json", "npm"],
|
|
9
|
+
];
|
|
10
|
+
|
|
11
|
+
/** Detect the package manager governing `dir` from its lockfile.
|
|
12
|
+
* Falls back to npm. `exec` is the argv prefix for running a script. */
|
|
13
|
+
export function detectPM(dir = process.cwd()) {
|
|
14
|
+
for (const [f, name] of LOCK) {
|
|
15
|
+
if (fs.existsSync(path.join(dir, f))) {
|
|
16
|
+
return { name, exec: name === "yarn" ? ["yarn"] : [name, "run"] };
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
return { name: "npm", exec: ["npm", "run"] };
|
|
20
|
+
}
|
package/cli/lib.mjs
ADDED
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
4
|
+
|
|
5
|
+
const here = path.dirname(fileURLToPath(import.meta.url)); // cli/
|
|
6
|
+
export const pkgRoot = path.resolve(here, ".."); // packages/create-obsidian-arrow
|
|
7
|
+
export const templateDir = path.join(pkgRoot, "template");
|
|
8
|
+
|
|
9
|
+
// Files/dirs the template owns and `refresh` may overwrite/merge.
|
|
10
|
+
//
|
|
11
|
+
// src/components/ and stories/ are user-owned and never touched.
|
|
12
|
+
// tools/ contains all viewer/router/sandbox infrastructure and IS managed.
|
|
13
|
+
// Everything else (public/, index.html, vite.config.ts, tsconfig.json,
|
|
14
|
+
// lockfile, .gitignore, port-parity.json, …) is also user-owned.
|
|
15
|
+
export const MANAGED = [
|
|
16
|
+
// Tooling
|
|
17
|
+
"scripts",
|
|
18
|
+
"docs",
|
|
19
|
+
".github",
|
|
20
|
+
".husky",
|
|
21
|
+
"biome.json",
|
|
22
|
+
"AGENTS.md",
|
|
23
|
+
"CLAUDE.md",
|
|
24
|
+
// Viewer infrastructure — all managed; src/components/ and stories/ are user-owned
|
|
25
|
+
"tools",
|
|
26
|
+
"src/main.ts",
|
|
27
|
+
"src/utilities.css",
|
|
28
|
+
// Viewer tests
|
|
29
|
+
"test",
|
|
30
|
+
];
|
|
31
|
+
|
|
32
|
+
export function fail(message) {
|
|
33
|
+
console.error(`create-obsidian-arrow: ${message}`);
|
|
34
|
+
process.exit(1);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function ensureTemplate() {
|
|
38
|
+
if (!fs.existsSync(templateDir)) {
|
|
39
|
+
fail("template/ is missing — run scripts/sync-template.mjs to build it.");
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** Nearest ancestor *above* `dir` that is a git repo, or null. */
|
|
44
|
+
export function outerRepoAbove(dir) {
|
|
45
|
+
let current = path.dirname(path.resolve(dir));
|
|
46
|
+
const fsRoot = path.parse(current).root;
|
|
47
|
+
while (current && current !== fsRoot) {
|
|
48
|
+
if (fs.existsSync(path.join(current, ".git"))) {
|
|
49
|
+
return current;
|
|
50
|
+
}
|
|
51
|
+
current = path.dirname(current);
|
|
52
|
+
}
|
|
53
|
+
return null;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** Recursively copy src→dest; returns the list of written file paths (relative
|
|
57
|
+
* to `base`). Honors `dryRun` (records but doesn't write). */
|
|
58
|
+
export function copyTree(src, dest, base, written, dryRun) {
|
|
59
|
+
const stat = fs.statSync(src);
|
|
60
|
+
if (stat.isDirectory()) {
|
|
61
|
+
if (!dryRun) {
|
|
62
|
+
fs.mkdirSync(dest, { recursive: true });
|
|
63
|
+
}
|
|
64
|
+
for (const entry of fs.readdirSync(src)) {
|
|
65
|
+
copyTree(path.join(src, entry), path.join(dest, entry), base, written, dryRun);
|
|
66
|
+
}
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
written.push(path.relative(base, dest));
|
|
70
|
+
if (!dryRun) {
|
|
71
|
+
fs.mkdirSync(path.dirname(dest), { recursive: true });
|
|
72
|
+
fs.copyFileSync(src, dest);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** Merge template package.json scripts + missing deps into the target's,
|
|
77
|
+
* preserving name/version/identity and existing dep versions. Returns the
|
|
78
|
+
* list of change descriptions.
|
|
79
|
+
*
|
|
80
|
+
* Also propagates top-level `lint-staged` and `pnpm` fields when absent in
|
|
81
|
+
* the target so refreshed old projects get husky/lint-staged fully wired. */
|
|
82
|
+
export function mergePackageJson(targetPkgPath, dryRun) {
|
|
83
|
+
const tpl = JSON.parse(fs.readFileSync(path.join(templateDir, "package.json"), "utf8"));
|
|
84
|
+
const pkg = JSON.parse(fs.readFileSync(targetPkgPath, "utf8"));
|
|
85
|
+
const changes = [];
|
|
86
|
+
|
|
87
|
+
pkg.scripts ??= {};
|
|
88
|
+
for (const [name, cmd] of Object.entries(tpl.scripts ?? {})) {
|
|
89
|
+
if (pkg.scripts[name] !== cmd) {
|
|
90
|
+
changes.push(`script ${name}`);
|
|
91
|
+
pkg.scripts[name] = cmd;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
for (const field of ["dependencies", "devDependencies"]) {
|
|
95
|
+
pkg[field] ??= {};
|
|
96
|
+
for (const [name, version] of Object.entries(tpl[field] ?? {})) {
|
|
97
|
+
if (!(name in pkg[field])) {
|
|
98
|
+
changes.push(`${field}: ${name}`);
|
|
99
|
+
pkg[field][name] = version;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// Propagate top-level lint-staged and pnpm fields when absent (so refreshed
|
|
105
|
+
// old projects get husky/lint-staged fully configured, not just the devDeps).
|
|
106
|
+
for (const field of ["lint-staged", "pnpm"]) {
|
|
107
|
+
if (tpl[field] !== undefined && pkg[field] === undefined) {
|
|
108
|
+
changes.push(field);
|
|
109
|
+
pkg[field] = tpl[field];
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
if (changes.length > 0 && !dryRun) {
|
|
114
|
+
fs.writeFileSync(targetPkgPath, `${JSON.stringify(pkg, null, "\t")}\n`);
|
|
115
|
+
}
|
|
116
|
+
return changes;
|
|
117
|
+
}
|
package/cli/refresh.mjs
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import chalk from "chalk";
|
|
4
|
+
import { detectPM } from "./detect-pm.mjs";
|
|
5
|
+
import { MANAGED, copyTree, ensureTemplate, fail, mergePackageJson, templateDir } from "./lib.mjs";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Refresh an existing project's *managed* files from the template and merge
|
|
9
|
+
* package.json scripts + missing deps. Preserves user code. `--dry-run` first.
|
|
10
|
+
*/
|
|
11
|
+
export function refresh(targetArg, opts = {}) {
|
|
12
|
+
ensureTemplate();
|
|
13
|
+
const dryRun = Boolean(opts.dryRun);
|
|
14
|
+
const root = path.resolve(process.cwd(), targetArg ?? ".");
|
|
15
|
+
if (!fs.existsSync(path.join(root, "package.json"))) {
|
|
16
|
+
fail(`no package.json in ${root} — is this a scaffolded project?`);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const written = [];
|
|
20
|
+
for (const name of MANAGED) {
|
|
21
|
+
const src = path.join(templateDir, name);
|
|
22
|
+
if (fs.existsSync(src)) {
|
|
23
|
+
copyTree(src, path.join(root, name), root, written, dryRun);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
const pkgChanges = mergePackageJson(path.join(root, "package.json"), dryRun);
|
|
27
|
+
|
|
28
|
+
// Create stories/ if missing — user-owned (never overwritten), but must
|
|
29
|
+
// exist before the first story file can be added.
|
|
30
|
+
const storiesDir = path.join(root, "stories");
|
|
31
|
+
const storiesCreated = !fs.existsSync(storiesDir);
|
|
32
|
+
if (storiesCreated && !dryRun) {
|
|
33
|
+
fs.mkdirSync(storiesDir);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
if (opts.json) {
|
|
37
|
+
process.stdout.write(
|
|
38
|
+
`${JSON.stringify({ action: "refresh", dir: root, written, pkgChanges })}\n`
|
|
39
|
+
);
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const pm = detectPM(root);
|
|
44
|
+
const run = pm.exec.join(" ");
|
|
45
|
+
const verb = dryRun ? "Would refresh" : "Refreshed";
|
|
46
|
+
console.log(
|
|
47
|
+
`${chalk.green(verb)} ${written.length} managed file(s) in ${path.relative(process.cwd(), root) || "."}:`
|
|
48
|
+
);
|
|
49
|
+
for (const file of written) {
|
|
50
|
+
console.log(` ${file}`);
|
|
51
|
+
}
|
|
52
|
+
if (storiesCreated) {
|
|
53
|
+
console.log(
|
|
54
|
+
` stories/ ${dryRun ? "(would create empty)" : "(created empty — add your *.stories.ts files here)"}`
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
if (pkgChanges.length > 0) {
|
|
58
|
+
console.log(`package.json: ${dryRun ? "would update" : "updated"} ${pkgChanges.join(", ")}`);
|
|
59
|
+
}
|
|
60
|
+
console.log(
|
|
61
|
+
dryRun
|
|
62
|
+
? "\n(dry run — nothing written.)"
|
|
63
|
+
: `\nLeft alone: src/components/, stories/, public/, index.html, vite.config.ts, tsconfig.json, .gitignore.\n(tools/, src/main.ts, src/utilities.css, and test/ were refreshed above.)\nRun \`${pm.name} install\` then \`${run} check\`. Update skills separately with \`${run} skills:update\`.\n`
|
|
64
|
+
);
|
|
65
|
+
}
|
package/index.mjs
CHANGED
|
@@ -1,221 +1,64 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
/**
|
|
3
|
-
* create-obsidian-arrow — scaffold or
|
|
3
|
+
* create-obsidian-arrow — scaffold or refresh an Obsidian-styled Arrow.js sandbox.
|
|
4
4
|
*
|
|
5
|
-
* create-obsidian-arrow <dir>
|
|
6
|
-
* create-obsidian-arrow
|
|
7
|
-
*
|
|
5
|
+
* create-obsidian-arrow create <dir> scaffold a new project into <dir>
|
|
6
|
+
* create-obsidian-arrow refresh [dir] refresh an existing project's tooling
|
|
7
|
+
* (default dir: cwd) — preserves your code
|
|
8
|
+
* create-obsidian-arrow <dir> bare-arg shorthand for `create <dir>`
|
|
8
9
|
*
|
|
9
10
|
* pnpm create obsidian-arrow my-app
|
|
10
|
-
*
|
|
11
|
-
* node create-obsidian-arrow/index.mjs update ./my-app
|
|
11
|
+
* npx create-obsidian-arrow my-app
|
|
12
12
|
*
|
|
13
|
-
*
|
|
13
|
+
* `create` copies the vendored template/, restores .gitignore (vendored as
|
|
14
14
|
* _gitignore), names the project, and runs `git init`.
|
|
15
15
|
*
|
|
16
|
-
*
|
|
16
|
+
* `refresh` refreshes *managed* files from the template and merges package.json
|
|
17
17
|
* scripts + missing deps. Managed = scripts/, docs/, .github/, .husky/,
|
|
18
18
|
* biome.json, AGENTS.md, CLAUDE.md, tools/, src/main.ts, src/utilities.css,
|
|
19
19
|
* test/. Never touches src/components/, stories/, public/, index.html,
|
|
20
20
|
* vite.config.ts, tsconfig.json, or .gitignore. Use --dry-run first.
|
|
21
21
|
*/
|
|
22
|
-
import {
|
|
23
|
-
import
|
|
24
|
-
import
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
22
|
+
import { Command } from "commander";
|
|
23
|
+
import { create } from "./cli/create.mjs";
|
|
24
|
+
import { refresh } from "./cli/refresh.mjs";
|
|
25
|
+
|
|
26
|
+
const program = new Command();
|
|
27
|
+
|
|
28
|
+
program
|
|
29
|
+
.name("create-obsidian-arrow")
|
|
30
|
+
.description("Scaffold or refresh an Obsidian-styled Arrow.js UI sandbox.");
|
|
31
|
+
|
|
32
|
+
program
|
|
33
|
+
.command("create")
|
|
34
|
+
.argument("<dir>", "directory to scaffold into")
|
|
35
|
+
.description("scaffold a new project into <dir>")
|
|
36
|
+
.option("--json", "emit a single JSON line describing the result")
|
|
37
|
+
.allowExcessArguments(false)
|
|
38
|
+
.action((dir, opts) => create(dir, opts));
|
|
39
|
+
|
|
40
|
+
program
|
|
41
|
+
.command("refresh")
|
|
42
|
+
.argument("[dir]", "project directory (default: cwd)")
|
|
43
|
+
.description("refresh an existing project's managed tooling")
|
|
44
|
+
.option("--dry-run", "list what would change without writing")
|
|
45
|
+
.option("--json", "emit a single JSON line describing the result")
|
|
46
|
+
.allowExcessArguments(false)
|
|
47
|
+
.action((dir, opts) => refresh(dir, opts));
|
|
48
|
+
|
|
49
|
+
// Bare-arg shorthand: `create-obsidian-arrow my-app` → `create my-app`, so
|
|
50
|
+
// `npx create-obsidian-arrow my-app` still works. If the first non-option token
|
|
51
|
+
// isn't a known subcommand, prepend `create` at the FRONT so any leading global
|
|
52
|
+
// flags (e.g. `--json my-app`) attach to the `create` subcommand rather than the
|
|
53
|
+
// (option-less) root program.
|
|
54
54
|
const argv = process.argv.slice(2);
|
|
55
|
-
const
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
process.exit(1);
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
if (!fs.existsSync(templateDir)) {
|
|
63
|
-
fail("template/ is missing — run scripts/sync-template.mjs to build it.");
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
/** Nearest ancestor *above* `dir` that is a git repo, or null. */
|
|
67
|
-
function outerRepoAbove(dir) {
|
|
68
|
-
let current = path.dirname(path.resolve(dir));
|
|
69
|
-
const fsRoot = path.parse(current).root;
|
|
70
|
-
while (current && current !== fsRoot) {
|
|
71
|
-
if (fs.existsSync(path.join(current, ".git"))) {
|
|
72
|
-
return current;
|
|
73
|
-
}
|
|
74
|
-
current = path.dirname(current);
|
|
75
|
-
}
|
|
76
|
-
return null;
|
|
55
|
+
const firstToken = argv.find((a) => !a.startsWith("-"));
|
|
56
|
+
const knownCommands = new Set(["create", "refresh", "help"]);
|
|
57
|
+
if (firstToken && !knownCommands.has(firstToken)) {
|
|
58
|
+
argv.unshift("create");
|
|
77
59
|
}
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
* to `base`). Honors --dry-run (records but doesn't write). */
|
|
81
|
-
function copyTree(src, dest, base, written) {
|
|
82
|
-
const stat = fs.statSync(src);
|
|
83
|
-
if (stat.isDirectory()) {
|
|
84
|
-
if (!dryRun) {
|
|
85
|
-
fs.mkdirSync(dest, { recursive: true });
|
|
86
|
-
}
|
|
87
|
-
for (const entry of fs.readdirSync(src)) {
|
|
88
|
-
copyTree(path.join(src, entry), path.join(dest, entry), base, written);
|
|
89
|
-
}
|
|
90
|
-
return;
|
|
91
|
-
}
|
|
92
|
-
written.push(path.relative(base, dest));
|
|
93
|
-
if (!dryRun) {
|
|
94
|
-
fs.mkdirSync(path.dirname(dest), { recursive: true });
|
|
95
|
-
fs.copyFileSync(src, dest);
|
|
96
|
-
}
|
|
60
|
+
if (argv.length === 0) {
|
|
61
|
+
program.help();
|
|
97
62
|
}
|
|
98
63
|
|
|
99
|
-
|
|
100
|
-
const destRoot = path.resolve(process.cwd(), targetArg);
|
|
101
|
-
const appName = path.basename(destRoot);
|
|
102
|
-
|
|
103
|
-
if (fs.existsSync(destRoot) && fs.readdirSync(destRoot).length > 0) {
|
|
104
|
-
fail(`target "${targetArg}" already exists and is not empty (use \`update\` to refresh it).`);
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
const written = [];
|
|
108
|
-
copyTree(templateDir, destRoot, destRoot, written);
|
|
109
|
-
// Restore .gitignore (npm omits .gitignore from packages; vendored as _gitignore).
|
|
110
|
-
const vendoredIgnore = path.join(destRoot, "_gitignore");
|
|
111
|
-
if (fs.existsSync(vendoredIgnore)) {
|
|
112
|
-
fs.renameSync(vendoredIgnore, path.join(destRoot, ".gitignore"));
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
// Personalize the project name via targeted replace (keeps Biome formatting).
|
|
116
|
-
const pkgPath = path.join(destRoot, "package.json");
|
|
117
|
-
const renamed = fs
|
|
118
|
-
.readFileSync(pkgPath, "utf8")
|
|
119
|
-
.replace(/("name":\s*)"[^"]*"/, (_m, prefix) => `${prefix}${JSON.stringify(appName)}`);
|
|
120
|
-
fs.writeFileSync(pkgPath, renamed);
|
|
121
|
-
|
|
122
|
-
spawnSync("git", ["init", "-q"], { cwd: destRoot, stdio: "ignore" });
|
|
123
|
-
|
|
124
|
-
console.log(`\nScaffolded ${appName} in ${path.relative(process.cwd(), destRoot) || "."}\n`);
|
|
125
|
-
console.log("Next steps:");
|
|
126
|
-
console.log(` cd ${targetArg}`);
|
|
127
|
-
console.log(" pnpm install");
|
|
128
|
-
console.log(" pnpm pull-css # extract Obsidian's app.css (macOS auto-detect)");
|
|
129
|
-
console.log(" pnpm dev\n");
|
|
130
|
-
|
|
131
|
-
const outer = outerRepoAbove(destRoot);
|
|
132
|
-
if (outer) {
|
|
133
|
-
console.log(`Note: this project is nested inside the repo at ${outer}.`);
|
|
134
|
-
console.log(" Bundled skills install scoped to THIS project. To install them at the");
|
|
135
|
-
console.log(` outer repo instead: pnpm skills:install --yes --project-dir=${outer}\n`);
|
|
136
|
-
}
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
/** Merge template package.json scripts + missing deps into the target's,
|
|
140
|
-
* preserving name/version/identity and existing dep versions. */
|
|
141
|
-
function mergePackageJson(targetPkgPath) {
|
|
142
|
-
const tpl = JSON.parse(fs.readFileSync(path.join(templateDir, "package.json"), "utf8"));
|
|
143
|
-
const pkg = JSON.parse(fs.readFileSync(targetPkgPath, "utf8"));
|
|
144
|
-
const changes = [];
|
|
145
|
-
|
|
146
|
-
pkg.scripts ??= {};
|
|
147
|
-
for (const [name, cmd] of Object.entries(tpl.scripts ?? {})) {
|
|
148
|
-
if (pkg.scripts[name] !== cmd) {
|
|
149
|
-
changes.push(`script ${name}`);
|
|
150
|
-
pkg.scripts[name] = cmd;
|
|
151
|
-
}
|
|
152
|
-
}
|
|
153
|
-
for (const field of ["dependencies", "devDependencies"]) {
|
|
154
|
-
pkg[field] ??= {};
|
|
155
|
-
for (const [name, version] of Object.entries(tpl[field] ?? {})) {
|
|
156
|
-
if (!(name in pkg[field])) {
|
|
157
|
-
changes.push(`${field}: ${name}`);
|
|
158
|
-
pkg[field][name] = version;
|
|
159
|
-
}
|
|
160
|
-
}
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
if (changes.length > 0 && !dryRun) {
|
|
164
|
-
fs.writeFileSync(targetPkgPath, `${JSON.stringify(pkg, null, "\t")}\n`);
|
|
165
|
-
}
|
|
166
|
-
return changes;
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
function update(targetArg) {
|
|
170
|
-
const root = path.resolve(process.cwd(), targetArg ?? ".");
|
|
171
|
-
if (!fs.existsSync(path.join(root, "package.json"))) {
|
|
172
|
-
fail(`no package.json in ${root} — is this a scaffolded project?`);
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
const written = [];
|
|
176
|
-
for (const name of MANAGED) {
|
|
177
|
-
const src = path.join(templateDir, name);
|
|
178
|
-
if (fs.existsSync(src)) {
|
|
179
|
-
copyTree(src, path.join(root, name), root, written);
|
|
180
|
-
}
|
|
181
|
-
}
|
|
182
|
-
const pkgChanges = mergePackageJson(path.join(root, "package.json"));
|
|
183
|
-
|
|
184
|
-
// Create stories/ if missing — user-owned (never overwritten), but must
|
|
185
|
-
// exist before the first story file can be added.
|
|
186
|
-
const storiesDir = path.join(root, "stories");
|
|
187
|
-
const storiesCreated = !fs.existsSync(storiesDir);
|
|
188
|
-
if (storiesCreated && !dryRun) {
|
|
189
|
-
fs.mkdirSync(storiesDir);
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
const verb = dryRun ? "Would refresh" : "Refreshed";
|
|
193
|
-
console.log(
|
|
194
|
-
`${verb} ${written.length} managed file(s) in ${path.relative(process.cwd(), root) || "."}:`
|
|
195
|
-
);
|
|
196
|
-
for (const file of written) {
|
|
197
|
-
console.log(` ${file}`);
|
|
198
|
-
}
|
|
199
|
-
if (storiesCreated) {
|
|
200
|
-
console.log(
|
|
201
|
-
` stories/ ${dryRun ? "(would create empty)" : "(created empty — add your *.stories.ts files here)"}`
|
|
202
|
-
);
|
|
203
|
-
}
|
|
204
|
-
if (pkgChanges.length > 0) {
|
|
205
|
-
console.log(`package.json: ${dryRun ? "would update" : "updated"} ${pkgChanges.join(", ")}`);
|
|
206
|
-
}
|
|
207
|
-
console.log(
|
|
208
|
-
dryRun
|
|
209
|
-
? "\n(dry run — nothing written.)"
|
|
210
|
-
: "\nLeft alone: src/components/, stories/, public/, index.html, vite.config.ts, tsconfig.json, .gitignore.\n(tools/, src/main.ts, src/utilities.css, and test/ were refreshed above.)\nRun `pnpm install` then `pnpm check`. Update skills separately with `pnpm skills:update`.\n"
|
|
211
|
-
);
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
const command = argv[0];
|
|
215
|
-
if (command === "update") {
|
|
216
|
-
update(argv.find((a) => a !== "update" && !a.startsWith("--")));
|
|
217
|
-
} else if (!command || command.startsWith("--")) {
|
|
218
|
-
fail("usage: create-obsidian-arrow <directory> | update [directory]");
|
|
219
|
-
} else {
|
|
220
|
-
scaffold(command);
|
|
221
|
-
}
|
|
64
|
+
program.parse(argv, { from: "user" });
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "create-obsidian-arrow",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.2",
|
|
4
4
|
"description": "Scaffold an Obsidian-styled Arrow.js UI sandbox (pnpm create obsidian-arrow <dir>).",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
},
|
|
9
9
|
"files": [
|
|
10
10
|
"index.mjs",
|
|
11
|
+
"cli",
|
|
11
12
|
"template"
|
|
12
13
|
],
|
|
13
14
|
"engines": {
|
|
@@ -36,5 +37,13 @@
|
|
|
36
37
|
"url": "git+https://github.com/kylebrodeur/obsidian-arrow-sandbox.git",
|
|
37
38
|
"directory": "create-obsidian-arrow"
|
|
38
39
|
},
|
|
39
|
-
"
|
|
40
|
+
"dependencies": {
|
|
41
|
+
"chalk": "^5.6.2",
|
|
42
|
+
"commander": "^12.1.0"
|
|
43
|
+
},
|
|
44
|
+
"scripts": {
|
|
45
|
+
"test": "node --test test/**/*.test.mjs",
|
|
46
|
+
"check": "node --test test/**/*.test.mjs",
|
|
47
|
+
"ci": "node --test test/**/*.test.mjs"
|
|
48
|
+
}
|
|
40
49
|
}
|
|
@@ -1,2 +1,3 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
1
|
+
#!/usr/bin/env sh
|
|
2
|
+
./node_modules/.bin/lint-staged
|
|
3
|
+
./node_modules/.bin/tsc -p tsconfig.json --noEmit
|