@shinaps/skills 0.42.0 → 0.44.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 +1 -1
- package/dist/bin/skills.js +10 -7
- package/dist/lib/auto-update.js +4 -4
- package/dist/lib/config-validation.js +1 -1
- package/dist/lib/install.js +6 -5
- package/dist/lib/linear-config.js +1 -1
- package/dist/lib/lint-plan.js +173 -0
- package/dist/lib/lint-rules.js +52 -0
- package/dist/lib/lint.js +233 -0
- package/dist/lib/lock.js +1 -1
- package/dist/lib/managed-config.js +1 -1
- package/dist/lib/managed-session.js +3 -3
- package/dist/lib/managed-setup.js +4 -4
- package/dist/lib/managed.js +6 -6
- package/dist/lib/transaction.js +2 -2
- package/docs/development.md +1 -1
- package/docs/install.md +3 -3
- package/docs/lint.md +71 -0
- package/package.json +9 -5
- package/plugins/shinaps-claude/.claude-plugin/plugin.json +1 -1
- package/plugins/shinaps-claude/references/linear/configuration.md +1 -1
- package/plugins/shinaps-claude/references/lint.md +34 -0
- package/plugins/shinaps-claude/references/rule-writing.md +33 -1
- package/plugins/shinaps-claude/references/rules.md +15 -3
- package/plugins/shinaps-claude/skills/extract-rules/SKILL.md +6 -5
- package/plugins/shinaps-claude/skills/refine/SKILL.md +7 -3
- package/plugins/shinaps-codex/.codex-plugin/plugin.json +1 -1
- package/plugins/shinaps-codex/references/linear/configuration.md +1 -1
- package/plugins/shinaps-codex/references/lint.md +34 -0
- package/plugins/shinaps-codex/references/rule-writing.md +33 -1
- package/plugins/shinaps-codex/references/rules.md +15 -3
- package/plugins/shinaps-codex/skills/extract-rules/SKILL.md +3 -3
- package/plugins/shinaps-codex/skills/refine/SKILL.md +3 -1
package/README.md
CHANGED
|
@@ -8,7 +8,7 @@ Claude Code と Codex で使う、開発ワークフローのスキル集です
|
|
|
8
8
|
## 必要なもの
|
|
9
9
|
|
|
10
10
|
- Claude Code または Codex CLI
|
|
11
|
-
- Node.js 22 以上(Codex への導入と、設定の検証・Managed Agents
|
|
11
|
+
- Node.js 22.13 以上の 22 系、または 24 以上(Codex への導入と、設定の検証・Managed Agents の操作・規約の機械検査に使う[配布 CLI](docs/install.md#配布-cli)に必要。機械検査の ESLint の要件に合わせている)。配布 CLI は機械検査のために ESLint・typescript-eslint・TypeScript などを依存に持ち、導入に約 50MB を使います。
|
|
12
12
|
- 機能によって追加で必要なもの(Linear・Claude Managed Agents など)は、下の「機能別の資料」の各資料に書いています。
|
|
13
13
|
|
|
14
14
|
## インストール
|
package/dist/bin/skills.js
CHANGED
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { readFile } from 'node:fs/promises';
|
|
3
3
|
import { join } from 'node:path';
|
|
4
|
-
import { packageRoot } from
|
|
5
|
-
import { validateLinearConfig } from
|
|
6
|
-
import { validateManagedConfig } from
|
|
7
|
-
import { managed } from
|
|
8
|
-
import { run } from
|
|
9
|
-
import { autoUpdate } from
|
|
10
|
-
import {
|
|
4
|
+
import { packageRoot } from "../lib/layout.js";
|
|
5
|
+
import { validateLinearConfig } from "../lib/linear-config.js";
|
|
6
|
+
import { validateManagedConfig } from "../lib/managed-config.js";
|
|
7
|
+
import { managed } from "../lib/managed.js";
|
|
8
|
+
import { run } from "../lib/install.js";
|
|
9
|
+
import { autoUpdate } from "../lib/auto-update.js";
|
|
10
|
+
import { lint } from "../lib/lint.js";
|
|
11
|
+
import { errorMessage } from "../lib/fs.js";
|
|
11
12
|
const [command, ...rest] = process.argv.slice(2);
|
|
12
13
|
try {
|
|
13
14
|
if (command === '--version') {
|
|
@@ -29,6 +30,8 @@ try {
|
|
|
29
30
|
await managed(rest);
|
|
30
31
|
else if (command === 'auto-update')
|
|
31
32
|
await autoUpdate(rest);
|
|
33
|
+
else if (command === 'lint')
|
|
34
|
+
await lint(rest);
|
|
32
35
|
else
|
|
33
36
|
await run(process.argv.slice(2));
|
|
34
37
|
}
|
package/dist/lib/auto-update.js
CHANGED
|
@@ -2,10 +2,10 @@ import { mkdir, writeFile, rm } from 'node:fs/promises';
|
|
|
2
2
|
import { join, dirname } from 'node:path';
|
|
3
3
|
import { homedir } from 'node:os';
|
|
4
4
|
import { execFileSync, spawnSync } from 'node:child_process';
|
|
5
|
-
import { withLock } from
|
|
6
|
-
import { install, openInstallation, recover } from
|
|
7
|
-
import { errorMessage, readJson, readJsonIfExists } from
|
|
8
|
-
import { codexHomeMismatch, codexHomeOf, isRecordedCodexHome, managedRoot, manifestPath, payloadDir, statePath, } from
|
|
5
|
+
import { withLock } from "./lock.js";
|
|
6
|
+
import { install, openInstallation, recover } from "./install.js";
|
|
7
|
+
import { errorMessage, readJson, readJsonIfExists } from "./fs.js";
|
|
8
|
+
import { codexHomeMismatch, codexHomeOf, isRecordedCodexHome, managedRoot, manifestPath, payloadDir, statePath, } from "./layout.js";
|
|
9
9
|
const actions = ['enable', 'disable', 'status', 'check'];
|
|
10
10
|
const isAction = (value) => actions.includes(value);
|
|
11
11
|
const label = 'io.shinaps.skills.update';
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { readFile } from 'node:fs/promises';
|
|
2
2
|
import { join } from 'node:path';
|
|
3
3
|
import { Ajv } from 'ajv';
|
|
4
|
-
import { packageRoot } from
|
|
4
|
+
import { packageRoot } from "./layout.js";
|
|
5
5
|
/** 明示されたファイルを同梱の契約で検証し、検証した内容を返す。設定の探索・変更や外部取得は行わない。 */
|
|
6
6
|
export async function validateConfig(path, contract) {
|
|
7
7
|
const input = JSON.parse(await readFile(path, 'utf8'));
|
package/dist/lib/install.js
CHANGED
|
@@ -2,10 +2,10 @@ import { readFile, readdir, cp, rm, mkdir } from 'node:fs/promises';
|
|
|
2
2
|
import { homedir } from 'node:os';
|
|
3
3
|
import { join } from 'node:path';
|
|
4
4
|
import { spawnSync } from 'node:child_process';
|
|
5
|
-
import { digest, registerPlugin, replaceInstallation, restore, } from
|
|
6
|
-
import { withLock } from
|
|
7
|
-
import { readTextIfExists, writeFileAtomic } from
|
|
8
|
-
import { codexHomeMismatch, codexHomeOf, isRecordedCodexHome, managedAgent, managedRoot, marketplaceName, packageRoot, payloadDir, payloadPaths, pluginDir, pluginId, statePath, } from
|
|
5
|
+
import { digest, registerPlugin, replaceInstallation, restore, } from "./transaction.js";
|
|
6
|
+
import { withLock } from "./lock.js";
|
|
7
|
+
import { readTextIfExists, writeFileAtomic } from "./fs.js";
|
|
8
|
+
import { codexHomeMismatch, codexHomeOf, isRecordedCodexHome, managedAgent, managedRoot, marketplaceName, packageRoot, payloadDir, payloadPaths, pluginDir, pluginId, statePath, } from "./layout.js";
|
|
9
9
|
const actions = ['install', 'update', 'uninstall', 'recover'];
|
|
10
10
|
const isAction = (value) => actions.includes(value);
|
|
11
11
|
const usage = [
|
|
@@ -14,6 +14,7 @@ const usage = [
|
|
|
14
14
|
' shinaps-skills validate-managed-config <file>',
|
|
15
15
|
' shinaps-skills managed <setup|run|status|reply|collect> (see managed --help)',
|
|
16
16
|
' shinaps-skills auto-update <enable|disable|status|check> codex',
|
|
17
|
+
' shinaps-skills lint --checks <checks.json> --root <project> --out <result.json> -- <files...>',
|
|
17
18
|
'Claude Code: /plugin marketplace add sh1n4ps/shinaps-skills',
|
|
18
19
|
' /plugin install shinaps@shinaps-skills',
|
|
19
20
|
].join('\n');
|
|
@@ -124,7 +125,7 @@ async function uninstall(installation, options) {
|
|
|
124
125
|
return;
|
|
125
126
|
}
|
|
126
127
|
// auto-update.ts は check でこのモジュールを使う。相互 import を避けるため実行時に読み込む。
|
|
127
|
-
const { autoUpdate } = await import(
|
|
128
|
+
const { autoUpdate } = await import("./auto-update.js");
|
|
128
129
|
await autoUpdate(['disable', 'codex'], options);
|
|
129
130
|
await command(['plugin', 'remove', pluginId]);
|
|
130
131
|
await command(['plugin', 'marketplace', 'remove', marketplaceName]);
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `shinaps-skills lint` の入力・実行単位・結果の分類。ESLint を呼ばない純関数だけを置き、実行は lint.ts が行う。
|
|
3
|
+
*
|
|
4
|
+
* 規約 R × ファイル F の組を AI 照合から外してよいのは、次がすべて成り立つときだけ(docs/lint.md)。
|
|
5
|
+
* - R の検査の coverage が full(登録時の意味のレビューの結果。ここでは値を読むだけ)
|
|
6
|
+
* - その検査が F に適用されると ESLint が判定し、検査が属する実行単位が完了し、F に致命的な報告も
|
|
7
|
+
* ruleId の無い報告も無い
|
|
8
|
+
* どれかが欠けた組は not-run にして AI へ回す。
|
|
9
|
+
*/
|
|
10
|
+
/** 検査を引く key。規約 id と、その規約の checks の中の位置で決まる。 */
|
|
11
|
+
export const checkKey = (ruleId, index) => `${ruleId}#${index}`;
|
|
12
|
+
const isObject = (value) => typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
13
|
+
/**
|
|
14
|
+
* 入力 JSON を検査の一覧にする。形が違えば throw し、呼び出し側は結果を書かずに失敗する(全組を AI へ)。
|
|
15
|
+
* 形だけを見る。ルール ID・options・glob の中身は検査せず、ESLint が拒否すればその検査だけが not-run になる。
|
|
16
|
+
*/
|
|
17
|
+
export function parseLintInput(raw) {
|
|
18
|
+
if (!isObject(raw) || !Array.isArray(raw.rules))
|
|
19
|
+
throw new Error('checks: expected { "rules": [...] }');
|
|
20
|
+
return {
|
|
21
|
+
rules: raw.rules.map((rule, ruleIndex) => {
|
|
22
|
+
const where = `checks: rules[${ruleIndex}]`;
|
|
23
|
+
if (!isObject(rule) || typeof rule.id !== 'string' || rule.id === '')
|
|
24
|
+
throw new Error(`${where}: expected a non-empty "id"`);
|
|
25
|
+
if (!Array.isArray(rule.checks))
|
|
26
|
+
throw new Error(`${where}: expected "checks" to be an array`);
|
|
27
|
+
return {
|
|
28
|
+
id: rule.id,
|
|
29
|
+
checks: rule.checks.map((check, checkIndex) => parseCheck(check, `${where}.checks[${checkIndex}]`)),
|
|
30
|
+
};
|
|
31
|
+
}),
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
function parseCheck(raw, where) {
|
|
35
|
+
if (!isObject(raw))
|
|
36
|
+
throw new Error(`${where}: expected an object`);
|
|
37
|
+
const { rule, options, files, coverage } = raw;
|
|
38
|
+
if (typeof rule !== 'string' || rule === '')
|
|
39
|
+
throw new Error(`${where}: expected a non-empty "rule"`);
|
|
40
|
+
if (options !== undefined && !Array.isArray(options))
|
|
41
|
+
throw new Error(`${where}: expected "options" to be an array`);
|
|
42
|
+
if (!Array.isArray(files) ||
|
|
43
|
+
files.length === 0 ||
|
|
44
|
+
!files.every((file) => typeof file === 'string' && file !== ''))
|
|
45
|
+
throw new Error(`${where}: expected "files" to be a non-empty array of globs`);
|
|
46
|
+
if (coverage !== 'full' && coverage !== 'partial')
|
|
47
|
+
throw new Error(`${where}: expected "coverage" to be "full" or "partial"`);
|
|
48
|
+
return { rule, options: options ?? [], files, coverage };
|
|
49
|
+
}
|
|
50
|
+
/** キーの順を揃えた JSON。同じ options を同じ文字列にして、実行単位でまとめるために使う。 */
|
|
51
|
+
function canonicalJson(value) {
|
|
52
|
+
if (Array.isArray(value))
|
|
53
|
+
return `[${value.map(canonicalJson).join(',')}]`;
|
|
54
|
+
if (isObject(value))
|
|
55
|
+
return `{${Object.keys(value)
|
|
56
|
+
.sort()
|
|
57
|
+
.map((key) => `${JSON.stringify(key)}:${canonicalJson(value[key])}`)
|
|
58
|
+
.join(',')}}`;
|
|
59
|
+
return JSON.stringify(value);
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* 検査を実行単位に分ける。同じルール・同じ options の検査は 1 つの設定にまとめ、同じルールで options が
|
|
63
|
+
* 違うものだけを別の単位にする(flat config では同じルールの設定は後勝ちで 1 つしか残らないため)。
|
|
64
|
+
* 型が要るルールは projectService を付けた単位に分ける。型の得られないファイルはその単位だけが fatal になり、
|
|
65
|
+
* 型なしの検査を巻き込まない。
|
|
66
|
+
* @param needsTypes ルール ID が型情報を要するか。解決できたルールについてだけ呼ぶ
|
|
67
|
+
*/
|
|
68
|
+
export function planUnits(bindings, needsTypes) {
|
|
69
|
+
const groups = new Map();
|
|
70
|
+
for (const binding of bindings) {
|
|
71
|
+
const key = `${binding.check.rule}\0${canonicalJson(binding.check.options)}`;
|
|
72
|
+
const group = groups.get(key);
|
|
73
|
+
if (group)
|
|
74
|
+
group.bindings.push(binding);
|
|
75
|
+
else
|
|
76
|
+
groups.set(key, {
|
|
77
|
+
rule: binding.check.rule,
|
|
78
|
+
options: binding.check.options,
|
|
79
|
+
bindings: [binding],
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
const units = new Map();
|
|
83
|
+
const variants = new Map();
|
|
84
|
+
for (const group of groups.values()) {
|
|
85
|
+
const typed = needsTypes(group.rule);
|
|
86
|
+
const variant = variants.get(group.rule) ?? 0;
|
|
87
|
+
variants.set(group.rule, variant + 1);
|
|
88
|
+
const id = `${typed ? 'typed' : 'syntax'}-${variant}`;
|
|
89
|
+
const unit = units.get(id) ?? { id, typed, rules: [] };
|
|
90
|
+
unit.rules.push(group);
|
|
91
|
+
units.set(id, unit);
|
|
92
|
+
}
|
|
93
|
+
return [...units.values()];
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* 組を分類する。検査は checkKey で引く。
|
|
97
|
+
* @param applied 検査ごとに、ESLint が適用すると判定したファイル(root からの相対パス)
|
|
98
|
+
* @param outcomeOf 検査ごとの、その検査が属する実行単位の結果。unusable の検査は含まない
|
|
99
|
+
* @param unusable 実行しなかった検査とその理由(この版に無いルール、ESLint が拒否した設定)
|
|
100
|
+
*/
|
|
101
|
+
export function classify(input) {
|
|
102
|
+
const pairs = [];
|
|
103
|
+
for (const rule of input.rules) {
|
|
104
|
+
if (rule.checks.length === 0)
|
|
105
|
+
continue;
|
|
106
|
+
for (const file of input.files) {
|
|
107
|
+
const findings = [];
|
|
108
|
+
const reasons = [];
|
|
109
|
+
let fullRan = false;
|
|
110
|
+
let anyRan = false;
|
|
111
|
+
for (const [index, check] of rule.checks.entries()) {
|
|
112
|
+
const key = checkKey(rule.id, index);
|
|
113
|
+
const unusable = input.unusable.get(key);
|
|
114
|
+
if (unusable !== undefined) {
|
|
115
|
+
reasons.push(`${check.rule}: ${unusable}`);
|
|
116
|
+
continue;
|
|
117
|
+
}
|
|
118
|
+
if (!input.applied.get(key)?.has(file))
|
|
119
|
+
continue;
|
|
120
|
+
const outcome = input.outcomeOf.get(key);
|
|
121
|
+
if (outcome?.error !== undefined) {
|
|
122
|
+
reasons.push(`${check.rule}: ${outcome.error}`);
|
|
123
|
+
continue;
|
|
124
|
+
}
|
|
125
|
+
const report = outcome?.reports.get(file);
|
|
126
|
+
if (!report) {
|
|
127
|
+
reasons.push(`${check.rule}: no result for the file`);
|
|
128
|
+
continue;
|
|
129
|
+
}
|
|
130
|
+
const ruleless = report.messages.find((message) => message.ruleId === null);
|
|
131
|
+
if (report.fatal || ruleless) {
|
|
132
|
+
reasons.push(`${check.rule}: ${ruleless?.message ?? 'fatal error'}`);
|
|
133
|
+
continue;
|
|
134
|
+
}
|
|
135
|
+
anyRan = true;
|
|
136
|
+
if (check.coverage === 'full')
|
|
137
|
+
fullRan = true;
|
|
138
|
+
for (const message of report.messages) {
|
|
139
|
+
if (message.ruleId === check.rule)
|
|
140
|
+
findings.push({
|
|
141
|
+
rule: check.rule,
|
|
142
|
+
line: message.line,
|
|
143
|
+
column: message.column,
|
|
144
|
+
message: message.message,
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
const state = fullRan
|
|
149
|
+
? findings.length > 0
|
|
150
|
+
? 'violation'
|
|
151
|
+
: 'covered'
|
|
152
|
+
: anyRan
|
|
153
|
+
? 'partial'
|
|
154
|
+
: 'not-run';
|
|
155
|
+
const pair = { ruleId: rule.id, file, state, findings: dedupe(findings) };
|
|
156
|
+
if (state === 'not-run')
|
|
157
|
+
pair.reason = reasons.length > 0 ? reasons.join('; ') : 'no check applies to the file';
|
|
158
|
+
pairs.push(pair);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
return pairs;
|
|
162
|
+
}
|
|
163
|
+
// 同じルール・同じ options の検査を 1 規約が 2 つ持つと、同じ報告が 2 回集まるため 1 つにする。
|
|
164
|
+
function dedupe(findings) {
|
|
165
|
+
const seen = new Set();
|
|
166
|
+
return findings.filter((finding) => {
|
|
167
|
+
const key = `${finding.rule}:${finding.line}:${finding.column}:${finding.message}`;
|
|
168
|
+
if (seen.has(key))
|
|
169
|
+
return false;
|
|
170
|
+
seen.add(key);
|
|
171
|
+
return true;
|
|
172
|
+
});
|
|
173
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Promise の executor が受け取る resolve / reject を、executor の外から使うために executor 内で別の場所へ
|
|
3
|
+
* 代入する書き方(deferred promise)を報告する。`Promise.withResolvers()` で書ける。
|
|
4
|
+
* 代入先は変数・プロパティを問わない。resolve / reject を値として渡す呼び出し(`setResolve(resolve)`)や、
|
|
5
|
+
* 包んだ関数の代入(`done = () => resolve()`)は報告しない(意図の判定に構文以上の情報が要るため)。
|
|
6
|
+
*/
|
|
7
|
+
const preferPromiseWithResolvers = {
|
|
8
|
+
meta: {
|
|
9
|
+
type: 'suggestion',
|
|
10
|
+
docs: {
|
|
11
|
+
description: 'Create deferred promises with Promise.withResolvers instead of leaking the executor callbacks',
|
|
12
|
+
},
|
|
13
|
+
messages: {
|
|
14
|
+
leaked: "Promise executor's {{name}} is assigned to be used outside the executor. Use Promise.withResolvers().",
|
|
15
|
+
},
|
|
16
|
+
schema: [],
|
|
17
|
+
},
|
|
18
|
+
create(context) {
|
|
19
|
+
// 名前だけで判定すると、executor の内側の関数が同じ名前の引数を持つ場合を取り違える。
|
|
20
|
+
// 右辺を scope で解決し、それを定義した関数が、祖先の `new Promise(...)` の第 1 引数そのものかを確かめる。
|
|
21
|
+
return {
|
|
22
|
+
AssignmentExpression(node) {
|
|
23
|
+
if (node.right.type !== 'Identifier')
|
|
24
|
+
return;
|
|
25
|
+
const definition = findVariable(context.sourceCode.getScope(node), node.right.name)
|
|
26
|
+
?.defs[0];
|
|
27
|
+
if (definition?.type !== 'Parameter')
|
|
28
|
+
return;
|
|
29
|
+
const leaksExecutorParameter = context.sourceCode
|
|
30
|
+
.getAncestors(node)
|
|
31
|
+
.some((ancestor) => ancestor.type === 'NewExpression' &&
|
|
32
|
+
ancestor.callee.type === 'Identifier' &&
|
|
33
|
+
ancestor.callee.name === 'Promise' &&
|
|
34
|
+
ancestor.arguments[0] === definition.node);
|
|
35
|
+
if (leaksExecutorParameter)
|
|
36
|
+
context.report({ node, messageId: 'leaked', data: { name: node.right.name } });
|
|
37
|
+
},
|
|
38
|
+
};
|
|
39
|
+
},
|
|
40
|
+
};
|
|
41
|
+
function findVariable(scope, name) {
|
|
42
|
+
for (let current = scope; current; current = current.upper) {
|
|
43
|
+
const variable = current.set.get(name);
|
|
44
|
+
if (variable)
|
|
45
|
+
return variable;
|
|
46
|
+
}
|
|
47
|
+
return undefined;
|
|
48
|
+
}
|
|
49
|
+
export const shinapsPlugin = {
|
|
50
|
+
meta: { name: 'shinaps' },
|
|
51
|
+
rules: { 'prefer-promise-with-resolvers': preferPromiseWithResolvers },
|
|
52
|
+
};
|
package/dist/lib/lint.js
ADDED
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
import { readFile, writeFile } from 'node:fs/promises';
|
|
2
|
+
import { isAbsolute, relative, resolve, sep } from 'node:path';
|
|
3
|
+
import { checkKey, classify, parseLintInput, planUnits, } from "./lint-plan.js";
|
|
4
|
+
import { shinapsPlugin } from "./lint-rules.js";
|
|
5
|
+
import { errorMessage } from "./fs.js";
|
|
6
|
+
const USAGE = 'Usage: shinaps-skills lint --checks <checks.json> --root <project> --out <result.json> -- <files...>';
|
|
7
|
+
/**
|
|
8
|
+
* `shinaps-skills lint`: 規約の検査を、プロジェクトの設定を使わずに ESLint の Node API で実行し、
|
|
9
|
+
* 規約 × ファイルの組を分類して result.json に書く。プロジェクトの lint 設定・script・node_modules の
|
|
10
|
+
* パッケージは読み込まない(overrideConfigFile: true、プラグインはこのパッケージの依存から渡す)。
|
|
11
|
+
* 対象ファイルへは書き込まない。
|
|
12
|
+
*/
|
|
13
|
+
export async function lint(args) {
|
|
14
|
+
const options = parseArgs(args);
|
|
15
|
+
const input = parseLintInput(JSON.parse(await readFile(options.checks, 'utf8')));
|
|
16
|
+
const result = await runLint(input, options.root, options.files);
|
|
17
|
+
await writeFile(options.out, `${JSON.stringify(result, null, 2)}\n`);
|
|
18
|
+
const counts = Object.fromEntries(['covered', 'violation', 'partial', 'not-run'].map((state) => [
|
|
19
|
+
state,
|
|
20
|
+
result.pairs.filter((pair) => pair.state === state).length,
|
|
21
|
+
]));
|
|
22
|
+
console.log(`Shinaps lint: ${JSON.stringify(counts)} → ${options.out}`);
|
|
23
|
+
}
|
|
24
|
+
function parseArgs(args) {
|
|
25
|
+
const separator = args.indexOf('--');
|
|
26
|
+
const flags = separator === -1 ? args : args.slice(0, separator);
|
|
27
|
+
const files = separator === -1 ? [] : args.slice(separator + 1);
|
|
28
|
+
const value = (name) => {
|
|
29
|
+
const index = flags.indexOf(name);
|
|
30
|
+
const found = index === -1 ? undefined : flags[index + 1];
|
|
31
|
+
if (!found)
|
|
32
|
+
throw new Error(`${name} is required. ${USAGE}`);
|
|
33
|
+
return found;
|
|
34
|
+
};
|
|
35
|
+
return { checks: value('--checks'), root: resolve(value('--root')), out: value('--out'), files };
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* @param root 対象プロジェクトのルート(絶対パス)
|
|
39
|
+
* @param files 対象ファイル。root からの相対パスか絶対パス。結果では root からの相対パス(区切りは `/`)で表す
|
|
40
|
+
*/
|
|
41
|
+
export async function runLint(input, root, files) {
|
|
42
|
+
const { ESLint } = await import('eslint');
|
|
43
|
+
const registry = await loadRegistry();
|
|
44
|
+
const relativeFiles = [...new Set(files.map((file) => toRelative(root, file)))];
|
|
45
|
+
const newESLint = (overrideConfig) => new ESLint({
|
|
46
|
+
cwd: root,
|
|
47
|
+
overrideConfigFile: true,
|
|
48
|
+
overrideConfig,
|
|
49
|
+
allowInlineConfig: false,
|
|
50
|
+
// 削除済みなどで存在しないファイルを渡しても単位全体を失敗にせず、そのファイルを「結果なし」
|
|
51
|
+
// (not-run) にするため。
|
|
52
|
+
errorOnUnmatchedPattern: false,
|
|
53
|
+
// 渡すのはファイルだけ。`src/*.ts` のような名前のファイルを glob として展開し、渡していない
|
|
54
|
+
// ファイルまで検査しないため。
|
|
55
|
+
globInputPaths: false,
|
|
56
|
+
});
|
|
57
|
+
const needsTypes = (rule) =>
|
|
58
|
+
// ESLint の型の docs には requiresTypeChecking が無い (typescript-eslint が付ける独自の項目)。
|
|
59
|
+
registry.rules.get(rule)?.meta?.docs
|
|
60
|
+
?.requiresTypeChecking === true;
|
|
61
|
+
// 検査が F に適用されるかは ESLint だけが判定する。その検査の設定だけを入れたインスタンスで
|
|
62
|
+
// calculateConfigForFile を呼び、ルールが有効なら適用される。glob の解釈を CLI 側で再現しない。
|
|
63
|
+
// ESLint が設定(不正な options など)を拒否した検査は、ここで not-run にして実行単位に入れない。
|
|
64
|
+
// 単位に入れると、同じ単位の他の検査まで実行できなくなるため。
|
|
65
|
+
const unusable = new Map();
|
|
66
|
+
const applied = new Map();
|
|
67
|
+
const bindings = [];
|
|
68
|
+
for (const rule of input.rules) {
|
|
69
|
+
for (const [index, check] of rule.checks.entries()) {
|
|
70
|
+
const key = checkKey(rule.id, index);
|
|
71
|
+
if (!registry.rules.has(check.rule)) {
|
|
72
|
+
unusable.set(key, 'unsupported rule in this version of @shinaps/skills');
|
|
73
|
+
continue;
|
|
74
|
+
}
|
|
75
|
+
const binding = { ruleId: rule.id, index, check };
|
|
76
|
+
const single = newESLint(unitConfig(registry, root, needsTypes(check.rule), [
|
|
77
|
+
{ rule: check.rule, options: check.options, bindings: [binding] },
|
|
78
|
+
]));
|
|
79
|
+
try {
|
|
80
|
+
const hits = new Set();
|
|
81
|
+
for (const file of relativeFiles) {
|
|
82
|
+
if (isActive(await single.calculateConfigForFile(resolve(root, file)), check.rule))
|
|
83
|
+
hits.add(file);
|
|
84
|
+
}
|
|
85
|
+
applied.set(key, hits);
|
|
86
|
+
bindings.push(binding);
|
|
87
|
+
}
|
|
88
|
+
catch (error) {
|
|
89
|
+
unusable.set(key, oneLine(error));
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
const outcomeOf = new Map();
|
|
94
|
+
const units = [];
|
|
95
|
+
for (const unit of planUnits(bindings, needsTypes)) {
|
|
96
|
+
const unitBindings = unit.rules.flatMap((planned) => planned.bindings);
|
|
97
|
+
const targets = [
|
|
98
|
+
...new Set(unitBindings.flatMap((binding) => [
|
|
99
|
+
...(applied.get(checkKey(binding.ruleId, binding.index)) ?? []),
|
|
100
|
+
])),
|
|
101
|
+
];
|
|
102
|
+
const outcome = await runUnit(unit, targets);
|
|
103
|
+
for (const binding of unitBindings)
|
|
104
|
+
outcomeOf.set(checkKey(binding.ruleId, binding.index), outcome);
|
|
105
|
+
units.push({
|
|
106
|
+
id: unit.id,
|
|
107
|
+
typed: unit.typed,
|
|
108
|
+
rules: unit.rules.map((planned) => planned.rule),
|
|
109
|
+
// ESLint が結果を返したファイルの数。単位のエラーや存在しないファイルでは、渡した数より少なくなる。
|
|
110
|
+
fileCount: outcome.reports.size,
|
|
111
|
+
...(outcome.error === undefined ? {} : { error: outcome.error }),
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
async function runUnit(unit, targets) {
|
|
115
|
+
// 渡すファイルが無ければ実行しない。ファイルを省くと ESLint は cwd 配下を全部検査するため。
|
|
116
|
+
if (targets.length === 0)
|
|
117
|
+
return { reports: new Map() };
|
|
118
|
+
try {
|
|
119
|
+
const results = await newESLint(unitConfig(registry, root, unit.typed, unit.rules)).lintFiles(targets.map((file) => resolve(root, file)));
|
|
120
|
+
const reports = new Map();
|
|
121
|
+
for (const result of results) {
|
|
122
|
+
reports.set(toRelative(root, result.filePath), {
|
|
123
|
+
fatal: result.fatalErrorCount > 0,
|
|
124
|
+
messages: result.messages.map((message) => ({
|
|
125
|
+
ruleId: message.ruleId,
|
|
126
|
+
line: message.line,
|
|
127
|
+
column: message.column,
|
|
128
|
+
message: message.message,
|
|
129
|
+
})),
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
return { reports };
|
|
133
|
+
}
|
|
134
|
+
catch (error) {
|
|
135
|
+
return { error: oneLine(error), reports: new Map() };
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
return {
|
|
139
|
+
root,
|
|
140
|
+
units,
|
|
141
|
+
pairs: classify({ rules: input.rules, files: relativeFiles, applied, outcomeOf, unusable }),
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
// ESLint の設定エラーは複数行で、理由が 2 行目以降にあるため、1 行にまとめて結果に残す。
|
|
145
|
+
const oneLine = (error) => errorMessage(error)
|
|
146
|
+
.replace(/\s*\n\s*/g, ' ')
|
|
147
|
+
.trim();
|
|
148
|
+
function toRelative(root, file) {
|
|
149
|
+
const absolute = isAbsolute(file) ? file : resolve(root, file);
|
|
150
|
+
return relative(root, absolute).split(sep).join('/');
|
|
151
|
+
}
|
|
152
|
+
function isActive(config, rule) {
|
|
153
|
+
const entry = config?.rules?.[rule];
|
|
154
|
+
const severity = Array.isArray(entry) ? entry[0] : entry;
|
|
155
|
+
return severity !== undefined && severity !== 0 && severity !== 'off';
|
|
156
|
+
}
|
|
157
|
+
async function loadRegistry() {
|
|
158
|
+
const [{ builtinRules }, tseslint, vue, vueParser] = await Promise.all([
|
|
159
|
+
import('eslint/use-at-your-own-risk'),
|
|
160
|
+
import('typescript-eslint'),
|
|
161
|
+
import('eslint-plugin-vue'),
|
|
162
|
+
import('vue-eslint-parser'),
|
|
163
|
+
]);
|
|
164
|
+
// typescript-eslint と eslint-plugin-vue が公開する型は ESLint の Plugin・Parser・RuleModule の型と
|
|
165
|
+
// 細部が一致しない (どちらも実行時は ESLint の API に従う)。flat config に渡すために ESLint の型にする。
|
|
166
|
+
const plugins = {
|
|
167
|
+
'@typescript-eslint': tseslint.default.plugin,
|
|
168
|
+
vue: vue.default,
|
|
169
|
+
shinaps: shinapsPlugin,
|
|
170
|
+
};
|
|
171
|
+
const rules = new Map(builtinRules);
|
|
172
|
+
for (const [prefix, plugin] of Object.entries(plugins)) {
|
|
173
|
+
for (const [name, rule] of Object.entries(plugin.rules ?? {}))
|
|
174
|
+
rules.set(`${prefix}/${name}`, rule);
|
|
175
|
+
}
|
|
176
|
+
return {
|
|
177
|
+
plugins,
|
|
178
|
+
rules,
|
|
179
|
+
parsers: {
|
|
180
|
+
typescript: tseslint.default.parser,
|
|
181
|
+
vue: vueParser.default,
|
|
182
|
+
},
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
const TS_FILES = ['**/*.ts', '**/*.tsx', '**/*.mts', '**/*.cts'];
|
|
186
|
+
// 検査の対象にする拡張子。ESLint は `src/**` のように全てのファイルに一致する glob だけではファイルを検査の
|
|
187
|
+
// 対象にしないので、拡張子を指定したパターンを両方の単位に置く。単位の種類で適用範囲が変わらないよう、
|
|
188
|
+
// 型付き・型なしで同じ一覧を使う。
|
|
189
|
+
const LINTED_FILES = [...TS_FILES, '**/*.js', '**/*.jsx', '**/*.mjs', '**/*.cjs', '**/*.vue'];
|
|
190
|
+
/**
|
|
191
|
+
* 実行単位の設定。parser は拡張子ごとの設定オブジェクトで指定し、ルールは検査の glob の和に付ける。
|
|
192
|
+
* 型付きの単位は、対象の全てのファイルを projectService 付きの typescript-eslint で読む。型の得られない
|
|
193
|
+
* ファイル (tsconfig に含まれない JS、Vue など) はそのファイルだけが fatal (not-run) になる。JS を ESLint
|
|
194
|
+
* の既定の parser で読むと、型を要求するルールが単位全体を例外で止め、他の検査を巻き込むため。
|
|
195
|
+
* 型付きの単位では Vue を扱わない (vue-eslint-parser と projectService の組み合わせは使っていない)。
|
|
196
|
+
*/
|
|
197
|
+
function unitConfig(registry, root, typed, rules) {
|
|
198
|
+
const parsers = typed
|
|
199
|
+
? [
|
|
200
|
+
{
|
|
201
|
+
files: LINTED_FILES,
|
|
202
|
+
languageOptions: {
|
|
203
|
+
parser: registry.parsers.typescript,
|
|
204
|
+
parserOptions: { projectService: true, tsconfigRootDir: root },
|
|
205
|
+
},
|
|
206
|
+
},
|
|
207
|
+
]
|
|
208
|
+
: [
|
|
209
|
+
{ files: LINTED_FILES },
|
|
210
|
+
// 既定の parser は JSX を読まないので、.jsx では JSX を有効にする (無いと常に parse error で not-run)。
|
|
211
|
+
{
|
|
212
|
+
files: ['**/*.jsx'],
|
|
213
|
+
languageOptions: { parserOptions: { ecmaFeatures: { jsx: true } } },
|
|
214
|
+
},
|
|
215
|
+
{ files: TS_FILES, languageOptions: { parser: registry.parsers.typescript } },
|
|
216
|
+
{
|
|
217
|
+
files: ['**/*.vue'],
|
|
218
|
+
languageOptions: {
|
|
219
|
+
parser: registry.parsers.vue,
|
|
220
|
+
parserOptions: { parser: registry.parsers.typescript, sourceType: 'module' },
|
|
221
|
+
},
|
|
222
|
+
},
|
|
223
|
+
];
|
|
224
|
+
return [
|
|
225
|
+
...parsers,
|
|
226
|
+
...rules.map((planned) => ({
|
|
227
|
+
files: [...new Set(planned.bindings.flatMap((binding) => binding.check.files))],
|
|
228
|
+
plugins: registry.plugins,
|
|
229
|
+
// options は JSON の配列 (unknown[]) で、ESLint の RuleEntry は要素の型を持たないので型だけを合わせる。
|
|
230
|
+
rules: { [planned.rule]: ['error', ...planned.options] },
|
|
231
|
+
})),
|
|
232
|
+
];
|
|
233
|
+
}
|
package/dist/lib/lock.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { mkdir, readFile, writeFile, rm } from 'node:fs/promises';
|
|
2
2
|
import { join } from 'node:path';
|
|
3
|
-
import { errorCode } from
|
|
3
|
+
import { errorCode } from "./fs.js";
|
|
4
4
|
/**
|
|
5
5
|
* `<directory>/update.lock` を保持したまま `work` を実行する。ロックはディレクトリで、保持者の pid を
|
|
6
6
|
* `pid` ファイルに記録する。保持者が存在しないロックは置き換える。保持者が生きているか pid をまだ読め
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { createHash, randomUUID } from 'node:crypto';
|
|
2
2
|
import { mkdir, readdir, rename, rm, stat } from 'node:fs/promises';
|
|
3
3
|
import { join, resolve } from 'node:path';
|
|
4
|
-
import { readJson, readJsonIfExists, writeFileAtomic } from
|
|
5
|
-
import { withLock } from
|
|
6
|
-
import { filesBeta, managedBeta } from
|
|
4
|
+
import { readJson, readJsonIfExists, writeFileAtomic } from "./fs.js";
|
|
5
|
+
import { withLock } from "./lock.js";
|
|
6
|
+
import { filesBeta, managedBeta } from "./managed-ant.js";
|
|
7
7
|
export const staffings = ['LITE', 'BASIC', 'ULTRA'];
|
|
8
8
|
export const permissions = ['commit', 'push', 'draft_pr'];
|
|
9
9
|
const leadKey = './agents/shinaps-dev-managed-cc.md';
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import { createHash } from 'node:crypto';
|
|
2
2
|
import { lstat, mkdir, readdir, readFile, rm } from 'node:fs/promises';
|
|
3
3
|
import { dirname, join, relative, sep } from 'node:path';
|
|
4
|
-
import { errorCode, readJsonIfExists, readTextIfExists, writeFileAtomic } from
|
|
5
|
-
import { withLock } from
|
|
6
|
-
import { managedTemplatePath } from
|
|
7
|
-
import { managedBeta } from
|
|
4
|
+
import { errorCode, readJsonIfExists, readTextIfExists, writeFileAtomic } from "./fs.js";
|
|
5
|
+
import { withLock } from "./lock.js";
|
|
6
|
+
import { managedTemplatePath } from "./layout.js";
|
|
7
|
+
import { managedBeta } from "./managed-ant.js";
|
|
8
8
|
/** 共通定義の 3 ディレクトリ。`definitions/` には、これと `claude-lock.json` 以外を置かない。 */
|
|
9
9
|
const definitionDirs = ['agents', 'environments', 'skills'];
|
|
10
10
|
const lockName = 'claude-lock.json';
|
package/dist/lib/managed.js
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import { readFile } from 'node:fs/promises';
|
|
2
2
|
import { homedir } from 'node:os';
|
|
3
|
-
import { errorMessage } from
|
|
4
|
-
import { managedAccountDir, managedRoot, packageRoot } from
|
|
5
|
-
import { antCommand } from
|
|
6
|
-
import { validateManagedConfig } from
|
|
7
|
-
import { collectOutputs, permissions, replySession, runSession, sessionStatus, staffings, } from
|
|
8
|
-
import { setupAccount } from
|
|
3
|
+
import { errorMessage } from "./fs.js";
|
|
4
|
+
import { managedAccountDir, managedRoot, packageRoot } from "./layout.js";
|
|
5
|
+
import { antCommand } from "./managed-ant.js";
|
|
6
|
+
import { validateManagedConfig } from "./managed-config.js";
|
|
7
|
+
import { collectOutputs, permissions, replySession, runSession, sessionStatus, staffings, } from "./managed-session.js";
|
|
8
|
+
import { setupAccount } from "./managed-setup.js";
|
|
9
9
|
const managedUsage = [
|
|
10
10
|
'Usage: shinaps-skills managed setup --account <name> [--account <name> ...] [--apply]',
|
|
11
11
|
' shinaps-skills managed run <managed.json> --account <name> --base <branch> --base-sha <sha> --head <branch> --staffing <LITE|BASIC|ULTRA> --allow <none|commit,push,draft_pr> --budget-cents <n> --task-file <file> [--verify <command>] [--author "<name> <email>"] [--title <title>] [--claude-md <file>]',
|
package/dist/lib/transaction.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { mkdir, rm, rename, cp } from 'node:fs/promises';
|
|
2
2
|
import { createHash } from 'node:crypto';
|
|
3
3
|
import { join } from 'node:path';
|
|
4
|
-
import { managedAgent, marketplaceName, payloadDir, pluginId, statePath } from
|
|
5
|
-
import { errorCode, errorMessage, readJsonIfExists, readTextIfExists, writeFileAtomic, } from
|
|
4
|
+
import { managedAgent, marketplaceName, payloadDir, pluginId, statePath } from "./layout.js";
|
|
5
|
+
import { errorCode, errorMessage, readJsonIfExists, readTextIfExists, writeFileAtomic, } from "./fs.js";
|
|
6
6
|
/** エージェント内容の sha256。state.json と journal の `nextHashes` はこの形式で記録する。 */
|
|
7
7
|
export const digest = (text) => createHash('sha256').update(text).digest('hex');
|
|
8
8
|
/** 配布物を Codex に登録する。install と restore で同じ手順を使う。 */
|