Files
Arcrun/scripts/sync-resource-rule.mjs
uncle6me-web bb548b6fdf 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 乾淨。
2026-08-12 23:37:01 +08:00

109 lines
4.5 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/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);
}