@1dot5/design-assistant 0.2.1 → 0.3.0
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 +98 -0
- package/bin/cli.js +210 -0
- package/package.json +11 -3
package/README.md
ADDED
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
# @1dot5/design-assistant
|
|
2
|
+
|
|
3
|
+
Next.js 16 + React 19 プロジェクト向けに、シニアデザイナー兼フロントエンドエンジニアとしての規約・Skill・ワークフローを一式ばらまくための CLI。
|
|
4
|
+
|
|
5
|
+
- `AGENTS.md` — エージェント共通の Single Source of Truth
|
|
6
|
+
- `.agent/*.md` — Claude Code 以外のツール(Cursor / Codex / OpenHands 等)から参照するビュー
|
|
7
|
+
- `.claude/skills/**/SKILL.md` — Claude Code 用の自動発火 Skill
|
|
8
|
+
- `.claude/CLAUDE.md` — Claude Code 用のエントリ(`AGENTS.md` と同一内容)
|
|
9
|
+
|
|
10
|
+
## 使い方
|
|
11
|
+
|
|
12
|
+
既存プロジェクトの直下で一発で展開する。
|
|
13
|
+
|
|
14
|
+
```sh
|
|
15
|
+
# 現在のディレクトリに展開
|
|
16
|
+
npx @1dot5/design-assistant
|
|
17
|
+
|
|
18
|
+
# 別のディレクトリに展開
|
|
19
|
+
npx @1dot5/design-assistant ./path/to/app
|
|
20
|
+
|
|
21
|
+
# 何が書かれるかだけ確認
|
|
22
|
+
npx @1dot5/design-assistant --dry-run
|
|
23
|
+
|
|
24
|
+
# 既存ファイルを上書き(更新したいとき)
|
|
25
|
+
npx @1dot5/design-assistant --force
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
pnpm / yarn / bun でも等価に動く。
|
|
29
|
+
|
|
30
|
+
```sh
|
|
31
|
+
pnpm dlx @1dot5/design-assistant
|
|
32
|
+
yarn dlx @1dot5/design-assistant
|
|
33
|
+
bunx @1dot5/design-assistant
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
## 動作
|
|
37
|
+
|
|
38
|
+
- 既存ファイルがあり内容が同一なら **何もしない**(`unchanged`)。
|
|
39
|
+
- 既存ファイルがあり内容が異なるなら **スキップ**し、末尾に差分一覧を表示する。`--force` で上書き可能。
|
|
40
|
+
- `.agent/*.md` と `.claude/CLAUDE.md` は、リポジトリ上はシンボリックリンクだが、インストール時には **実ファイルとしてコピー**される。これにより npm tarball やクロスプラットフォーム環境で壊れない。
|
|
41
|
+
|
|
42
|
+
### コマンド
|
|
43
|
+
|
|
44
|
+
```
|
|
45
|
+
Usage
|
|
46
|
+
npx @1dot5/design-assistant [dir] [options]
|
|
47
|
+
|
|
48
|
+
Arguments
|
|
49
|
+
dir Target directory (default: ".")
|
|
50
|
+
|
|
51
|
+
Options
|
|
52
|
+
-f, --force Overwrite existing files
|
|
53
|
+
-n, --dry-run Show what would be written without writing
|
|
54
|
+
-h, --help Show this help
|
|
55
|
+
-v, --version Print version
|
|
56
|
+
|
|
57
|
+
Exit codes
|
|
58
|
+
0 success (all files written, or already up to date)
|
|
59
|
+
1 some files were skipped due to conflicts (rerun with --force)
|
|
60
|
+
2 invalid arguments
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
## 展開後の構成
|
|
64
|
+
|
|
65
|
+
```
|
|
66
|
+
<target>/
|
|
67
|
+
├── AGENTS.md
|
|
68
|
+
├── .agent/
|
|
69
|
+
│ ├── components.md
|
|
70
|
+
│ ├── design-to-code.md
|
|
71
|
+
│ ├── design.md
|
|
72
|
+
│ ├── frontend-arch.md
|
|
73
|
+
│ └── review.md
|
|
74
|
+
└── .claude/
|
|
75
|
+
├── CLAUDE.md
|
|
76
|
+
└── skills/
|
|
77
|
+
├── component-architect/SKILL.md
|
|
78
|
+
├── design-review/SKILL.md
|
|
79
|
+
├── design-to-code/SKILL.md
|
|
80
|
+
├── frontend-architecture/SKILL.md
|
|
81
|
+
├── pencil-design-flow/SKILL.md
|
|
82
|
+
└── ui-designer/SKILL.md
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
## 更新
|
|
86
|
+
|
|
87
|
+
このパッケージが更新されたら、プロジェクト側で再実行すれば差分だけ取り込める。
|
|
88
|
+
|
|
89
|
+
```sh
|
|
90
|
+
npx @1dot5/design-assistant --dry-run # まずは差分確認
|
|
91
|
+
npx @1dot5/design-assistant --force # 納得したら上書き
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
プロジェクト側で `AGENTS.md` や Skill を **カスタマイズしている場合は `--force` を使わない**。デフォルト動作は安全側で、衝突時はスキップ+exit 1 を返す。
|
|
95
|
+
|
|
96
|
+
## ライセンス
|
|
97
|
+
|
|
98
|
+
MIT
|
package/bin/cli.js
ADDED
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { parseArgs } from "node:util";
|
|
3
|
+
import {
|
|
4
|
+
access,
|
|
5
|
+
copyFile,
|
|
6
|
+
mkdir,
|
|
7
|
+
readdir,
|
|
8
|
+
readFile,
|
|
9
|
+
realpath,
|
|
10
|
+
stat,
|
|
11
|
+
} from "node:fs/promises";
|
|
12
|
+
import { constants } from "node:fs";
|
|
13
|
+
import { fileURLToPath } from "node:url";
|
|
14
|
+
import { dirname, join, resolve } from "node:path";
|
|
15
|
+
import { createRequire } from "node:module";
|
|
16
|
+
|
|
17
|
+
const require = createRequire(import.meta.url);
|
|
18
|
+
const pkg = require("../package.json");
|
|
19
|
+
|
|
20
|
+
const PKG_ROOT = resolve(dirname(fileURLToPath(import.meta.url)), "..");
|
|
21
|
+
|
|
22
|
+
const FILE_SOURCES = ["AGENTS.md"];
|
|
23
|
+
const DIR_SOURCES = [".claude/skills"];
|
|
24
|
+
|
|
25
|
+
// dest path (in target project) -> source path (in this package).
|
|
26
|
+
// Repo uses symlinks for these; tarballs don't preserve symlinks reliably, so
|
|
27
|
+
// the installer synthesizes them by copying from the underlying source.
|
|
28
|
+
export const ALIASES = {
|
|
29
|
+
".agent/components.md": ".claude/skills/component-architect/SKILL.md",
|
|
30
|
+
".agent/design-to-code.md": ".claude/skills/design-to-code/SKILL.md",
|
|
31
|
+
".agent/design.md": ".claude/skills/ui-designer/SKILL.md",
|
|
32
|
+
".agent/frontend-arch.md": ".claude/skills/frontend-architecture/SKILL.md",
|
|
33
|
+
".agent/review.md": ".claude/skills/design-review/SKILL.md",
|
|
34
|
+
".claude/CLAUDE.md": "AGENTS.md",
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
const HELP = `${pkg.name} v${pkg.version}
|
|
38
|
+
${pkg.description}
|
|
39
|
+
|
|
40
|
+
Usage
|
|
41
|
+
npx ${pkg.name} [dir] [options]
|
|
42
|
+
|
|
43
|
+
Arguments
|
|
44
|
+
dir Target directory (default: ".")
|
|
45
|
+
|
|
46
|
+
Options
|
|
47
|
+
-f, --force Overwrite existing files
|
|
48
|
+
-n, --dry-run Show what would be written without writing
|
|
49
|
+
-h, --help Show this help
|
|
50
|
+
-v, --version Print version
|
|
51
|
+
|
|
52
|
+
Examples
|
|
53
|
+
npx ${pkg.name} Install into the current directory
|
|
54
|
+
npx ${pkg.name} ./my-app Install into ./my-app
|
|
55
|
+
npx ${pkg.name} --dry-run Preview the file list
|
|
56
|
+
npx ${pkg.name} --force Overwrite existing files
|
|
57
|
+
|
|
58
|
+
Exit codes
|
|
59
|
+
0 success (all files written, or already up to date)
|
|
60
|
+
1 some files were skipped due to conflicts (rerun with --force)
|
|
61
|
+
2 invalid arguments
|
|
62
|
+
`;
|
|
63
|
+
|
|
64
|
+
function parse() {
|
|
65
|
+
try {
|
|
66
|
+
return parseArgs({
|
|
67
|
+
allowPositionals: true,
|
|
68
|
+
options: {
|
|
69
|
+
force: { type: "boolean", short: "f", default: false },
|
|
70
|
+
"dry-run": { type: "boolean", short: "n", default: false },
|
|
71
|
+
help: { type: "boolean", short: "h", default: false },
|
|
72
|
+
version: { type: "boolean", short: "v", default: false },
|
|
73
|
+
},
|
|
74
|
+
});
|
|
75
|
+
} catch (err) {
|
|
76
|
+
process.stderr.write(`error: ${err.message}\n\n${HELP}`);
|
|
77
|
+
process.exit(2);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
async function walk(absRoot, rel = "") {
|
|
82
|
+
const abs = rel ? join(absRoot, rel) : absRoot;
|
|
83
|
+
const st = await stat(abs);
|
|
84
|
+
if (st.isFile()) return [rel];
|
|
85
|
+
if (!st.isDirectory()) return [];
|
|
86
|
+
const out = [];
|
|
87
|
+
for (const entry of await readdir(abs)) {
|
|
88
|
+
out.push(...(await walk(absRoot, rel ? join(rel, entry) : entry)));
|
|
89
|
+
}
|
|
90
|
+
return out;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
async function collectOps() {
|
|
94
|
+
const ops = [];
|
|
95
|
+
for (const rel of FILE_SOURCES) ops.push({ src: rel, dst: rel });
|
|
96
|
+
for (const dir of DIR_SOURCES) {
|
|
97
|
+
const inner = await walk(join(PKG_ROOT, dir));
|
|
98
|
+
for (const rel of inner) {
|
|
99
|
+
const full = join(dir, rel);
|
|
100
|
+
ops.push({ src: full, dst: full });
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
for (const [dst, src] of Object.entries(ALIASES)) {
|
|
104
|
+
ops.push({ src, dst });
|
|
105
|
+
}
|
|
106
|
+
return ops;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
async function exists(p) {
|
|
110
|
+
try {
|
|
111
|
+
await access(p, constants.F_OK);
|
|
112
|
+
return true;
|
|
113
|
+
} catch {
|
|
114
|
+
return false;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
async function sameContent(a, b) {
|
|
119
|
+
const [bufA, bufB] = await Promise.all([readFile(a), readFile(b)]);
|
|
120
|
+
return bufA.equals(bufB);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
async function main() {
|
|
124
|
+
const { values, positionals } = parse();
|
|
125
|
+
|
|
126
|
+
if (values.help) {
|
|
127
|
+
process.stdout.write(HELP);
|
|
128
|
+
return 0;
|
|
129
|
+
}
|
|
130
|
+
if (values.version) {
|
|
131
|
+
process.stdout.write(`${pkg.version}\n`);
|
|
132
|
+
return 0;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const target = resolve(process.cwd(), positionals[0] ?? ".");
|
|
136
|
+
const dryRun = values["dry-run"];
|
|
137
|
+
const force = values.force;
|
|
138
|
+
|
|
139
|
+
const ops = await collectOps();
|
|
140
|
+
|
|
141
|
+
const toWrite = [];
|
|
142
|
+
const toSkip = [];
|
|
143
|
+
const unchanged = [];
|
|
144
|
+
|
|
145
|
+
for (const op of ops) {
|
|
146
|
+
const srcAbs = join(PKG_ROOT, op.src);
|
|
147
|
+
const dstAbs = join(target, op.dst);
|
|
148
|
+
if (!(await exists(dstAbs))) {
|
|
149
|
+
toWrite.push({ ...op, srcAbs, dstAbs, kind: "create" });
|
|
150
|
+
continue;
|
|
151
|
+
}
|
|
152
|
+
if (await sameContent(srcAbs, dstAbs)) {
|
|
153
|
+
unchanged.push(op.dst);
|
|
154
|
+
continue;
|
|
155
|
+
}
|
|
156
|
+
if (force) toWrite.push({ ...op, srcAbs, dstAbs, kind: "overwrite" });
|
|
157
|
+
else toSkip.push(op.dst);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
process.stdout.write(`target: ${target}\n`);
|
|
161
|
+
|
|
162
|
+
if (toSkip.length) {
|
|
163
|
+
process.stdout.write(
|
|
164
|
+
`\nskipped ${toSkip.length} existing file(s) (use --force to overwrite):\n`,
|
|
165
|
+
);
|
|
166
|
+
for (const rel of toSkip) process.stdout.write(` - ${rel}\n`);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
if (unchanged.length) {
|
|
170
|
+
process.stdout.write(`\nunchanged: ${unchanged.length} file(s)\n`);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
if (toWrite.length === 0) {
|
|
174
|
+
process.stdout.write(
|
|
175
|
+
toSkip.length ? "\nnothing to write.\n" : "\nalready up to date.\n",
|
|
176
|
+
);
|
|
177
|
+
return toSkip.length ? 1 : 0;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
process.stdout.write(
|
|
181
|
+
`\n${dryRun ? "would write" : "writing"} ${toWrite.length} file(s):\n`,
|
|
182
|
+
);
|
|
183
|
+
for (const op of toWrite) {
|
|
184
|
+
process.stdout.write(` ${op.kind === "overwrite" ? "*" : "+"} ${op.dst}\n`);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
if (dryRun) return 0;
|
|
188
|
+
|
|
189
|
+
for (const op of toWrite) {
|
|
190
|
+
await mkdir(dirname(op.dstAbs), { recursive: true });
|
|
191
|
+
await copyFile(op.srcAbs, op.dstAbs);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
process.stdout.write(`\ndone.\n`);
|
|
195
|
+
return toSkip.length ? 1 : 0;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// Only run main() when invoked as a script, not when imported by tests.
|
|
199
|
+
// realpath normalizes /tmp → /private/tmp on macOS so this works via npx too.
|
|
200
|
+
const invokedAs = process.argv[1]
|
|
201
|
+
? await realpath(process.argv[1]).catch(() => process.argv[1])
|
|
202
|
+
: "";
|
|
203
|
+
if (invokedAs === fileURLToPath(import.meta.url)) {
|
|
204
|
+
try {
|
|
205
|
+
process.exit((await main()) ?? 0);
|
|
206
|
+
} catch (err) {
|
|
207
|
+
process.stderr.write(`error: ${err.stack ?? err.message ?? err}\n`);
|
|
208
|
+
process.exit(1);
|
|
209
|
+
}
|
|
210
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@1dot5/design-assistant",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "Design assistant agents, skills, and conventions for Next.js 16 + React 19 projects",
|
|
5
5
|
"homepage": "https://github.com/1dot5/da#readme",
|
|
6
6
|
"bugs": {
|
|
@@ -12,15 +12,23 @@
|
|
|
12
12
|
},
|
|
13
13
|
"license": "MIT",
|
|
14
14
|
"type": "module",
|
|
15
|
+
"bin": {
|
|
16
|
+
"design-assistant": "./bin/cli.js"
|
|
17
|
+
},
|
|
15
18
|
"files": [
|
|
16
19
|
"AGENTS.md",
|
|
17
|
-
".
|
|
18
|
-
".claude/skills"
|
|
20
|
+
"README.md",
|
|
21
|
+
".claude/skills",
|
|
22
|
+
"bin"
|
|
19
23
|
],
|
|
20
24
|
"engines": {
|
|
21
25
|
"node": ">=20"
|
|
22
26
|
},
|
|
23
27
|
"publishConfig": {
|
|
24
28
|
"access": "public"
|
|
29
|
+
},
|
|
30
|
+
"scripts": {
|
|
31
|
+
"verify:aliases": "node scripts/verify-aliases.mjs",
|
|
32
|
+
"smoke": "rm -rf .smoke && node bin/cli.js .smoke && rm -rf .smoke"
|
|
25
33
|
}
|
|
26
34
|
}
|