@shinaps/skills 0.11.0 → 0.12.1
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/.agents/plugins/marketplace.json +8 -6
- package/README.md +52 -6
- package/dist/bin/skills.js +15 -0
- package/dist/lib/auto-update.js +245 -0
- package/dist/lib/fs.js +49 -0
- package/dist/lib/install.js +204 -0
- package/dist/lib/layout.js +50 -0
- package/dist/lib/lock.js +109 -0
- package/dist/lib/transaction.js +115 -0
- package/package.json +35 -7
- package/plugins/{shinaps → shinaps-claude}/.claude-plugin/plugin.json +1 -1
- package/plugins/{shinaps → shinaps-claude}/skills/commit/SKILL.md +9 -2
- package/plugins/shinaps-codex/.codex-plugin/plugin.json +1 -1
- package/plugins/shinaps-codex/skills/commit/SKILL.md +9 -2
- package/bin/skills.mjs +0 -9
- package/lib/install.mjs +0 -116
- /package/plugins/{shinaps → shinaps-claude}/agents/shinaps-architect.md +0 -0
- /package/plugins/{shinaps → shinaps-claude}/agents/shinaps-debug-validator.md +0 -0
- /package/plugins/{shinaps → shinaps-claude}/agents/shinaps-debugger.md +0 -0
- /package/plugins/{shinaps → shinaps-claude}/agents/shinaps-explorer.md +0 -0
- /package/plugins/{shinaps → shinaps-claude}/agents/shinaps-investigator.md +0 -0
- /package/plugins/{shinaps → shinaps-claude}/agents/shinaps-plan-reviewer.md +0 -0
- /package/plugins/{shinaps → shinaps-claude}/agents/shinaps-refine-agent.md +0 -0
- /package/plugins/{shinaps → shinaps-claude}/agents/shinaps-review-validator.md +0 -0
- /package/plugins/{shinaps → shinaps-claude}/agents/shinaps-reviewer.md +0 -0
- /package/plugins/{shinaps → shinaps-claude}/agents/shinaps-spec-writer.md +0 -0
- /package/plugins/{shinaps → shinaps-claude}/agents/shinaps-split-reviewer.md +0 -0
- /package/plugins/{shinaps → shinaps-claude}/agents/shinaps-survey-validator.md +0 -0
- /package/plugins/{shinaps → shinaps-claude}/agents/shinaps-tech-surveyor.md +0 -0
- /package/plugins/{shinaps → shinaps-claude}/skills/create-pr/SKILL.md +0 -0
- /package/plugins/{shinaps → shinaps-claude}/skills/debug/SKILL.md +0 -0
- /package/plugins/{shinaps → shinaps-claude}/skills/dev/SKILL.md +0 -0
- /package/plugins/{shinaps → shinaps-claude}/skills/explore/SKILL.md +0 -0
- /package/plugins/{shinaps → shinaps-claude}/skills/explore/agents/openai.yaml +0 -0
- /package/plugins/{shinaps → shinaps-claude}/skills/extract-rules/SKILL.md +0 -0
- /package/plugins/{shinaps → shinaps-claude}/skills/refine/SKILL.md +0 -0
- /package/plugins/{shinaps → shinaps-claude}/skills/release/SKILL.md +0 -0
- /package/plugins/{shinaps → shinaps-claude}/skills/review/SKILL.md +0 -0
- /package/plugins/{shinaps → shinaps-claude}/skills/simplify/SKILL.md +0 -0
- /package/plugins/{shinaps → shinaps-claude}/skills/spec/SKILL.md +0 -0
- /package/plugins/{shinaps → shinaps-claude}/skills/verify-ui/SKILL.md +0 -0
- /package/{codex → plugins/shinaps-codex}/agents/shinaps-architect.toml +0 -0
- /package/{codex → plugins/shinaps-codex}/agents/shinaps-debug-validator.toml +0 -0
- /package/{codex → plugins/shinaps-codex}/agents/shinaps-debugger.toml +0 -0
- /package/{codex → plugins/shinaps-codex}/agents/shinaps-explorer.toml +0 -0
- /package/{codex → plugins/shinaps-codex}/agents/shinaps-investigator.toml +0 -0
- /package/{codex → plugins/shinaps-codex}/agents/shinaps-plan-reviewer.toml +0 -0
- /package/{codex → plugins/shinaps-codex}/agents/shinaps-refine-agent.toml +0 -0
- /package/{codex → plugins/shinaps-codex}/agents/shinaps-review-validator.toml +0 -0
- /package/{codex → plugins/shinaps-codex}/agents/shinaps-reviewer.toml +0 -0
- /package/{codex → plugins/shinaps-codex}/agents/shinaps-spec-writer.toml +0 -0
- /package/{codex → plugins/shinaps-codex}/agents/shinaps-split-reviewer.toml +0 -0
- /package/{codex → plugins/shinaps-codex}/agents/shinaps-survey-validator.toml +0 -0
- /package/{codex → plugins/shinaps-codex}/agents/shinaps-tech-surveyor.toml +0 -0
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { existsSync } from 'node:fs';
|
|
2
|
+
import { join, dirname, resolve } from 'node:path';
|
|
3
|
+
import { fileURLToPath } from 'node:url';
|
|
4
|
+
// ファイルの配置と形式。
|
|
5
|
+
// launchd ジョブは enable した版の CLI ではなく、実行のたびに npm から取得した最新版の CLI を起動する。
|
|
6
|
+
// そのためここに定義した場所と形式 (auto-update.json、state.json、transaction/journal.json) は
|
|
7
|
+
// どの版でも読めるように保ち、変える場合は旧形式も読めるようにする。
|
|
8
|
+
/**
|
|
9
|
+
* このパッケージのルートディレクトリ。リポジトリの checkout、または npm が展開したディレクトリ。
|
|
10
|
+
* このファイルは checkout では `lib/`、配布物では `dist/lib/` にあり深さが違うので、package.json が
|
|
11
|
+
* ある最も近い親ディレクトリを探す。
|
|
12
|
+
*/
|
|
13
|
+
export const packageRoot = findPackageRoot(dirname(fileURLToPath(import.meta.url)));
|
|
14
|
+
function findPackageRoot(directory) {
|
|
15
|
+
if (existsSync(join(directory, 'package.json')))
|
|
16
|
+
return directory;
|
|
17
|
+
const parent = dirname(directory);
|
|
18
|
+
if (parent === directory)
|
|
19
|
+
throw new Error(`package.json not found above ${directory}`);
|
|
20
|
+
return findPackageRoot(parent);
|
|
21
|
+
}
|
|
22
|
+
/** Codex plugin のディレクトリ。パッケージルートからも配置済み配布物からも同じ相対パス。 */
|
|
23
|
+
export const pluginDir = 'plugins/shinaps-codex';
|
|
24
|
+
export const manifestPath = `${pluginDir}/.codex-plugin/plugin.json`;
|
|
25
|
+
/** Codex の marketplace 定義。 */
|
|
26
|
+
const marketplacePath = '.agents/plugins/marketplace.json';
|
|
27
|
+
/** 配布物としてパッケージからコピーするパス。 */
|
|
28
|
+
export const payloadPaths = [pluginDir, marketplacePath];
|
|
29
|
+
/** marketplace.json の name と、その中の plugin の name。 */
|
|
30
|
+
export const marketplaceName = 'shinaps-skills-codex';
|
|
31
|
+
export const pluginId = `shinaps@${marketplaceName}`;
|
|
32
|
+
/**
|
|
33
|
+
* インストーラが扱えるエージェントのファイル名。一致しないファイルは置き換えも削除もしない。一致しても
|
|
34
|
+
* 所有の証明にはならず、実際に置き換え・削除するのは state.json に記録した内容と一致するファイルだけ。
|
|
35
|
+
*/
|
|
36
|
+
export const managedAgent = /^shinaps-[a-z0-9-]+\.toml$/;
|
|
37
|
+
/** 管理ディレクトリ。配布物、state.json、自動更新の設定とロックを置く。 */
|
|
38
|
+
export const managedRoot = (home) => join(home, '.local', 'share', 'shinaps-ai', 'skills');
|
|
39
|
+
/** 配置済みの配布物。Codex の marketplace はここを指す。 */
|
|
40
|
+
export const payloadDir = (root) => join(root, 'codex');
|
|
41
|
+
export const statePath = (root) => join(root, 'state.json');
|
|
42
|
+
/** CODEX_HOME の解決値。オプション (`auto-update check` が渡す) → 環境変数 → Codex の既定の順。 */
|
|
43
|
+
export const codexHomeOf = (options, home) => resolve(options.codexHome ?? process.env.CODEX_HOME ?? join(home, '.codex'));
|
|
44
|
+
/**
|
|
45
|
+
* 記録された CODEX_HOME が今回の導入先と同じか。旧版 (0.11.0) は指定された値を resolve せずに
|
|
46
|
+
* 保存していたので、記録側を resolve して比べる。
|
|
47
|
+
*/
|
|
48
|
+
export const isRecordedCodexHome = (recorded, current) => resolve(recorded) === current;
|
|
49
|
+
/** CODEX_HOME 不一致のメッセージの共通部分。続けて対処を書く。 */
|
|
50
|
+
export const codexHomeMismatch = (recorded, current) => `Shinaps is installed in CODEX_HOME=${recorded}, but this run uses ${current}.`;
|
package/dist/lib/lock.js
ADDED
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import { mkdir, readFile, writeFile, rm } from 'node:fs/promises';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import { errorCode } from './fs.js';
|
|
4
|
+
/**
|
|
5
|
+
* `<directory>/update.lock` を保持したまま `work` を実行する。ロックはディレクトリで、保持者の pid を
|
|
6
|
+
* `pid` ファイルに記録する。保持者が存在しないロックは置き換える。保持者が生きているか pid をまだ読め
|
|
7
|
+
* ないロックは "Another Shinaps update is running" で、別の処理が古いロックの判定中なら
|
|
8
|
+
* "Another Shinaps update is recovering its lock" で reject する。
|
|
9
|
+
* 入れ子にできるのは別のディレクトリ同士だけ。scheduler ロックの中で installation ロックを取る
|
|
10
|
+
* (`auto-update check`) 場合と、その逆 (uninstall) の両方がある。保持中のロックは待たずに即失敗する
|
|
11
|
+
* ので、どちらの向きでもデッドロックしない。
|
|
12
|
+
* @param directory ロックの親ディレクトリ。無ければ作る。古いロックの置き換え中は `lock-recovery`
|
|
13
|
+
* もここに置く。
|
|
14
|
+
* @param work ロック保持中に実行する処理。戻り値をそのまま返す。
|
|
15
|
+
*/
|
|
16
|
+
export async function withLock(directory, work) {
|
|
17
|
+
await mkdir(directory, { recursive: true });
|
|
18
|
+
const lock = join(directory, 'update.lock');
|
|
19
|
+
try {
|
|
20
|
+
await mkdir(lock);
|
|
21
|
+
}
|
|
22
|
+
catch (error) {
|
|
23
|
+
if (errorCode(error) !== 'EEXIST')
|
|
24
|
+
throw error;
|
|
25
|
+
await replaceStaleLock(directory, lock);
|
|
26
|
+
}
|
|
27
|
+
try {
|
|
28
|
+
await writeFile(join(lock, 'pid'), String(process.pid));
|
|
29
|
+
return await work();
|
|
30
|
+
}
|
|
31
|
+
finally {
|
|
32
|
+
await rm(lock, { recursive: true, force: true });
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
// 古いロックの置き換えは、lock-recovery ディレクトリを作れた 1 プロセスだけが行う。複数のプロセスが
|
|
36
|
+
// 同時に置き換えると、別のプロセスが取得した直後のロックを削除してしまうため。lock-recovery はこの
|
|
37
|
+
// 関数の中だけで保持し、work の前に削除する。この間に強制終了されると lock-recovery が残り、自動では
|
|
38
|
+
// 削除されないので、エラーメッセージにパスを含めて手で削除できるようにする。
|
|
39
|
+
async function replaceStaleLock(directory, lock) {
|
|
40
|
+
const recovery = join(directory, 'lock-recovery');
|
|
41
|
+
try {
|
|
42
|
+
await mkdir(recovery);
|
|
43
|
+
}
|
|
44
|
+
catch (error) {
|
|
45
|
+
if (errorCode(error) !== 'EEXIST')
|
|
46
|
+
throw error;
|
|
47
|
+
throw new Error(`Another Shinaps update is recovering its lock: ${recovery}. Retry later; remove that directory only after confirming no update is running.`);
|
|
48
|
+
}
|
|
49
|
+
try {
|
|
50
|
+
const pidPath = join(lock, 'pid');
|
|
51
|
+
const holder = await readLockPid(pidPath);
|
|
52
|
+
if (holderMayBeAlive(holder))
|
|
53
|
+
throw lockBusyError(lock);
|
|
54
|
+
// pid を読んでから生存を確認するまでの間に、保持者が終了してロックを解放し、別のプロセスが取得
|
|
55
|
+
// している可能性がある。そのため pid を読み直し、同じ終了済みの pid のままのときだけ古いロック
|
|
56
|
+
// として削除する。読み直しから削除までの間は、保持者が終了済みで、ほかの置き換え処理も
|
|
57
|
+
// lock-recovery で止まるため、ロックが変わることはない。
|
|
58
|
+
if ((await readLockPid(pidPath)) !== holder)
|
|
59
|
+
throw lockBusyError(lock);
|
|
60
|
+
await rm(lock, { recursive: true, force: true });
|
|
61
|
+
try {
|
|
62
|
+
await mkdir(lock);
|
|
63
|
+
}
|
|
64
|
+
catch (error) {
|
|
65
|
+
if (errorCode(error) !== 'EEXIST')
|
|
66
|
+
throw error;
|
|
67
|
+
// 上の rm と mkdir の間に別のプロセスが通常の経路で取得した。
|
|
68
|
+
throw lockBusyError(lock);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
finally {
|
|
72
|
+
await rm(recovery, { recursive: true, force: true });
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
// pid ファイルの本文。保持者がまだ書いていなければ null。
|
|
76
|
+
async function readLockPid(pidPath) {
|
|
77
|
+
try {
|
|
78
|
+
return await readFile(pidPath, 'utf8');
|
|
79
|
+
}
|
|
80
|
+
catch (error) {
|
|
81
|
+
if (errorCode(error) !== 'ENOENT')
|
|
82
|
+
throw error;
|
|
83
|
+
return null;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
// pid ファイルが無い、空または数値でない、別ユーザーのプロセス (EPERM)、のいずれも実行中として扱う。
|
|
87
|
+
// 保持者が mkdir と pid の書き込みの間にいる場合と、その間に強制終了された場合を区別できないため。
|
|
88
|
+
// ESRCH のときだけ保持者がいないとみなす。強制終了で残ったロックは手で削除する (エラーメッセージに
|
|
89
|
+
// パスを含める)。
|
|
90
|
+
function holderMayBeAlive(text) {
|
|
91
|
+
if (text === null)
|
|
92
|
+
return true;
|
|
93
|
+
const pid = Number(text);
|
|
94
|
+
if (!Number.isSafeInteger(pid) || pid <= 0)
|
|
95
|
+
return true;
|
|
96
|
+
try {
|
|
97
|
+
process.kill(pid, 0);
|
|
98
|
+
return true;
|
|
99
|
+
}
|
|
100
|
+
catch (error) {
|
|
101
|
+
const code = errorCode(error);
|
|
102
|
+
if (code === 'ESRCH')
|
|
103
|
+
return false;
|
|
104
|
+
if (code === 'EPERM')
|
|
105
|
+
return true;
|
|
106
|
+
throw error;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
const lockBusyError = (lock) => new Error(`Another Shinaps update is running: ${lock}. Retry after it finishes; remove the lock only after confirming no update is running.`);
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import { mkdir, rm, rename, cp } from 'node:fs/promises';
|
|
2
|
+
import { createHash } from 'node:crypto';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
import { managedAgent, marketplaceName, payloadDir, pluginId, statePath } from './layout.js';
|
|
5
|
+
import { errorCode, errorMessage, readJsonIfExists, readTextIfExists, writeFileAtomic, } from './fs.js';
|
|
6
|
+
/** エージェント内容の sha256。state.json と journal の `nextHashes` はこの形式で記録する。 */
|
|
7
|
+
export const digest = (text) => createHash('sha256').update(text).digest('hex');
|
|
8
|
+
/** 配布物を Codex に登録する。install と restore で同じ手順を使う。 */
|
|
9
|
+
export async function registerPlugin(command, payload) {
|
|
10
|
+
await command(['plugin', 'marketplace', 'add', payload]);
|
|
11
|
+
await command(['plugin', 'add', pluginId]);
|
|
12
|
+
}
|
|
13
|
+
const journalDir = (root) => join(root, 'transaction');
|
|
14
|
+
/**
|
|
15
|
+
* 配置済みの配布物を `preparedDir` で置き換えて `apply` を実行する。両方を取り消せるだけの記録を
|
|
16
|
+
* `<root>/transaction` に残す。失敗したら `restore` を実行し、元のエラーを再 throw する。restore も
|
|
17
|
+
* 失敗した場合は両方のエラーを持つ AggregateError を throw し、バックアップは `<root>/transaction`
|
|
18
|
+
* に残る。次回の install / update / uninstall / recover が、開始時に restore をやり直す。
|
|
19
|
+
* @param target 更新の対象。
|
|
20
|
+
* @param journal 変更前の記録。`hadPayload` を付けて journal.json として保存する。
|
|
21
|
+
* @param preparedDir 準備済みの配布物。`<root>/codex` と同じファイルシステムにあること。rename で
|
|
22
|
+
* 配置するので成功後は存在しない。
|
|
23
|
+
* @param apply 配布物を置いたあとに行う処理。Codex への登録、エージェントの書き込み、state.json。
|
|
24
|
+
*/
|
|
25
|
+
export async function replaceInstallation(target, journal, preparedDir, apply) {
|
|
26
|
+
const dir = journalDir(target.root);
|
|
27
|
+
const payload = payloadDir(target.root);
|
|
28
|
+
await rm(dir, { recursive: true, force: true });
|
|
29
|
+
await mkdir(dir);
|
|
30
|
+
let hadPayload = true;
|
|
31
|
+
try {
|
|
32
|
+
await cp(payload, join(dir, 'payload'), { recursive: true });
|
|
33
|
+
}
|
|
34
|
+
catch (error) {
|
|
35
|
+
if (errorCode(error) !== 'ENOENT')
|
|
36
|
+
throw error;
|
|
37
|
+
hadPayload = false;
|
|
38
|
+
}
|
|
39
|
+
// 一時ファイルに書いてから rename するので、journal.json が書きかけの内容で読まれることはない。
|
|
40
|
+
// fsync はしていないため、電源断のあとは journal が残らず、配布物やエージェントが更新途中のまま残る
|
|
41
|
+
// ことがある。
|
|
42
|
+
const saved = { ...journal, hadPayload };
|
|
43
|
+
await writeFileAtomic(join(dir, 'journal.json'), JSON.stringify(saved));
|
|
44
|
+
try {
|
|
45
|
+
await rm(payload, { recursive: true, force: true });
|
|
46
|
+
await rename(preparedDir, payload);
|
|
47
|
+
await apply();
|
|
48
|
+
}
|
|
49
|
+
catch (error) {
|
|
50
|
+
try {
|
|
51
|
+
await restore(target);
|
|
52
|
+
}
|
|
53
|
+
catch (recovery) {
|
|
54
|
+
throw new AggregateError([error, recovery], `${errorMessage(error)}; recovery incomplete: ${errorMessage(recovery)}. Backup retained in ${dir}; run "shinaps-skills recover codex" after fixing the cause.`, { cause: error });
|
|
55
|
+
}
|
|
56
|
+
throw error;
|
|
57
|
+
}
|
|
58
|
+
await removeJournal(dir);
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* `replaceInstallation` が書いた `<root>/transaction/journal.json` の内容で、中断した install / update
|
|
62
|
+
* の前の状態に戻す。journal が無ければ何もせず返る。journal が別の CODEX_HOME のものか、中断後に
|
|
63
|
+
* エージェントファイルが編集されている (旧内容でも新内容でもない) 場合は、何も変更せずに throw する。
|
|
64
|
+
* journal が消えるまで何度でも繰り返せる。journal の削除が最後の手順で、完了の印になる。
|
|
65
|
+
* @param target 更新の対象。`command` は、Codex が plugin を独自のキャッシュにも保持しているため、
|
|
66
|
+
* 戻した配布物を登録し直すのに使う。
|
|
67
|
+
*/
|
|
68
|
+
export async function restore(target) {
|
|
69
|
+
const { root, codexHome, command } = target;
|
|
70
|
+
const dir = journalDir(root);
|
|
71
|
+
const journalPath = join(dir, 'journal.json');
|
|
72
|
+
const journal = await readJsonIfExists(journalPath);
|
|
73
|
+
if (!journal)
|
|
74
|
+
return;
|
|
75
|
+
if (journal.codexHome !== codexHome) {
|
|
76
|
+
throw new Error(`An interrupted update in CODEX_HOME=${journal.codexHome} must be recovered first, but this run uses ${codexHome}. Run "CODEX_HOME=${journal.codexHome} shinaps-skills recover codex".`);
|
|
77
|
+
}
|
|
78
|
+
const agentsDir = join(codexHome, 'agents');
|
|
79
|
+
for (const [name, priorContent] of Object.entries(journal.agents)) {
|
|
80
|
+
if (!managedAgent.test(name)) {
|
|
81
|
+
throw new Error(`Invalid agent filename "${name}" in ${journalPath}.`);
|
|
82
|
+
}
|
|
83
|
+
const current = await readTextIfExists(join(agentsDir, name));
|
|
84
|
+
if (current === null)
|
|
85
|
+
continue;
|
|
86
|
+
if (current !== priorContent && digest(current) !== journal.nextHashes[name]) {
|
|
87
|
+
throw new Error(`Modified agent preserved during recovery: ${join(agentsDir, name)}. Move the edited file elsewhere, then run "shinaps-skills recover codex".`);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
const payload = payloadDir(root);
|
|
91
|
+
await rm(payload, { recursive: true, force: true });
|
|
92
|
+
if (journal.hadPayload)
|
|
93
|
+
await cp(join(dir, 'payload'), payload, { recursive: true });
|
|
94
|
+
await mkdir(agentsDir, { recursive: true });
|
|
95
|
+
for (const [name, content] of Object.entries(journal.agents)) {
|
|
96
|
+
if (content === null)
|
|
97
|
+
await rm(join(agentsDir, name), { force: true });
|
|
98
|
+
else
|
|
99
|
+
await writeFileAtomic(join(agentsDir, name), content);
|
|
100
|
+
}
|
|
101
|
+
if (journal.state === null)
|
|
102
|
+
await rm(statePath(root), { force: true });
|
|
103
|
+
else
|
|
104
|
+
await writeFileAtomic(statePath(root), journal.state);
|
|
105
|
+
// Codex は plugin を独自のキャッシュにも保持しているため、ファイルを戻したあとで登録し直す。
|
|
106
|
+
if (journal.hadPayload)
|
|
107
|
+
await registerPlugin(command, payload);
|
|
108
|
+
await removeJournal(dir);
|
|
109
|
+
}
|
|
110
|
+
// journal の削除が完了の時点。順序が重要で、payload/ が先に消えて journal が残ると、次の restore が
|
|
111
|
+
// 新しい配布物を削除したうえで古い配布物のコピーに失敗する。
|
|
112
|
+
async function removeJournal(dir) {
|
|
113
|
+
await rm(join(dir, 'journal.json'));
|
|
114
|
+
await rm(dir, { recursive: true });
|
|
115
|
+
}
|
package/package.json
CHANGED
|
@@ -1,13 +1,41 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@shinaps/skills",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.12.1",
|
|
4
4
|
"description": "Install Shinaps AI workflows and custom agents for Codex",
|
|
5
5
|
"type": "module",
|
|
6
|
-
"bin": {
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
"
|
|
10
|
-
|
|
6
|
+
"bin": {
|
|
7
|
+
"shinaps-skills": "dist/bin/skills.js"
|
|
8
|
+
},
|
|
9
|
+
"files": [
|
|
10
|
+
"dist/",
|
|
11
|
+
"plugins/",
|
|
12
|
+
".agents/plugins/marketplace.json",
|
|
13
|
+
"README.md",
|
|
14
|
+
"LICENSE"
|
|
15
|
+
],
|
|
16
|
+
"engines": {
|
|
17
|
+
"node": ">=22"
|
|
18
|
+
},
|
|
19
|
+
"scripts": {
|
|
20
|
+
"build": "tsc -p tsconfig.build.json",
|
|
21
|
+
"prepack": "npm run build",
|
|
22
|
+
"test": "node --test \"test/*.test.ts\"",
|
|
23
|
+
"check": "npm run format:check && tsc --noEmit",
|
|
24
|
+
"verify:package": "node scripts/verify-package.ts",
|
|
25
|
+
"format": "biome format --write .",
|
|
26
|
+
"format:check": "biome format ."
|
|
27
|
+
},
|
|
28
|
+
"repository": {
|
|
29
|
+
"type": "git",
|
|
30
|
+
"url": "https://github.com/sh1n4ps/shinaps-skills.git"
|
|
31
|
+
},
|
|
11
32
|
"license": "MIT",
|
|
12
|
-
"publishConfig": {
|
|
33
|
+
"publishConfig": {
|
|
34
|
+
"access": "public"
|
|
35
|
+
},
|
|
36
|
+
"devDependencies": {
|
|
37
|
+
"@biomejs/biome": "2.5.15",
|
|
38
|
+
"@types/node": "^26.6.4",
|
|
39
|
+
"typescript": "^7.0.2"
|
|
40
|
+
}
|
|
13
41
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: commit
|
|
3
|
-
description:
|
|
3
|
+
description: 許可された差分を依存順の小さなローカルコミットへ分割し、各単位を検証・レビューする。開始時に通常(推奨)・自律の進め方をユーザーに確認し、選択された範囲で実行する。
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
## 作業記録の保存先
|
|
@@ -19,7 +19,14 @@ description: 許可された差分を依存順の小さなローカルコミッ
|
|
|
19
19
|
|
|
20
20
|
## 範囲と進め方
|
|
21
21
|
|
|
22
|
-
|
|
22
|
+
コミットはユーザーが明示した場合だけ行う。開始時に、今回の対象についてユーザー自身が進め方を選択済みか確認する。未選択なら、利用可能な入力ツール(なければ文章)で「今回のコミットはどちらで進めますか?」と一問だけ聞く。
|
|
23
|
+
|
|
24
|
+
- **通常(推奨)**: 単位ごとに差分・理由・検証結果・メッセージを提示し、承認を得てからコミットする。
|
|
25
|
+
- **自律**: 許可された対象を単位ごとに検証・レビューし、個別の承認を求めずコミットする。
|
|
26
|
+
|
|
27
|
+
回答を受け取るまで作業を進めない。未回答や推奨選択肢の初期選択を承認と扱わない。「コミットして」「進めて」などの一般的な依頼や、エージェント自身の計画から自律モードを推測しない。今回の対象についてユーザーが通常・自律を明示済みならその選択を引き継ぎ、同じ確認を繰り返さない。過去の別対象での自律選択は引き継がない。
|
|
28
|
+
|
|
29
|
+
どちらも対象外変更、push、PR、公開、既存履歴の書き換えは含まない。
|
|
23
30
|
|
|
24
31
|
Git操作と修正は現在のセッションが行う。独立レビューは利用可能かつ許可された場合に使う。指定のエージェントがなければ、その役割の観点でレビューする。独立レビューがプロジェクトの必須条件なのに実行できなければ、セルフレビューを同等と称さず止める。別worktreeの作成や別セッションへの委託を、このスキルから許可しない。
|
|
25
32
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: commit
|
|
3
|
-
description:
|
|
3
|
+
description: 許可された差分を依存順の小さなローカルコミットへ分割し、各単位を検証・レビューする。開始時に通常(推奨)・自律の進め方をユーザーに確認し、選択された範囲で実行する。
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
## 作業記録の保存先
|
|
@@ -19,7 +19,14 @@ description: 許可された差分を依存順の小さなローカルコミッ
|
|
|
19
19
|
|
|
20
20
|
## 範囲と進め方
|
|
21
21
|
|
|
22
|
-
|
|
22
|
+
コミットはユーザーが明示した場合だけ行う。開始時に、今回の対象についてユーザー自身が進め方を選択済みか確認する。未選択なら、利用可能な入力ツール(なければ文章)で「今回のコミットはどちらで進めますか?」と一問だけ聞く。
|
|
23
|
+
|
|
24
|
+
- **通常(推奨)**: 単位ごとに差分・理由・検証結果・メッセージを提示し、承認を得てからコミットする。
|
|
25
|
+
- **自律**: 許可された対象を単位ごとに検証・レビューし、個別の承認を求めずコミットする。
|
|
26
|
+
|
|
27
|
+
回答を受け取るまで作業を進めない。未回答や推奨選択肢の初期選択を承認と扱わない。「コミットして」「進めて」などの一般的な依頼や、エージェント自身の計画から自律モードを推測しない。今回の対象についてユーザーが通常・自律を明示済みならその選択を引き継ぎ、同じ確認を繰り返さない。過去の別対象での自律選択は引き継がない。
|
|
28
|
+
|
|
29
|
+
どちらも対象外変更、push、PR、公開、既存履歴の書き換えは含まない。
|
|
23
30
|
|
|
24
31
|
Git操作と修正は現在のセッションが行う。独立レビューは利用可能かつ許可された場合に使う。指定のエージェントがなければ、その役割の観点でレビューする。独立レビューがプロジェクトの必須条件なのに実行できなければ、セルフレビューを同等と称さず止める。別worktreeの作成や別セッションへの委託を、このスキルから許可しない。
|
|
25
32
|
|
package/bin/skills.mjs
DELETED
package/lib/install.mjs
DELETED
|
@@ -1,116 +0,0 @@
|
|
|
1
|
-
import { mkdir, readFile, writeFile, readdir, cp, rm, rename, lstat } from 'node:fs/promises';
|
|
2
|
-
import { homedir } from 'node:os';
|
|
3
|
-
import { join, dirname, resolve } from 'node:path';
|
|
4
|
-
import { fileURLToPath } from 'node:url';
|
|
5
|
-
import { createHash, randomUUID } from 'node:crypto';
|
|
6
|
-
import { spawnSync } from 'node:child_process';
|
|
7
|
-
|
|
8
|
-
const source = resolve(dirname(fileURLToPath(import.meta.url)), '..');
|
|
9
|
-
const plugin = 'shinaps@shinaps-skills-codex';
|
|
10
|
-
const digest = text => createHash('sha256').update(text).digest('hex');
|
|
11
|
-
|
|
12
|
-
async function optional(path) {
|
|
13
|
-
try {
|
|
14
|
-
if ((await lstat(path)).isSymbolicLink()) throw new Error(`Symlink preserved: ${path}`);
|
|
15
|
-
return await readFile(path, 'utf8');
|
|
16
|
-
}
|
|
17
|
-
catch (error) { if (error.code === 'ENOENT') return null; throw error; }
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
export async function run(args, options = {}) {
|
|
21
|
-
const [action, target, ...extra] = args;
|
|
22
|
-
const log = options.log ?? console.log;
|
|
23
|
-
if (!action || action === '--help' || action === 'help') {
|
|
24
|
-
log('Usage: shinaps-skills <install|update|uninstall> codex\nClaude Code: /plugin marketplace add sh1n4ps/shinaps-skills\n /plugin install shinaps@shinaps-skills');
|
|
25
|
-
return;
|
|
26
|
-
}
|
|
27
|
-
if (!['install', 'update', 'uninstall'].includes(action) || target !== 'codex' || extra.length) {
|
|
28
|
-
throw new Error('Use install, update or uninstall followed by codex. See --help.');
|
|
29
|
-
}
|
|
30
|
-
const home = options.home ?? homedir();
|
|
31
|
-
const codexHome = options.codexHome ?? process.env.CODEX_HOME ?? join(home, '.codex');
|
|
32
|
-
const root = join(home, '.local', 'share', 'shinaps-ai', 'skills');
|
|
33
|
-
const statePath = join(root, 'state.json');
|
|
34
|
-
const payload = join(root, 'codex');
|
|
35
|
-
const agentsDir = join(codexHome, 'agents');
|
|
36
|
-
const stateText = await optional(statePath);
|
|
37
|
-
const state = stateText ? JSON.parse(stateText) : { agents: {}, codexHome };
|
|
38
|
-
// Ownership is bound to the installation target, so a changed CODEX_HOME cannot
|
|
39
|
-
// make an uninstall remove a different account's files.
|
|
40
|
-
if (state.codexHome !== codexHome) throw new Error('CODEX_HOME differs from the existing install. Use the original target first.');
|
|
41
|
-
const command = options.command ?? ((argv) => {
|
|
42
|
-
const result = spawnSync('codex', argv, { stdio: 'inherit' });
|
|
43
|
-
if (result.error) throw result.error;
|
|
44
|
-
if (result.status !== 0) throw new Error(`codex ${argv.join(' ')} failed. Fix the reported error and retry; installed state is retained.`);
|
|
45
|
-
});
|
|
46
|
-
const save = async () => {
|
|
47
|
-
await mkdir(root, { recursive: true });
|
|
48
|
-
await writeFile(statePath + '.tmp', JSON.stringify(state, null, 2) + '\n');
|
|
49
|
-
await rename(statePath + '.tmp', statePath);
|
|
50
|
-
};
|
|
51
|
-
// A prefix does not prove ownership. Only exact recorded content is replaced
|
|
52
|
-
// or deleted; a user's edits require their explicit resolution.
|
|
53
|
-
for (const [name, hash] of Object.entries(state.agents)) {
|
|
54
|
-
if (!/^shinaps-[a-z0-9-]+\.toml$/.test(name)) throw new Error('Invalid managed agent filename.');
|
|
55
|
-
const current = await optional(join(agentsDir, name));
|
|
56
|
-
if (current !== null && digest(current) !== hash && digest(current) !== state.pending?.[name]) throw new Error(`Modified agent preserved: ${join(agentsDir, name)}`);
|
|
57
|
-
}
|
|
58
|
-
if (action === 'uninstall') {
|
|
59
|
-
if (!stateText) { log('No managed Shinaps installation found.'); return; }
|
|
60
|
-
await command(['plugin', 'remove', plugin]);
|
|
61
|
-
await command(['plugin', 'marketplace', 'remove', 'shinaps-skills-codex']);
|
|
62
|
-
for (const name of Object.keys(state.agents)) await rm(join(agentsDir, name), { force: true });
|
|
63
|
-
await rm(payload, { recursive: true, force: true });
|
|
64
|
-
await rm(statePath);
|
|
65
|
-
log('Removed managed Codex plugin and agents. Other skills and MCP credentials were preserved.');
|
|
66
|
-
return;
|
|
67
|
-
}
|
|
68
|
-
const agentSource = join(options.source ?? source, 'codex', 'agents');
|
|
69
|
-
const next = {};
|
|
70
|
-
for (const name of await readdir(agentSource)) {
|
|
71
|
-
if (!/^shinaps-[a-z0-9-]+\.toml$/.test(name)) continue;
|
|
72
|
-
const content = await readFile(join(agentSource, name), 'utf8');
|
|
73
|
-
const current = await optional(join(agentsDir, name));
|
|
74
|
-
if (!(name in state.agents) && current !== null) throw new Error(`Unmanaged agent preserved: ${join(agentsDir, name)}. Remove or relocate the old Shinaps registration first.`);
|
|
75
|
-
next[name] = { content, hash: digest(content) };
|
|
76
|
-
}
|
|
77
|
-
// npx's download cache is temporary. Keep the marketplace payload at a stable
|
|
78
|
-
// location so later Codex sessions and updates do not depend on that cache.
|
|
79
|
-
await rm(payload, { recursive: true, force: true });
|
|
80
|
-
await mkdir(payload, { recursive: true });
|
|
81
|
-
await cp(join(options.source ?? source, 'plugins', 'shinaps-codex'), join(payload, 'plugins', 'shinaps-codex'), { recursive: true });
|
|
82
|
-
await mkdir(join(payload, '.agents', 'plugins'), { recursive: true });
|
|
83
|
-
await cp(join(options.source ?? source, '.agents', 'plugins', 'marketplace.json'), join(payload, '.agents', 'plugins', 'marketplace.json'));
|
|
84
|
-
await save();
|
|
85
|
-
await command(['plugin', 'marketplace', 'add', payload]);
|
|
86
|
-
await command(['plugin', 'add', plugin]);
|
|
87
|
-
await mkdir(agentsDir, { recursive: true });
|
|
88
|
-
for (const [name, { content, hash }] of Object.entries(next)) {
|
|
89
|
-
// Keep both the old and new digest until the atomic replacement completes.
|
|
90
|
-
// Retrying after an interrupted update can then recognize either version.
|
|
91
|
-
state.agents[name] ??= hash;
|
|
92
|
-
state.pending = { [name]: hash };
|
|
93
|
-
await save();
|
|
94
|
-
const temporary = join(agentsDir, `.${name}.${randomUUID()}.tmp`);
|
|
95
|
-
try {
|
|
96
|
-
await writeFile(temporary, content, { flag: 'wx' });
|
|
97
|
-
await rename(temporary, join(agentsDir, name));
|
|
98
|
-
} finally {
|
|
99
|
-
await rm(temporary, { force: true });
|
|
100
|
-
}
|
|
101
|
-
state.agents[name] = hash;
|
|
102
|
-
delete state.pending;
|
|
103
|
-
await save();
|
|
104
|
-
}
|
|
105
|
-
for (const name of Object.keys(state.agents)) {
|
|
106
|
-
if (!(name in next)) {
|
|
107
|
-
await rm(join(agentsDir, name), { force: true });
|
|
108
|
-
delete state.agents[name];
|
|
109
|
-
}
|
|
110
|
-
}
|
|
111
|
-
await save();
|
|
112
|
-
log('Shinaps installed. Start a new Codex session to load skills and agents.');
|
|
113
|
-
log('No Shinaps AI account is required to install. For MCP features, create an account at https://app.shinaps.ai and use your client’s authentication flow.');
|
|
114
|
-
log('MCP setup (only if not already configured): codex mcp add shinaps-ai --url https://app.shinaps.ai/mcp');
|
|
115
|
-
log('Then: codex mcp login shinaps-ai');
|
|
116
|
-
}
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|