@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.
Files changed (54) hide show
  1. package/.agents/plugins/marketplace.json +8 -6
  2. package/README.md +52 -6
  3. package/dist/bin/skills.js +15 -0
  4. package/dist/lib/auto-update.js +245 -0
  5. package/dist/lib/fs.js +49 -0
  6. package/dist/lib/install.js +204 -0
  7. package/dist/lib/layout.js +50 -0
  8. package/dist/lib/lock.js +109 -0
  9. package/dist/lib/transaction.js +115 -0
  10. package/package.json +35 -7
  11. package/plugins/{shinaps → shinaps-claude}/.claude-plugin/plugin.json +1 -1
  12. package/plugins/{shinaps → shinaps-claude}/skills/commit/SKILL.md +9 -2
  13. package/plugins/shinaps-codex/.codex-plugin/plugin.json +1 -1
  14. package/plugins/shinaps-codex/skills/commit/SKILL.md +9 -2
  15. package/bin/skills.mjs +0 -9
  16. package/lib/install.mjs +0 -116
  17. /package/plugins/{shinaps → shinaps-claude}/agents/shinaps-architect.md +0 -0
  18. /package/plugins/{shinaps → shinaps-claude}/agents/shinaps-debug-validator.md +0 -0
  19. /package/plugins/{shinaps → shinaps-claude}/agents/shinaps-debugger.md +0 -0
  20. /package/plugins/{shinaps → shinaps-claude}/agents/shinaps-explorer.md +0 -0
  21. /package/plugins/{shinaps → shinaps-claude}/agents/shinaps-investigator.md +0 -0
  22. /package/plugins/{shinaps → shinaps-claude}/agents/shinaps-plan-reviewer.md +0 -0
  23. /package/plugins/{shinaps → shinaps-claude}/agents/shinaps-refine-agent.md +0 -0
  24. /package/plugins/{shinaps → shinaps-claude}/agents/shinaps-review-validator.md +0 -0
  25. /package/plugins/{shinaps → shinaps-claude}/agents/shinaps-reviewer.md +0 -0
  26. /package/plugins/{shinaps → shinaps-claude}/agents/shinaps-spec-writer.md +0 -0
  27. /package/plugins/{shinaps → shinaps-claude}/agents/shinaps-split-reviewer.md +0 -0
  28. /package/plugins/{shinaps → shinaps-claude}/agents/shinaps-survey-validator.md +0 -0
  29. /package/plugins/{shinaps → shinaps-claude}/agents/shinaps-tech-surveyor.md +0 -0
  30. /package/plugins/{shinaps → shinaps-claude}/skills/create-pr/SKILL.md +0 -0
  31. /package/plugins/{shinaps → shinaps-claude}/skills/debug/SKILL.md +0 -0
  32. /package/plugins/{shinaps → shinaps-claude}/skills/dev/SKILL.md +0 -0
  33. /package/plugins/{shinaps → shinaps-claude}/skills/explore/SKILL.md +0 -0
  34. /package/plugins/{shinaps → shinaps-claude}/skills/explore/agents/openai.yaml +0 -0
  35. /package/plugins/{shinaps → shinaps-claude}/skills/extract-rules/SKILL.md +0 -0
  36. /package/plugins/{shinaps → shinaps-claude}/skills/refine/SKILL.md +0 -0
  37. /package/plugins/{shinaps → shinaps-claude}/skills/release/SKILL.md +0 -0
  38. /package/plugins/{shinaps → shinaps-claude}/skills/review/SKILL.md +0 -0
  39. /package/plugins/{shinaps → shinaps-claude}/skills/simplify/SKILL.md +0 -0
  40. /package/plugins/{shinaps → shinaps-claude}/skills/spec/SKILL.md +0 -0
  41. /package/plugins/{shinaps → shinaps-claude}/skills/verify-ui/SKILL.md +0 -0
  42. /package/{codex → plugins/shinaps-codex}/agents/shinaps-architect.toml +0 -0
  43. /package/{codex → plugins/shinaps-codex}/agents/shinaps-debug-validator.toml +0 -0
  44. /package/{codex → plugins/shinaps-codex}/agents/shinaps-debugger.toml +0 -0
  45. /package/{codex → plugins/shinaps-codex}/agents/shinaps-explorer.toml +0 -0
  46. /package/{codex → plugins/shinaps-codex}/agents/shinaps-investigator.toml +0 -0
  47. /package/{codex → plugins/shinaps-codex}/agents/shinaps-plan-reviewer.toml +0 -0
  48. /package/{codex → plugins/shinaps-codex}/agents/shinaps-refine-agent.toml +0 -0
  49. /package/{codex → plugins/shinaps-codex}/agents/shinaps-review-validator.toml +0 -0
  50. /package/{codex → plugins/shinaps-codex}/agents/shinaps-reviewer.toml +0 -0
  51. /package/{codex → plugins/shinaps-codex}/agents/shinaps-spec-writer.toml +0 -0
  52. /package/{codex → plugins/shinaps-codex}/agents/shinaps-split-reviewer.toml +0 -0
  53. /package/{codex → plugins/shinaps-codex}/agents/shinaps-survey-validator.toml +0 -0
  54. /package/{codex → plugins/shinaps-codex}/agents/shinaps-tech-surveyor.toml +0 -0
@@ -1,10 +1,12 @@
1
1
  {
2
2
  "name": "shinaps-skills-codex",
3
3
  "interface": { "displayName": "Shinaps AI for Codex" },
4
- "plugins": [{
5
- "name": "shinaps",
6
- "source": { "source": "local", "path": "./plugins/shinaps-codex" },
7
- "policy": { "installation": "AVAILABLE", "authentication": "ON_USE" },
8
- "category": "Developer Tools"
9
- }]
4
+ "plugins": [
5
+ {
6
+ "name": "shinaps",
7
+ "source": { "source": "local", "path": "./plugins/shinaps-codex" },
8
+ "policy": { "installation": "AVAILABLE", "authentication": "ON_USE" },
9
+ "category": "Developer Tools"
10
+ }
11
+ ]
10
12
  }
package/README.md CHANGED
@@ -21,7 +21,7 @@ Shinaps AIの利用者向け開発ワークフロー。Claude CodeとCodexそれ
21
21
  | `extract-rules` | PRレビューからruleを抽出する |
22
22
  | `simplify` | 目的から見直して不要な構造や手順を削る |
23
23
  | `verify-ui` | 対象環境の実画面で見た目・操作を確かめる |
24
- | `commit` | 検証できる単位へ分けてコミットする。自律モードは明示依頼時のみ |
24
+ | `commit` | 開始時に通常(推奨)・自律を確認し、検証できる単位へ分けてコミットする |
25
25
  | `create-pr` | リポジトリの規約に合わせてPRを作成する |
26
26
  | `release` | 公開条件を確定し、対象バージョンの利用可能性を確認する |
27
27
 
@@ -46,9 +46,12 @@ Node.js 22以上と、`plugin marketplace add` / `plugin add` に対応したCod
46
46
  npx @shinaps/skills@latest install codex
47
47
  npx @shinaps/skills@latest update codex
48
48
  npx @shinaps/skills@latest uninstall codex
49
+ npx @shinaps/skills@latest recover codex
49
50
  ```
50
51
 
51
- ローカル開発では、このcheckoutから `node bin/skills.mjs install codex` のように同じ操作を実行できる。
52
+ `recover codex`は、中断した更新の記録が残っている場合に復元だけを行う。記録が無ければ何もしない。
53
+
54
+ ローカル開発では、このcheckoutから `node bin/skills.ts install codex` のように同じ操作を実行できる。Node.js 22.18以上なら型除去でTypeScriptをそのまま実行する (ビルド不要)。
52
55
 
53
56
  CLIはスキルをCodex pluginとして登録し、13個のカスタムエージェントを`${CODEX_HOME:-~/.codex}/agents/`へ配置する。新しいセッションで`shinaps:dev`、`shinaps:review`等が読み込まれる。エージェントのモデルと推論設定は親セッションから継承し、調査・レビュー担当は読み取り専用を既定にする。親セッションの実行時設定が優先される場合もあるため、権限の強制境界とは扱わない。
54
57
 
@@ -56,12 +59,41 @@ CLIはスキルをCodex pluginとして登録し、13個のカスタムエージ
56
59
 
57
60
  uninstallはこのCLIのplugin・marketplace・管理済みエージェントを取り除く。個人用スキルと、別途登録したMCP接続・認証情報は保持する。
58
61
 
62
+ ### 自動更新(Codex / macOS)
63
+
64
+ ```sh
65
+ npx @shinaps/skills@latest auto-update enable codex
66
+ npx @shinaps/skills@latest auto-update status codex
67
+ npx @shinaps/skills@latest auto-update check codex
68
+ npx @shinaps/skills@latest auto-update disable codex
69
+ ```
70
+
71
+ 初期状態は無効。導入済みの利用者が`enable`するとmacOSのユーザーLaunchAgentを登録し、ログイン中に1日1回(12:00)確認する。スリープ中に12:00を過ぎた場合は起床時に1回だけ実行する。電源が切れていた回は実行しない。確認のたびにnpmから`latest`のパッケージを取得して実行し、導入版より新しい安定版の場合だけ更新する。古い版への自動ダウングレードはしない。更新内容は新しいCodexセッションから反映される。
72
+
73
+ `enable`は通常のログインシェルから実行する。CodexやClaude Codeのセッション内からは実行しない。登録時のPATH、npmの場所、`CODEX_HOME`が記録され、以後の確認でもそのまま使われる。ツールの場所を変えた場合は再度`enable`する。npmの設定は`~/.npmrc`のものが使われる。
74
+
75
+ 登録直後に次の手順で動作を確認できる。`kickstart`はすぐに戻るので、npmの取得が終わるまで待ってから残りを実行する。
76
+
77
+ ```sh
78
+ launchctl kickstart -k gui/$(id -u)/io.shinaps.skills.update
79
+ launchctl print gui/$(id -u)/io.shinaps.skills.update | grep 'last exit code'
80
+ npx @shinaps/skills@latest auto-update status codex
81
+ ```
82
+
83
+ 更新前に新版を準備し、旧スキル・エージェント・管理情報を退避する。更新中のエラー時は旧内容とCodexのプラグイン登録を復元する。強制終了などで復元できなかった場合は、次の更新確認時または`recover codex`の実行時に、`~/.local/share/shinaps-ai/skills/transaction/`に残った記録から復元する。復元にも失敗した場合は同じ場所にバックアップを保持して停止するので、エラーの原因を直してから`recover codex`を実行する。導入後のユーザー編集や同時更新は上書きせず停止する。
84
+
85
+ `status`は設定と最終確認結果を表示する。失敗の詳細は`~/.local/share/shinaps-ai/skills/auto-update-error.log`、通常ログは同じ場所の`auto-update.log`に記録する。npmがパッケージを取得できなかった場合は最終確認結果は更新されず、エラーログだけに記録される。通知ポップアップは出さない。`disable`は予定実行を停止し、導入したスキルは保持する。`uninstall codex`は予定実行も停止する。
86
+
87
+ macOS以外では予定実行の登録に対応していないため手動更新を使う。
88
+
89
+ Claude Codeは標準のmarketplace自動更新機能を使う。`/plugin` のMarketplacesから対象marketplaceのauto-updateを有効にする。GitHubから自動取得するにはリモートmarketplaceを登録する必要があり、ローカルcheckout登録ではソースのgit pullまで自動実行しない。
90
+
59
91
  ## 開発と配布
60
92
 
61
- - Claude版: `plugins/shinaps/`、marketplaceは`.claude-plugin/marketplace.json`
93
+ - Claude版: `plugins/shinaps-claude/`、marketplaceは`.claude-plugin/marketplace.json`
62
94
  - Codex版: `plugins/shinaps-codex/`、marketplaceは`.agents/plugins/marketplace.json`
63
- - Codexカスタムエージェント: `codex/agents/`
64
- - 配布CLI: `bin/skills.mjs` / `lib/install.mjs`
95
+ - Codexカスタムエージェント: `plugins/shinaps-codex/agents/`
96
+ - 配布CLI: ソースは `bin/skills.ts` と `lib/*.ts`。`npm run build` (`tsc`) で `dist/` にJSを出力し、npmにはそれを配布する。Node.jsは`node_modules`配下の`.ts`に型除去を適用しないため、ソースのままは配布できない。checkoutでは型除去で直接実行するので、`enum`など型除去で消せない構文は使わない (`tsconfig.json` の `erasableSyntaxOnly`)
65
97
 
66
98
  品質基準とプロジェクト設定の方針は揃えつつ、クライアント固有の実行手順・エージェント形式は別々に管理する。Claude向けのエージェント呼び出しをCodexに読み替えさせる方式は使わない。スキルを追加・変更した場合は両版への適用可否を判断する。
67
99
 
@@ -73,6 +105,20 @@ npm pack --dry-run
73
105
 
74
106
  公開前にはnpmスコープ`@shinaps`の利用権限とtarball内容を確認する。CLIのバージョンと両pluginのバージョンを揃える。開発中の同一バージョンも`update codex`で再配置できる。
75
107
 
108
+ ### CIとnpm自動公開
109
+
110
+ `.github/workflows/publish.yml` はmainへのpush・PR・手動実行でテストと配布内容を検証する。通常のGitHub Releaseを公開すると、検証成功後に同じtagのパッケージをnpmへpublic公開する。prereleaseは公開しない。手動実行は検証専用で、npmへの公開は行わない。
111
+
112
+ リリース時は`package.json`と両plugin manifestのバージョンを揃えてコミットし、`v<version>` tagを対象にGitHub Releaseを公開する。tagとバージョンが不一致なら停止する。既存のnpmバージョンは再公開できないため、次の変更には新しいバージョンを使う。
113
+
114
+ npmの`@shinaps/skills`にGitHub ActionsのTrusted Publisherを登録する。GitHub ownerは`sh1n4ps`、repositoryは`shinaps-skills`、workflow filenameは`publish.yml`、direct publishを許可する。CIはGitHub-hosted Ubuntu・Node.js 24を使い、publish jobだけに`id-token: write`を付与する。`NPM_TOKEN` secretは不要。初回の信頼設定には本人認証が必要だが、その後のCI公開に毎回のセキュリティキー操作は不要。
115
+
116
+ ローカルで同じ配布検証を行うには`npm run verify:package`を使う。`RELEASE_TAG=v<version>`を付ければtag整合性も確認できる。npmのprovenanceは公開リポジトリからのTrusted Publishingが対象で、privateリポジトリでは生成されない。
117
+
118
+ ### コード整形
119
+
120
+ `npm ci` で開発依存を導入し、`npm run format` でBiomeによる整形を行う。`npm run check` は整形と型 (`tsc --noEmit`) を検証する。`npm pack` と `npm publish` は `prepack` で自動的に `npm run build` を実行する。`npm run verify:package` は配布物の一覧を比較したうえで、tarballを一時ディレクトリにinstallして起動できることを確認する。Biomeは整形用途で使用し、lintとimportの自動整理は無効。Markdown・TOMLのエージェント定義は整形対象外。
121
+
76
122
  ## Shinaps AIとの接続
77
123
 
78
124
  rule照合・抽出が必要な工程には `shinaps-ai` MCPを登録する。サービスURLは `https://app.shinaps.ai/mcp`。ClaudeとCodexそれぞれのMCP設定形式・ログイン手順に従う。開発用localhostはローカルサーバーを利用する場合だけ追加する。
@@ -114,7 +160,7 @@ codex mcp login shinaps-ai
114
160
  | `shinaps-debug-validator` | 仮説の実コード検証 (root-cause / contributing-factor / red-herring / needs-reproduction 分類) |
115
161
  | `shinaps-split-reviewer` | `shinaps:commit` の分割案を「単位の diff だけで妥当性を判断できるか」で批判 + 修正版最終分割計画 (worktree は read-only) |
116
162
 
117
- Claude版はMarkdown、Codex版はTOMLで役割を定義する。**subagent 内では shinaps MCP を直接叩かず、rule 全文は Skill 側から prompt 経由で渡す** 原則を全 system prompt に明記 (credit 最適化)。
163
+ Claude版はMarkdown、Codex版はTOMLで役割を定義する。**subagent 内では shinaps MCP を直接呼び出さず、rule 全文は Skill 側から prompt 経由で渡す** 原則を全 system prompt に明記する。MCP呼び出しの利用量が重複しないようにするため。
118
164
 
119
165
 
120
166
  ## 作業成果物
@@ -0,0 +1,15 @@
1
+ #!/usr/bin/env node
2
+ import { run } from '../lib/install.js';
3
+ import { autoUpdate } from '../lib/auto-update.js';
4
+ import { errorMessage } from '../lib/fs.js';
5
+ const [command, ...rest] = process.argv.slice(2);
6
+ try {
7
+ if (command === 'auto-update')
8
+ await autoUpdate(rest);
9
+ else
10
+ await run(process.argv.slice(2));
11
+ }
12
+ catch (error) {
13
+ console.error(`Shinaps: ${errorMessage(error)}`);
14
+ process.exitCode = 1;
15
+ }
@@ -0,0 +1,245 @@
1
+ import { mkdir, writeFile, rm } from 'node:fs/promises';
2
+ import { join, dirname } from 'node:path';
3
+ import { homedir } from 'node:os';
4
+ import { execFileSync, spawnSync } from 'node:child_process';
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
+ const actions = ['enable', 'disable', 'status', 'check'];
10
+ const isAction = (value) => actions.includes(value);
11
+ const label = 'io.shinaps.skills.update';
12
+ // StartInterval ではなく時刻指定にする。launchd はスリープ中に来た間隔の実行をスキップするが、時刻
13
+ // 指定ならスリープ中に過ぎた回を起床時に 1 回実行する (man launchd.plist)。12:00 は Mac が起きていて
14
+ // ログイン中である可能性が高い時刻として選んだ。
15
+ const schedule = { hour: 12, minute: 0 };
16
+ /**
17
+ * `shinaps-skills auto-update <enable|disable|status|check> codex`。
18
+ * @param args サブコマンドと対象。例: `['enable', 'codex']`。
19
+ * @param options 動作環境の指定。`auto-update check` とテストが使う。
20
+ */
21
+ export async function autoUpdate(args, options = {}) {
22
+ const [action, target, ...extra] = args;
23
+ if (!isAction(action) || target !== 'codex' || extra.length) {
24
+ throw new Error(`Unknown command: auto-update ${args.join(' ')}. Usage: shinaps-skills auto-update <enable|disable|status|check> codex`);
25
+ }
26
+ const home = options.home ?? homedir();
27
+ const root = managedRoot(home);
28
+ const context = {
29
+ home,
30
+ root,
31
+ configPath: join(root, 'auto-update.json'),
32
+ resultPath: join(root, 'auto-update-result.json'),
33
+ log: options.log ?? console.log,
34
+ options,
35
+ };
36
+ // enable / disable / check は同じロックで直列にする。check の実行中に disable が走らないようにするため。
37
+ const schedulerDir = join(root, 'scheduler');
38
+ switch (action) {
39
+ case 'status':
40
+ return status(context);
41
+ case 'check':
42
+ return withLock(schedulerDir, () => check(context));
43
+ case 'disable':
44
+ return withLock(schedulerDir, () => disable(context));
45
+ case 'enable':
46
+ return withLock(schedulerDir, () => enable(context));
47
+ }
48
+ }
49
+ async function status(context) {
50
+ const config = await readJsonIfExists(context.configPath);
51
+ const lastCheck = await readJsonIfExists(context.resultPath);
52
+ context.log(JSON.stringify({ enabled: config !== null, config, lastCheck }, null, 2));
53
+ }
54
+ async function check(context) {
55
+ // 設定はロックを取ってから読む。取得前に読むと、その間に完了した disable を見逃し、無効にしたあとで
56
+ // 更新が実行されるため。
57
+ const config = await readJsonIfExists(context.configPath);
58
+ if (!config) {
59
+ throw new Error('Auto-update is not enabled. Run "shinaps-skills auto-update enable codex" first.');
60
+ }
61
+ const result = { checkedAt: new Date().toISOString() };
62
+ try {
63
+ await updateIfNewer(context, config, result);
64
+ }
65
+ catch (error) {
66
+ result.status = 'failed';
67
+ await recordResult(context.resultPath, result, error);
68
+ throw error;
69
+ }
70
+ await recordResult(context.resultPath, result);
71
+ context.log(JSON.stringify(result));
72
+ }
73
+ async function updateIfNewer(context, config, result) {
74
+ const installation = openInstallation({ ...context.options, codexHome: config.codexHome });
75
+ await recover(installation);
76
+ const state = await readJson(statePath(context.root));
77
+ if (!isRecordedCodexHome(state.codexHome, config.codexHome)) {
78
+ throw new Error(`Shinaps is installed in CODEX_HOME=${state.codexHome}, but auto-update is configured for ${config.codexHome}. Run "CODEX_HOME=${state.codexHome} shinaps-skills auto-update enable codex".`);
79
+ }
80
+ // 導入済みの版は配布物の manifest にある。state.json には記録していない。
81
+ result.installed = await packagedVersion(payloadDir(context.root));
82
+ // launchd ジョブは `npm exec @shinaps/skills@latest` でこのコマンドを起動するので、このプロセスの
83
+ // パッケージが最新版になる。導入済みより新しければ、このパッケージを配布元にして update する。
84
+ result.available = await packagedVersion(installation.source);
85
+ const updated = newer(result.available, result.installed);
86
+ if (updated) {
87
+ await install(installation, 'update');
88
+ result.installed = result.available;
89
+ }
90
+ result.status = updated ? 'updated' : 'up-to-date';
91
+ }
92
+ const packagedVersion = async (directory) => (await readJson(join(directory, manifestPath))).version;
93
+ // 結果ファイルにはエラーメッセージを書かない。エラーは再 throw されて launchd のエラーログ
94
+ // (auto-update-error.log) に出力されるので、詳細はそちらで確認する。結果ファイルには status と、読み
95
+ // 取れた版だけを記録する。
96
+ async function recordResult(path, result, failure) {
97
+ try {
98
+ await writeFile(path, JSON.stringify(result, null, 2) + '\n');
99
+ }
100
+ catch (writeError) {
101
+ if (failure === undefined)
102
+ throw writeError;
103
+ throw new AggregateError([failure, writeError], `${errorMessage(failure)}; result not recorded: ${errorMessage(writeError)}`, { cause: failure });
104
+ }
105
+ }
106
+ async function disable(context) {
107
+ const config = await readJsonIfExists(context.configPath);
108
+ // OS の確認より前に判定する。uninstall はどの OS でも disable を呼び、自動更新を有効にしたことのない
109
+ // 導入でもそこで失敗してはならない。
110
+ if (!config) {
111
+ context.log('Auto-update is already disabled.');
112
+ return;
113
+ }
114
+ const { service, plistPath, launchctl } = launchd(context);
115
+ await launchctl(['bootout', service]);
116
+ await rm(plistPath, { force: true });
117
+ await rm(context.configPath);
118
+ context.log('Daily auto-update disabled. Installed skills and agents were preserved.');
119
+ }
120
+ async function enable(context) {
121
+ const { home, root, configPath, options, log } = context;
122
+ const config = await readJsonIfExists(configPath);
123
+ const { uid, service, plistPath, launchctl } = launchd(context);
124
+ const state = await readJsonIfExists(statePath(root));
125
+ if (!state) {
126
+ throw new Error('Shinaps is not installed for Codex. Run "shinaps-skills install codex" first.');
127
+ }
128
+ const codexHome = codexHomeOf(options, home);
129
+ if (!isRecordedCodexHome(state.codexHome, codexHome)) {
130
+ throw new Error(`${codexHomeMismatch(state.codexHome, codexHome)} Run again with CODEX_HOME=${state.codexHome}.`);
131
+ }
132
+ const nextConfig = {
133
+ codexHome,
134
+ // `status` で表示する。同じパスが ProgramArguments の先頭にも入る。
135
+ npm: options.npm ?? execFileSync('/usr/bin/which', ['npm'], { encoding: 'utf8' }).trim(),
136
+ };
137
+ // ジョブは固定のコピーではなく、`npm exec @latest` で毎回最新版の CLI を取得して check を実行する
138
+ // (npm はタグを毎回オンラインで解決する)。そのため CLI 自体を更新する仕組みは持たない。代わりに、
139
+ // このコマンド行と各ファイルの形式は版をまたいで保つ (layout.ts の冒頭を参照)。
140
+ const plistText = renderPlist({
141
+ programArguments: [
142
+ nextConfig.npm,
143
+ 'exec',
144
+ '--yes',
145
+ '--package',
146
+ '@shinaps/skills@latest',
147
+ '--',
148
+ 'shinaps-skills',
149
+ 'auto-update',
150
+ 'check',
151
+ 'codex',
152
+ ],
153
+ path: options.path ?? process.env.PATH ?? '',
154
+ root,
155
+ });
156
+ // 読み込み済みのラベルへの bootstrap は失敗するので、再 enable では先に今のジョブを外す。
157
+ if (config)
158
+ await launchctl(['bootout', service]);
159
+ await mkdir(dirname(plistPath), { recursive: true });
160
+ await writeFile(plistPath, plistText);
161
+ await writeFile(configPath, JSON.stringify(nextConfig, null, 2) + '\n');
162
+ try {
163
+ await launchctl(['bootstrap', `gui/${uid}`, plistPath]);
164
+ }
165
+ catch (error) {
166
+ try {
167
+ await rm(configPath, { force: true });
168
+ await rm(plistPath, { force: true });
169
+ }
170
+ catch (cleanup) {
171
+ throw new AggregateError([error, cleanup], `${errorMessage(error)}; cleanup failed: ${errorMessage(cleanup)}`, { cause: error });
172
+ }
173
+ throw error;
174
+ }
175
+ const time = `${schedule.hour}:${String(schedule.minute).padStart(2, '0')}`;
176
+ log(`Daily auto-update enabled: runs at ${time} while you are logged in (a run missed during sleep starts on wake).`);
177
+ log(`To test it now: launchctl kickstart -k ${service}`);
178
+ log(`Then check the result: shinaps-skills auto-update status codex (errors: ${join(root, 'auto-update-error.log')})`);
179
+ }
180
+ /** launchd の操作に必要な値。macOS 以外では throw する。 */
181
+ function launchd(context) {
182
+ const { options, home } = context;
183
+ if ((options.platform ?? process.platform) !== 'darwin') {
184
+ throw new Error('Auto-update is available only on macOS. Run "shinaps-skills update codex" to update manually.');
185
+ }
186
+ // macOS であることを確認済みなので getuid は存在する。
187
+ const uid = options.uid ?? process.getuid();
188
+ return {
189
+ uid,
190
+ service: `gui/${uid}/${label}`,
191
+ plistPath: join(home, 'Library/LaunchAgents', `${label}.plist`),
192
+ launchctl: options.launch ?? launchctlCommand,
193
+ };
194
+ }
195
+ const launchctlCommand = (argv) => {
196
+ const result = spawnSync('/bin/launchctl', argv, { encoding: 'utf8' });
197
+ if (result.error)
198
+ throw result.error;
199
+ // 読み込まれていないラベルの bootout は終了コード 3 (ESRCH。`launchctl error 3` で意味を確認できる)
200
+ // で終わる。disable と再 enable はジョブが読み込まれていなくても続けるので、この場合だけ成功として
201
+ // 扱う。終了コードの意味は文書化されていないため、ほかの失敗は区別せずにエラーにする。
202
+ if (result.status !== 0 && !(argv[0] === 'bootout' && result.status === 3)) {
203
+ throw new Error(`launchctl ${argv[0]} failed: ${result.stderr.trim()}`);
204
+ }
205
+ };
206
+ function renderPlist(job) {
207
+ const entry = (argument) => `<string>${escapeXml(argument)}</string>`;
208
+ return `<?xml version="1.0" encoding="UTF-8"?>
209
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
210
+ <plist version="1.0"><dict>
211
+ <key>Label</key><string>${label}</string>
212
+ <key>ProgramArguments</key><array>${job.programArguments.map(entry).join('')}</array>
213
+ <key>StartCalendarInterval</key><dict><key>Hour</key><integer>${schedule.hour}</integer><key>Minute</key><integer>${schedule.minute}</integer></dict>
214
+ <key>EnvironmentVariables</key><dict><key>PATH</key><string>${escapeXml(job.path)}</string></dict>
215
+ <key>WorkingDirectory</key><string>${escapeXml(job.root)}</string>
216
+ <key>StandardOutPath</key><string>${escapeXml(join(job.root, 'auto-update.log'))}</string>
217
+ <key>StandardErrorPath</key><string>${escapeXml(join(job.root, 'auto-update-error.log'))}</string>
218
+ </dict></plist>
219
+ `;
220
+ }
221
+ const xmlEntities = {
222
+ '<': '&lt;',
223
+ '>': '&gt;',
224
+ '&': '&amp;',
225
+ '"': '&quot;',
226
+ "'": '&apos;',
227
+ };
228
+ const escapeXml = (value) => value.replace(/[<>&"']/g, (character) => xmlEntities[character]);
229
+ /** `candidate` が `installed` より新しい安定版か。安定版 (x.y.z) 以外は throw する。 */
230
+ export function newer(candidate, installed) {
231
+ const parse = (value) => {
232
+ if (typeof value !== 'string' || !/^\d+\.\d+\.\d+$/.test(value)) {
233
+ throw new Error(`Expected a stable version, got ${value}`);
234
+ }
235
+ return value.split('.').map(Number);
236
+ };
237
+ const candidateParts = parse(candidate);
238
+ const installedParts = parse(installed);
239
+ for (let index = 0; index < 3; index++) {
240
+ if (candidateParts[index] !== installedParts[index]) {
241
+ return candidateParts[index] > installedParts[index];
242
+ }
243
+ }
244
+ return false;
245
+ }
package/dist/lib/fs.js ADDED
@@ -0,0 +1,49 @@
1
+ import { lstat, readFile, rename, rm, writeFile } from 'node:fs/promises';
2
+ import { randomUUID } from 'node:crypto';
3
+ import { basename, dirname, join } from 'node:path';
4
+ /** OS エラーの code (ENOENT など)。Error 以外や code を持たないエラーは undefined。 */
5
+ export const errorCode = (error) => error instanceof Error ? error.code : undefined;
6
+ export const errorMessage = (error) => error instanceof Error ? error.message : String(error);
7
+ export const readJson = async (path) => JSON.parse(await readFile(path, 'utf8'));
8
+ /** JSON ファイルを読む。無ければ null。 */
9
+ export async function readJsonIfExists(path) {
10
+ try {
11
+ return await readJson(path);
12
+ }
13
+ catch (error) {
14
+ if (errorCode(error) === 'ENOENT')
15
+ return null;
16
+ throw error;
17
+ }
18
+ }
19
+ /**
20
+ * テキストファイルを読む。無ければ null。シンボリックリンクは辿らずに throw する。リンク先にある
21
+ * ユーザーのファイルを読み書きしないため。
22
+ */
23
+ export async function readTextIfExists(path) {
24
+ try {
25
+ if ((await lstat(path)).isSymbolicLink()) {
26
+ throw new Error(`Symlink preserved: ${path}. Replace it with a regular file or remove it, then retry.`);
27
+ }
28
+ return await readFile(path, 'utf8');
29
+ }
30
+ catch (error) {
31
+ if (errorCode(error) === 'ENOENT')
32
+ return null;
33
+ throw error;
34
+ }
35
+ }
36
+ /**
37
+ * 同じディレクトリの一時ファイルに書いてから rename する。読む側が書きかけの内容を見ることはなく、
38
+ * 既存のシンボリックリンクも辿らずに置き換える。
39
+ */
40
+ export async function writeFileAtomic(path, content) {
41
+ const temporary = join(dirname(path), `.${basename(path)}.${randomUUID()}.tmp`);
42
+ try {
43
+ await writeFile(temporary, content, { flag: 'wx' });
44
+ await rename(temporary, path);
45
+ }
46
+ finally {
47
+ await rm(temporary, { force: true });
48
+ }
49
+ }
@@ -0,0 +1,204 @@
1
+ import { readFile, readdir, cp, rm, mkdir } from 'node:fs/promises';
2
+ import { homedir } from 'node:os';
3
+ import { join } from 'node:path';
4
+ import { spawnSync } from 'node:child_process';
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
+ const actions = ['install', 'update', 'uninstall', 'recover'];
10
+ const isAction = (value) => actions.includes(value);
11
+ const usage = [
12
+ 'Usage: shinaps-skills <install|update|uninstall|recover> codex',
13
+ ' shinaps-skills auto-update <enable|disable|status|check> codex',
14
+ 'Claude Code: /plugin marketplace add sh1n4ps/shinaps-skills',
15
+ ' /plugin install shinaps@shinaps-skills',
16
+ ].join('\n');
17
+ const installGuidance = [
18
+ '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.',
19
+ 'MCP setup (only if not already configured): codex mcp add shinaps-ai --url https://app.shinaps.ai/mcp',
20
+ 'Then: codex mcp login shinaps-ai',
21
+ ];
22
+ /**
23
+ * `shinaps-skills <install|update|uninstall|recover> codex` を実行する。どの操作も、開始時に中断した
24
+ * 更新があれば restore で元に戻す。
25
+ * @param args コマンドライン引数 (node とスクリプトのパスを除いたもの)。例: `['install', 'codex']`。
26
+ * @param options 動作環境の指定。`auto-update check` とテストが使う。
27
+ */
28
+ export async function run(args, options = {}) {
29
+ const log = options.log ?? console.log;
30
+ const [action, target, ...extra] = args;
31
+ if (!action || action === '--help' || action === 'help') {
32
+ log(usage);
33
+ return;
34
+ }
35
+ if (!isAction(action) || target !== 'codex' || extra.length) {
36
+ throw new Error(`Unknown command: ${args.join(' ')}. ${usage.split('\n')[0]} (see --help).`);
37
+ }
38
+ const installation = openInstallation(options);
39
+ switch (action) {
40
+ case 'recover':
41
+ return recover(installation);
42
+ case 'uninstall':
43
+ return uninstall(installation, options);
44
+ case 'install':
45
+ case 'update':
46
+ return install(installation, action);
47
+ }
48
+ }
49
+ export function openInstallation(options) {
50
+ const home = options.home ?? homedir();
51
+ const codexHome = codexHomeOf(options, home);
52
+ return {
53
+ root: managedRoot(home),
54
+ codexHome,
55
+ agentsDir: join(codexHome, 'agents'),
56
+ command: options.command ?? codexCommand(codexHome),
57
+ source: options.source ?? packageRoot,
58
+ log: options.log ?? console.log,
59
+ };
60
+ }
61
+ /** 中断した更新があれば元に戻す。無ければ何もしない。 */
62
+ export function recover(installation) {
63
+ return withLock(installation.root, () => restore(installation));
64
+ }
65
+ /**
66
+ * 配布物とエージェントを `installation.source` の内容に置き換える。state.json の記録と違うエージェントが
67
+ * あれば何も変更せずに止める。
68
+ * @param action install と update の違いは完了メッセージだけ。
69
+ */
70
+ export async function install(installation, action) {
71
+ const { root, codexHome, agentsDir, command, log } = installation;
72
+ await withLock(root, async () => {
73
+ await restore(installation);
74
+ const recorded = await readVerifiedState(installation);
75
+ const state = recorded?.state ?? { agents: {}, codexHome };
76
+ const nextAgents = await readPackagedAgents(installation, state);
77
+ const preparedDir = await preparePayload(installation);
78
+ const journal = {
79
+ state: recorded?.text ?? null,
80
+ agents: {},
81
+ codexHome,
82
+ nextHashes: {},
83
+ };
84
+ for (const name of new Set([...Object.keys(state.agents), ...Object.keys(nextAgents)])) {
85
+ journal.agents[name] = await readTextIfExists(join(agentsDir, name));
86
+ journal.nextHashes[name] = nextAgents[name]?.hash ?? null;
87
+ }
88
+ const nextState = {
89
+ agents: Object.fromEntries(Object.entries(nextAgents).map(([name, { hash }]) => [name, hash])),
90
+ codexHome,
91
+ };
92
+ const removed = Object.keys(state.agents).filter((name) => !(name in nextAgents));
93
+ await replaceInstallation(installation, journal, preparedDir, async () => {
94
+ // codex コマンドより前に作る。codex は既定のディレクトリだけを自分で作り、指定された
95
+ // CODEX_HOME が存在しないと失敗するため。
96
+ await mkdir(agentsDir, { recursive: true });
97
+ await registerPlugin(command, payloadDir(root));
98
+ for (const [name, { content }] of Object.entries(nextAgents)) {
99
+ await writeFileAtomic(join(agentsDir, name), content);
100
+ }
101
+ for (const name of removed)
102
+ await rm(join(agentsDir, name), { force: true });
103
+ await writeFileAtomic(statePath(root), JSON.stringify(nextState, null, 2) + '\n');
104
+ });
105
+ });
106
+ if (action === 'update') {
107
+ log('Shinaps updated. Start a new Codex session to load the new skills and agents.');
108
+ return;
109
+ }
110
+ log('Shinaps installed. Start a new Codex session to load skills and agents.');
111
+ for (const line of installGuidance)
112
+ log(line);
113
+ }
114
+ async function uninstall(installation, options) {
115
+ const { root, agentsDir, command, log } = installation;
116
+ await withLock(root, async () => {
117
+ await restore(installation);
118
+ const recorded = await readVerifiedState(installation);
119
+ if (!recorded) {
120
+ log('No managed Shinaps installation found.');
121
+ return;
122
+ }
123
+ // auto-update.ts は check でこのモジュールを使う。相互 import を避けるため実行時に読み込む。
124
+ const { autoUpdate } = await import('./auto-update.js');
125
+ await autoUpdate(['disable', 'codex'], options);
126
+ await command(['plugin', 'remove', pluginId]);
127
+ await command(['plugin', 'marketplace', 'remove', marketplaceName]);
128
+ for (const name of Object.keys(recorded.state.agents)) {
129
+ await rm(join(agentsDir, name), { force: true });
130
+ }
131
+ await rm(payloadDir(root), { recursive: true, force: true });
132
+ await rm(statePath(root));
133
+ log('Removed managed Codex plugin and agents. Other skills and MCP credentials were preserved.');
134
+ });
135
+ }
136
+ // codex にはファイル操作と同じ CODEX_HOME を見せる。options.codexHome (`auto-update check` が渡す)
137
+ // はシェルの環境変数と違い得るので、解決済みの値を明示的に渡す。
138
+ const codexCommand = (codexHome) => (argv) => {
139
+ const result = spawnSync('codex', argv, {
140
+ stdio: 'inherit',
141
+ env: { ...process.env, CODEX_HOME: codexHome },
142
+ });
143
+ if (result.error)
144
+ throw result.error;
145
+ if (result.status !== 0) {
146
+ throw new Error(`codex ${argv.join(' ')} exited with ${result.status ?? result.signal}. Fix the error codex reported above, then retry.`);
147
+ }
148
+ };
149
+ /**
150
+ * state.json を読み、記録された導入先とエージェントを今回の実行で扱えることを確かめる。無ければ null。
151
+ * 名前の接頭辞は所有の証明にならないので、記録した内容と一致するファイルだけを置き換え・削除する。
152
+ * ユーザーが編集したファイルがあれば、何も変更しないうちに止めて、移動か復元を求める。
153
+ */
154
+ async function readVerifiedState(installation) {
155
+ const { root, codexHome, agentsDir } = installation;
156
+ const path = statePath(root);
157
+ const text = await readTextIfExists(path);
158
+ if (text === null)
159
+ return null;
160
+ const state = JSON.parse(text);
161
+ // state.json は記録した CODEX_HOME のエージェントだけを管理する。別の CODEX_HOME で実行した
162
+ // uninstall が、そちらにある同名のファイルを削除しないように、一致しなければ止める。
163
+ if (!isRecordedCodexHome(state.codexHome, codexHome)) {
164
+ throw new Error(`${codexHomeMismatch(state.codexHome, codexHome)} Run again with CODEX_HOME=${state.codexHome}, or run "CODEX_HOME=${state.codexHome} shinaps-skills uninstall codex" first.`);
165
+ }
166
+ for (const [name, hash] of Object.entries(state.agents)) {
167
+ if (!managedAgent.test(name))
168
+ throw new Error(`Invalid agent filename "${name}" in ${path}.`);
169
+ const current = await readTextIfExists(join(agentsDir, name));
170
+ if (current === null)
171
+ continue;
172
+ const currentHash = digest(current);
173
+ if (currentHash !== hash && currentHash !== state.pending?.[name]) {
174
+ throw new Error(`Modified agent preserved: ${join(agentsDir, name)}. Move the edited file elsewhere or restore its content, then retry.`);
175
+ }
176
+ }
177
+ return { text, state };
178
+ }
179
+ /** パッケージに入っているエージェント。導入先に Shinaps が置いていない同名ファイルがあれば止める。 */
180
+ async function readPackagedAgents(installation, state) {
181
+ const { source, agentsDir } = installation;
182
+ const directory = join(source, pluginDir, 'agents');
183
+ const agents = {};
184
+ for (const name of await readdir(directory)) {
185
+ if (!managedAgent.test(name))
186
+ continue;
187
+ const content = await readFile(join(directory, name), 'utf8');
188
+ if (!(name in state.agents) && (await readTextIfExists(join(agentsDir, name))) !== null) {
189
+ throw new Error(`Unmanaged agent preserved: ${join(agentsDir, name)}. Shinaps did not install this file. Move or rename it, then retry.`);
190
+ }
191
+ agents[name] = { content, hash: digest(content) };
192
+ }
193
+ return agents;
194
+ }
195
+ // npx のダウンロードキャッシュは一時的。配布物は管理ディレクトリにコピーし、後続の Codex セッションと
196
+ // 更新がキャッシュに依存しないようにする。
197
+ async function preparePayload(installation) {
198
+ const prepared = join(installation.root, 'prepared');
199
+ await rm(prepared, { recursive: true, force: true });
200
+ for (const path of payloadPaths) {
201
+ await cp(join(installation.source, path), join(prepared, path), { recursive: true });
202
+ }
203
+ return prepared;
204
+ }