refactor(shared): 「該用哪些資源」搬出 CLI——一份實作,acr 與安裝器吃同一條規則
leo 2026-08-12:「根本就不應該在 CLI,我要的是一個大家都可以用到的規則。」
「這個實例該用哪些資源」換到安裝器就要重寫一次 ⇒ 依 rules/07-thin-shell.md 的判準
它是**能力**,而它原本住在 cli/src/lib/resource-resolver.ts ⇒ 那本身就是違規。
後果已經真的發生:acr 那條有 Arcrun#97 的修法、安裝器那條沒有,於是安裝器照名字
找、找不到就建一顆空的綁上去 ⇒「我按了更新,工作流和登入全不見了」。
規則搬到 shared/resource-rule/(零依賴 ESM,Node 與 Workers runtime 都直接跑):
· rule.mjs 規則本體+把 CF 回應讀成事實的 normalizeLive*
· cf-resource-api.mjs ResourceApi 的 CF REST 實作——**眼睛也共用**:
兩條路各自解讀 CF 回應,只要一邊看不到既有綁定就會去新建,
#97 不需要規則寫錯就能重演
· installer-entry.mjs 安裝器唯一該碰的入口 resolveInstanceResources()
不是做成 cypher 端點的理由(自舉):這條規則要在「決定怎麼裝」的當下就用得到,
而那時 cypher 可能還不存在(安裝器的工作正是把它生出來);且輸入是使用者自己帳號的
綁定狀態,不該送去平台換答案。它是純函式,用不著變成服務。
只有一份,機械看守:
· 安裝器直接 import repo archive 裡的原稿,**不需要副本**
· acr 因為 npm pack 打不進套件目錄外的檔案,帶一份逐位元組鏡射
(scripts/sync-resource-rule.mjs 產生;build/test 先跑 --check,差一位元組就紅)
——同 cli/harness/ 產生物+世代閘的既有慣例
· cli/tests/single-implementation.test.ts 掃全 repo:7 支規則函式的實作只有一處
CLI 淨 -496 行(邏輯是搬走,不是複製)。cf-api.ts 的 CfAccountClient 保留公開介面,
ResourceApi 那七個方法全部委派共用 client。
驗證:cli 58/58 綠(含新增的兩條路一致性 fixture + 三種情境),tsc --noEmit 乾淨。
This commit is contained in:
@@ -0,0 +1,108 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* sync-resource-rule.mjs — 把「該用哪些資源」這條規則的**唯一原稿**同步給需要打包的呼叫端。
|
||||
*
|
||||
* 【為什麼需要這支】
|
||||
* 規則的原稿在 `shared/resource-rule/rule.mjs`(理由見該檔開頭)。
|
||||
* 兩條路取用它的方式不同:
|
||||
*
|
||||
* · **安裝器 / 任何 Worker**:本來就會下載這個 repo 的 archive 當部署來源,
|
||||
* 直接 import `shared/resource-rule/rule.mjs`。**不需要副本,本支不管它。**
|
||||
*
|
||||
* · **`acr` CLI**:`arcrun` 是獨立 npm 套件,`npm pack` 打不進套件目錄外的檔案
|
||||
* ⇒ 套件裡必須有一份。這支就是產生那一份的地方。
|
||||
*
|
||||
* 【這算不算「第二份實作」】
|
||||
* 不算,而且是機械保證的:產生物是**逐位元組副本**,`--check` 一有差就 exit 1,
|
||||
* 而 `npm run build` 與 `npm test` 都會先跑 `--check`。
|
||||
* 也就是說「有人手改了 CLI 那一份」= build 紅、publish 擋下。
|
||||
* ——同 `cli/harness/`(產生物進 repo + `check:harness` 世代閘)的既有慣例,
|
||||
* 不是為本票新發明的做法。
|
||||
*
|
||||
* 用法:
|
||||
* node scripts/sync-resource-rule.mjs 產生/更新副本
|
||||
* node scripts/sync-resource-rule.mjs --check 只檢查,有漂移就 exit 1(不寫檔)
|
||||
*/
|
||||
import { readFileSync, writeFileSync, existsSync, mkdirSync, readdirSync } from 'node:fs';
|
||||
import { join, resolve } from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { createHash } from 'node:crypto';
|
||||
|
||||
// REPO 一律由本檔位置推導,不吃 cwd(比照 scripts/build-worker-artifacts.mjs)。
|
||||
const REPO = resolve(fileURLToPath(new URL('.', import.meta.url)), '..');
|
||||
const SOURCE_DIR = join(REPO, 'shared/resource-rule');
|
||||
|
||||
/**
|
||||
* 需要「套件內自帶一份」的呼叫端。**整個目錄原樣鏡射**(不是挑檔案)——
|
||||
* 檔名與相對位置保持一致,`cf-resource-api.mjs` 裡的 `./rule.mjs` 才不用改寫。
|
||||
* 安裝器不在此列:它直接讀 repo archive 裡的原稿,連副本都不需要。
|
||||
*/
|
||||
const MIRRORS = ['cli/src/lib/resource-rule'];
|
||||
|
||||
const CHECK_ONLY = process.argv.includes('--check');
|
||||
|
||||
/** @param {string|Buffer} b */
|
||||
const sha256 = (b) => createHash('sha256').update(b).digest('hex');
|
||||
|
||||
if (!existsSync(SOURCE_DIR)) {
|
||||
console.error(`❌ 找不到規則原稿目錄:${SOURCE_DIR}`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
/** 原稿目錄裡所有 .mjs(README / 測試不進副本)。 */
|
||||
const FILES = readdirSync(SOURCE_DIR).filter((f) => f.endsWith('.mjs')).sort();
|
||||
if (FILES.length === 0) {
|
||||
console.error(`❌ ${SOURCE_DIR} 裡沒有任何 .mjs 原稿`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
let drifted = 0;
|
||||
for (const mirror of MIRRORS) {
|
||||
for (const file of FILES) {
|
||||
const src = readFileSync(join(SOURCE_DIR, file));
|
||||
const srcHash = sha256(src);
|
||||
const rel = `${mirror}/${file}`;
|
||||
const abs = join(REPO, mirror, file);
|
||||
const had = existsSync(abs) ? readFileSync(abs) : null;
|
||||
|
||||
if (had !== null && sha256(had) === srcHash) {
|
||||
console.log(`✔ ${rel} = 原稿(sha256 ${srcHash.slice(0, 12)})`);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (CHECK_ONLY) {
|
||||
drifted++;
|
||||
console.error(
|
||||
had === null
|
||||
? `✗ ${rel} 不存在——跑 \`node scripts/sync-resource-rule.mjs\` 產生。`
|
||||
: `✗ ${rel} 與原稿不一致(副本 ${sha256(had).slice(0, 12)} ≠ 原稿 ${srcHash.slice(0, 12)})。\n` +
|
||||
` 這一份是**產生物**,不要手改:規則要改就改 shared/resource-rule/${file},` +
|
||||
`然後跑 \`node scripts/sync-resource-rule.mjs\`。`,
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
mkdirSync(join(REPO, mirror), { recursive: true });
|
||||
writeFileSync(abs, src);
|
||||
console.log(`↻ ${rel} ← shared/resource-rule/${file}(sha256 ${srcHash.slice(0, 12)})`);
|
||||
}
|
||||
|
||||
// 副本目錄裡多出來的 .mjs = 有人在產生物旁邊自己加了一支(第二份實作的常見長法)。
|
||||
const mirrorAbs = join(REPO, mirror);
|
||||
const extra = existsSync(mirrorAbs)
|
||||
? readdirSync(mirrorAbs).filter((f) => f.endsWith('.mjs') && !FILES.includes(f))
|
||||
: [];
|
||||
for (const f of extra) {
|
||||
drifted++;
|
||||
console.error(`✗ ${mirror}/${f} 在原稿目錄裡不存在——副本目錄不是放自己東西的地方。`);
|
||||
}
|
||||
}
|
||||
|
||||
if (drifted > 0) {
|
||||
console.error(
|
||||
`\n❌ ${drifted} 項與規則原稿脫節。` +
|
||||
`\n「該用哪些資源」只能有一份實作(.claude/rules/07-thin-shell.md)——` +
|
||||
`副本漂移就是第二份實作偷偷長出來的樣子。`,
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
Reference in New Issue
Block a user