技能文件新增路徑守則一節,寫明這支技能的每一次腳本呼叫都要是字面絕對路徑,不留未展開的變數,也不留波浪號。 新增 Step 0:開頭跑一次 readlink -f "$JSC_HOME/current" 解出根目錄,整輪只解這一次,之後每一處都把代稱換成那個目錄。解出來的路徑要用 [ -d ] 查過存在才算數——JSC_HOME 沒設時那道指令會印出 /current、結束碼 0,非空又是絕對路徑,只看前三項會直接放行,而那個目錄並不存在。文件同時明講解到 current 這一層就停,不要再往下解一層,因為再解就落到帶版本號的快取路徑,而那種路徑放不進允許清單。 四處跨外掛呼叫改成字面路徑寫法。其中三處原本寫成相對路徑,那才是模型自己補成帶變數路徑的源頭。技能自己的四支腳本另外處理:jsc-cli 不在 current 底下,那裡只有另外三個外掛,所以改用 CLI 載入技能時講明的外掛基底目錄,原樣照抄、不跑指令。 無人看管的輪次在第一支腳本就被權限層擋下,整輪一個外掛都沒部署。2026-09-02 到 09-03 用非互動模式比照排程環境實測多種寫法:權限層拿未展開的字面字串做靜態比對,帶變數或波浪號的路徑對不上任何允許規則,只有允許清單上的完整字面指令跑得動,連 readlink、ls 這種讀取指令都要各自有規則。放寬允許清單解不掉,因為規則本身也是靜態比對,而且路徑中段的萬用字元不匹配。 deploy 是人在現場跑的技能,開頭那一次核准詢問有人可以按,所以保留自己解路徑的做法。守則另外寫下一句擋回頭路:為了看起來整齊而把字面路徑改回帶變數的寫法,等於再一次弄壞每一個無人看管的輪次。 行為說明表的 deploy 四列同步改寫,觸發時機沒動。 影響排程觸發的無人看管輪次,它們現在才跑得完第一支腳本。現場執行部署的操作者只多出開頭那一次核准詢問。
19 KiB
name, description
| name | description |
|---|---|
| deploy | Batch install, update, or uninstall the whole jsc skill set on every installed AI CLI. Detect CLIs, read the version recommendation and the marketplace domain list in parallel, then ask the user for the mode via decision tree unless the caller already passed one, then run each CLI's native plugin commands in parallel with the unified jsc marketplace (token jsc-{domain}@jsc). Domain list comes from the plugins/meta marketplace.json, never hardcoded. After install or update, hand the detected CLI list to jsc-hooks:hooks-install, write this machine's update and remove guides via write-guides.sh, and demand a session restart. Use for rollout or removal of the jsc plugins; not for a single skill. |
deploy — batch install, update, or uninstall the skill set
Path rule — every script call is a literal absolute path
Write every script call in this skill as a literal absolute path. Never hand the shell a path that still holds a variable or a tilde — $JSC_HOME/..., ~/.jsc/..., or anything like them. The permission layer matches paths statically. It never expands a variable or a tilde, so such a path matches no allow rule, and the call falls through to an approval prompt. An unattended round has nobody to approve it. The run then dies at its first script, before it deploys anything.
Measured on a real machine, not assumed. $JSC_HOME/current/jsc-assist/tools/patrol.sh was blocked and never ran. ~/.jsc/current/... was blocked and never ran. /root/.jsc/current/... ran. Adding an allow rule that itself starts with $JSC_HOME to ~/.claude/settings.json changed nothing: the call stayed blocked. A wider allow list is not the fix, because the rule text is matched statically too.
Portability is no reason to put the variable back. Step 0 resolves the root once, at run time, on whatever machine this runs on — that is where portability comes from. Rewriting {JSC_ROOT}/jsc-hooks/... back to $JSC_HOME/current/jsc-hooks/... for tidiness re-breaks every unattended round.
Step 0 — resolve the two roots, once
Before step 1, run this one command:
readlink -f "$JSC_HOME/current"
It prints one absolute directory: the absolute path of the current directory itself. Call it {JSC_ROOT} for the rest of this document. Stop at that directory — never resolve one level further. current is an ordinary directory, and the symbolic links are its entries, one per plugin; resolving one of those entries lands on the versioned plugin cache (/root/.claude/plugins/cache/jsc/jsc-hooks/0.4.2, say), and a versioned path is exactly the kind no allow rule can hold — a rule with * where the version segment goes matches nothing, measured. {JSC_ROOT} is the version-free root, and staying at it is the whole point. This is the only place a variable may appear. The shell expands it inside the command itself, so no unexpanded path ever reaches the permission layer.
Substitute {JSC_ROOT} with that directory in every later call, so what runs is a literal absolute path. {JSC_ROOT}/jsc-hooks/hooks/version-guard.sh becomes, for example, /root/.jsc/current/jsc-hooks/hooks/version-guard.sh.
Resolve it once, at the start of the run. Do not re-resolve it per call. Do not add a tool that prints it.
Empty output, a non-zero exit, or a path that is not an existing directory → stop and report that $JSC_HOME/current does not resolve. The third item is the one the first two wave through, so check it: with JSC_HOME unset the command prints /current and exits 0 — non-empty, absolute, and nowhere — and every literal path built from it then names a place that is not there. Run [ -d "{the path just printed}" ] in the same approved step as the resolve, and treat only an existing directory as a root. Every cross-plugin script this skill calls lives under it.
The second root — this skill's own tools/
jsc-cli is not one of the links under {JSC_ROOT}; that directory carries jsc-assist, jsc-gitea and jsc-hooks and nothing else. So tools/detect-clis.sh, tools/deploy.sh, tools/check-requires.sh and tools/write-guides.sh cannot be reached through {JSC_ROOT}, and none of them may be written as a bare relative path either — the rule above wants a literal absolute path at every call site, and a call site with no way to build one is where a prefix gets guessed.
They sit at {plugin root}/tools/, and the plugin root is the base directory the CLI states when it loads this skill. Take that literal path verbatim, call it {CLI_ROOT}, and write all four calls as {CLI_ROOT}/tools/{script} — /root/.claude/plugins/cache/jsc/jsc-cli/0.3.2/tools/deploy.sh, for example. No command runs for this one, and it is taken once, like {JSC_ROOT}.
That base directory carries a version segment, so no allow rule covers it and each of those four calls raises an approval prompt. That is acceptable in this skill and in no unattended one: deploy runs with a person in front of it — step 3 asks them for the mode, step 4 rewrites every CLI's plugin set — so there is somebody to approve. Never carry this branch into a skill that runs from a scheduler, and never guess a prefix when the invocation states no base directory: report that this skill's own plugin root is unknown and stop, because a guessed prefix runs some other version's copy of these scripts, or nothing at all.
Done when {JSC_ROOT} holds one existing absolute directory and {CLI_ROOT} holds one literal absolute path.
Inputs a caller may pass
jsc-cli:setup already confirmed the mode with the user and already holds a fresh version report. Re-asking and re-querying would put a second decision tree in front of someone who just answered it.
| Input | Effect |
|---|---|
mode (install / update / uninstall) |
Step 3 skips the question and states which caller set the mode |
| version report | Step 1 skips its version collector; step 2 shows the report it was handed and names its source |
Nothing passed in → run every step as written below.
Steps
-
Collect the three facts the rest of the run needs. They are independent, so start all three at once and wait for all three.
- Installed CLIs —
{CLI_ROOT}/tools/detect-clis.sh, printing{name}<TAB>{path}<TAB>{version}. Exit 0 with at least one row → take the CLI list from it. Exit 0 with no row → stop, and report that none of claude, codex, copilot, antigravity, kiro is installed. Any non-zero exit → stop and report the exit code and stderr; never guess a CLI list. - Version evidence and recommendation — two subcommands of
{JSC_ROOT}/jsc-hooks/hooks/version-guard.sh, both needed, run together:reportprints the per-plugin rows{domain}<TAB>{本機}<TAB>{遠端}<TAB>{落後|最新|超前|查詢失敗}closing withbehind<TAB>{count}, andrecommendprints one single line and nothing else —recommend<TAB>{update|none|unverifiable}.recommenddeliberately never reprints the table, so its second column stays readable bycut; the version table that steps 2, 3 and 7 show comes fromreport, and the conclusion comes fromrecommend. Skip this collector when the caller passed a version report. - Domain list — read
plugins[].namefrom the unified marketplace (never hardcode it; this skill then follows automatically when domains are added or removed):{JSC_ROOT}/jsc-gitea/tools/gitea.sh api GET /repos/plugins/meta/raw/.claude-plugin/marketplace.json. Exit 0 with at least oneplugins[].name→ use that list. Exit 0 with an empty or unparseable list → stop and report that the marketplace holds no plugin entry. Any non-zero exit → stop and report the exit code and stderr; a partial domain list would install a partial skill set and look successful.
The marketplace is unified as
jsc; the install token isjsc-{domain}@jsc. Eachplugins[].namealready carries thejsc-prefix (e.g.jsc-ask) — pass it to{CLI_ROOT}/tools/deploy.shas-is, prefixed or not; the script normalizes it.Done when the CLI list holds at least one CLI, the domain list holds at least one name, and the recommendation is either in hand or explicitly inherited from the caller.
- Installed CLIs —
-
Show the
reportversion table to the user as-is. It is the evidence behind the recommendation, so never summarise it away. A report handed in by a caller is shown the same way, with a line naming that caller as its source.Branch on the
recommendline:Exit recommendvalueWhat it means and what to say 0 updateAt least one domain is behind. Mark updateas the recommended option in step 3, and name every behind domain with its local and remote version. One domain behind is enough; do not wait for a majority0 noneEvery domain row is 最新or超前. Recommend nothing; present the three options neutrally0 unverifiableThe version check could not run — no local plugin registry, or every remote lookup failed. Say so plainly and base no recommendation on it. Unverified is not the same as up to date 0 no recommendline in the output at allThis jsc-hooks build has no recommendsubcommand — an olderversion-guard.shtreats the argument as a hook invocation and exits 0 without printing anything. Derive the same three values yourself from thereporttable already collected in step 1.2: any落後row →update; no{domain}row at all, or anoregistryline →unverifiable; otherwisenone. Say in step 7's report that the recommendation came from this fallback path, not fromrecommendnon-zero — Report the version check as unverifiablewith the exit code and stderr, and carry on to step 3 without a recommendationAny single domain row reading
查詢失敗is called out by name even when the overall value isnone. An unverified domain is not the same as an up-to-date one, and must not be counted as either.Done when the table is on screen and the recommendation is stated as exactly one of
update,noneorunverifiable. -
Ask the user for the mode per the
jsc-ask:askrules:install/update/uninstall. Every option states its impact scope: which CLIs it touches and which configs it writes. A caller that already passed a mode skips this step, and the report names that caller instead.Done when the user has named exactly one of
install,updateoruninstall, or the inherited mode is named with its source. -
Run
{CLI_ROOT}/tools/deploy.sh {mode} {cli} {domain}...once per detected CLI, passing the whole domain list in one call so the marketplace command runs only once. This step MUST run as a sub agent, one sub agent per CLI, and all of them start together — the CLIs write to separate plugin directories, so serialising them only adds up their install times.The script prints
cmdandexitlines for every command, onerequiresline before each domain update, optionalcompatlines for Codex cache links, and oneresultline at the end;-nprints the commands without running them.Exit Action 0 Every command for that CLI succeeded. Record its resultline1 At least one command failed. Record that CLI as failed and quote every exitline whose code is non-zero2 Usage error — the mode, the CLI name or the domain list is wrong. Report it as a defect in this skill, and do not retry with a guessed argument other Record that CLI as failed with the exit code and stderr On update,
{CLI_ROOT}/tools/check-requires.sh {cli} {manifest}checks each domain'sjsc.requiresbefore that domain is updated. Exit 0 updates the domain as usual. Exit 1 — a missing or too-old required jsc plugin — prints awarnline and the domain is still updated: skipping it would leave a behind domain permanently unable to reach the version its dependency needs. The block lives one layer up, at skill invocation time, wherejsc-hooks/hooks/version-guard.shstops that domain's skills. That last mention is a description of where the block happens, not a call this skill makes — nothing here runsversion-guard.shexcept step 1.2, which is written as{JSC_ROOT}/jsc-hooks/hooks/version-guard.sh, so do not read it as a call site that was left un-prefixed. Exit 4 — the manifest is unreadable, is not valid JSON, or python3 is missing — prints anoteline and also still updates the domain: no verdict is not the same fact as behind, so it gets its own line rather than awarnthat would send the operator hunting for a version problem that is not there. Exit 2 or any other code — acheck-requires.shusage error or a broken script — prints askipline and leaves that domain untouched, because a checker that failed outright is not a pass. Codex update preserves oldjsc-cliandjsc-hookscache version paths as symlinks to the newest installed version, so a still-running Codex deploy can keep using its helper scripts and a still-running Codex session whose hook_run_id points at the old cache can finish withoutNo such file. Antigravity cannot install from a Gitea URL, so the script clones each domain into the local plugin directory (JSC_LOCAL_PLUGINS, default$JSC_HOME/plugins) and installs from that path — keep that clone, because update pulls the same one. That default deliberately avoids a development checkout: when the directory holds uncommitted changes or unpushed commits, the script prints askipline, leaves the tree untouched, and installs the on-disk content.Done when every detected CLI has reported an exit code and a
resultline, every skipped domain has a checker-failure reason or a local-tree reason, and everywarndomain is named with the version it still has to catch up to. -
After install or update, call
jsc-hooks:hooks-installand hand it the CLI list from step 1.1, so it does not probe the same five executables a second time.hooks-installstill detects for itself when it receives no list — that fallback is what keeps it usable on its own.Take its aggregate result rather than re-reading each CLI's smoke detail; the installer already judged purge, wiring, smoke and scan per CLI, and refreshes
{JSC_ROOT}/jsc-hookson the way — the pathdeploy.shfollows to reachrestart-gate.sh. Keep exactly one extra judgement here, because it is a deploy-side fact the installer does not rule on: a smoke result containingNo such fileis a failed update, since it means a rewritten hook path cannot execute. Report it and do not let the deploy finish as successful.Done when hooks-install has returned an aggregate verdict for every CLI in the list, and every
No such filein it is reported as an update failure. -
After install or update, run
{CLI_ROOT}/tools/write-guides.sh {mode} {domain}...once for the whole machine, after every CLI in step 4 has finished. It rewrites$JSC_HOME/update-guide.mdand$JSC_HOME/remove-guide.mdfrom the live detection result, so the later update and removal runs have the real commands for this machine. Skip it foruninstall: the guides describe an installed skill set.Exit Action 0 Both wrotelines printed. Name both paths in step 72 Usage error — the mode or the domain list is wrong. Report it as a defect in this skill; the deploy itself still stands 4 $JSC_HOMEor one of the two files could not be written. Name the path and the stderr, and say the machine has no up-to-date guide until this is fixedother Report the guide write as failed with the exit code, and say which of the two files did print a wrotelineDone when both
wrotelines are printed, or the failure is reported with the exit code and the paths involved. -
Report the run and close it, in one block. The result and any failure reason for every CLI × mode, plus every
skipline, everywarnline, every Codexcompatline, and every CLI that could not be version-checked in step 2.For install or update, the same block ends with the restart instruction, in these words: 「請關閉目前的工作階段並重新啟動,新的技能內容才會載入」.
deploy.shrecorded this round in$JSC_HOME/restart-required.d/{cli}— one file per CLI — and prints its path on arestartline;jsc-hooksreads only that CLI's own file and keeps reminding until that CLI restarts, withJSC_RESTART_GATE=offas the escape hatch. Restarting one CLI clears its own file and leaves the others' gates standing. Name the two guide paths from step 6 in that same closing block, so the operator knows where this machine's update and removal commands now live.Done when every detected CLI appears in the report with its
resultstatus, and — for install or update — the restart instruction is printed with both guide paths named, or step 6's failure is repeated in their place. -
Record how the run ended. This is the last thing this skill does, and it runs on every path out of the skill, the ones that stop at step 1 included. Call
{JSC_ROOT}/jsc-hooks/tools/report-status.sh skill-end jsc-cli:deploy {status} {exit code} [detail]{exit code}is the exit code of whatever decided the outcome — the worstdeploy.shexit of the run, or the collector that stopped step 1 — and0when nothing failed.{detail}is one short line, no more than 200 characters: the mode and the per-CLI counts fit there, thecmdandexitlines do not. If the script is not on this machine, skip this step in silence and finish the run as it stood — missing infrastructure is not a failure, and a reporting call may never change what this skill returns or reports.Record it after step 7's block, never instead of it. The event stream carries one status; the operator still needs the per-CLI report on screen.
status When this skill uses it okEvery detected CLI's deploy.shexited 0, hooks-install returned a clean verdict for each of them with noNo such filein any smoke result, and both guides printed theirwrotelineblockedNothing was deployed because there was nothing to deploy to: detect-clis.shexited 0 with no row, so none of claude, codex, copilot, antigravity, kiro is installed and the run stops before any plugin commandfailedThe run broke: the marketplace read in step 1.3 exited non-zero or returned no plugin entry, or every detected CLI's deploy.shcame back non-zero. Also used when a smoke result carriesNo such file, which this skill judges as a failed update even when hooks-install did notdegradedThe deploy landed on part of the machine only: some CLIs exited 0 while others failed, a domain was left untouched by a skipline fromcheck-requires.sh, orwrite-guides.shexited 4 so the plugins are installed but this machine has no up-to-date update and remove guideabortedThe user chose none of install,updateoruninstallat step 3, or stopped the run before step 4 launched the first CLI, so no plugin command ranDone when exactly one
skill-endline was recorded for this run, or the script was absent and the run finished without it.