受託でお預かりしているサイトの保守環境を Antigravity へ移した夜のことです。初回セットアップで VS Code の設定を取り込めましたので、その時点では移行は済んだつもりでおりました。手が止まったのは、いつもどおり保存したときです。整形が走りません。拡張ビューを開くと、入っているはずのものが並んでおりませんでした。
設定は移ったのに、拡張は移っていない——同じ「取り込み」という言葉の下で、別々のことが起きておりました。
その晩に確かめたことと、足りない分をどう埋めるか決めるまでの手順を書き残します。手元の一覧は 34 本で、Open VSX 側に見当たらなかったのは 5 本、およそ 15% でした。数としては少ないのですが、この 5 本の扱いを決めるまでに二晩かかりました。
取り込めたのは設定で、拡張はもともと別の経路です
Antigravity は VS Code のコードベースから派生しておりますので、画面の作りもキーバインドも見慣れたままです。初回セットアップでは VS Code や Cursor の設定を引き継げます。ここまでは期待どおりでした。
ところが拡張機能だけは、取得先そのものが違います。Microsoft の Visual Studio Marketplace は同社の製品向けに提供されており、派生エディタからは利用できません。Antigravity が参照しているのは Eclipse Foundation が運営する Open VSX です。
つまり移行で起きているのは「拡張が消えた」ではなく、「別の棚を見に行っている」という状態です。棚が違えば、置かれている本も版も変わります。ここを取り違えると、再インストールを繰り返すだけの夜になります。私も最初の三十分はそれをしておりました。
同じ拡張 ID がどちらの棚にもある場合もあれば、片方にしかない場合もあります。さらに厄介なのは、ID は同じでも中身の出どころが違う場合です。この三つ目を見落としたのが、私の最初の失敗でした。
一覧の突き合わせは、目でやらずに機械へ渡します
拡張ビューを上から眺めて「あれが無い、これも無い」と拾う方法は、二十本を超えたあたりで破綻します。私は移行前の環境で ID の一覧を吐き出し、Open VSX の公開 API に順番に問い合わせる形へ切り替えました。
Open VSX は https://open-vsx.org/api/{publisher}/{name} で拡張の情報を JSON で返します。存在しなければ 404 が返りますので、判定はそれで足ります。
#!/usr/bin/env python3
"""reconcile_extensions.py — 手元の拡張 ID を Open VSX に突き合わせて仕分けます。
準備: 移行前の環境で code --list-extensions > installed.txt
実行: python3 reconcile_extensions.py installed.txt > ledger.tsv
出力は TSV です。state 列が absent の行が「埋め方を決めるべき拡張」になります。
"""
import json
import sys
import time
import urllib.error
import urllib.request
API = "https://open-vsx.org/api/{ns}/{name}"
UA = {"User-Agent": "ext-reconcile/1.0 (personal migration audit)"}
def lookup(ext_id):
ns, _, name = ext_id.partition(".")
if not name:
# publisher.name の形式でない行(空行やコメント)はここで落とします
return {"state": "invalid", "note": "publisher.name 形式ではありません"}
req = urllib.request.Request(API.format(ns=ns, name=name), headers=UA)
try:
with urllib.request.urlopen(req, timeout=10) as res:
data = json.load(res)
except urllib.error.HTTPError as e:
if e.code == 404:
return {"state": "absent", "note": "Open VSX に同じ ID がありません"}
if e.code == 429:
# 連続アクセスで絞られたときは、間を空けて一度だけ取り直します
time.sleep(5)
return lookup(ext_id)
return {"state": "error", "note": "HTTP {}".format(e.code)}
except urllib.error.URLError as e:
return {"state": "error", "note": str(e.reason)}
published_by = (data.get("publishedBy") or {}).get("loginName", "")
access = data.get("namespaceAccess", "")
# namespace が公開状態のままなら、所有者以外も同じ棚へ置ける余地が残っています
state = "present" if access == "restricted" else "present-unclaimed"
return {
"state": state,
"version": data.get("version", ""),
"published_by": published_by,
"access": access,
"note": "",
}
def main(path):
print("\t".join(["ext_id", "state", "version", "published_by", "access", "note"]))
for line in open(path, encoding="utf-8"):
ext_id = line.strip()
if not ext_id or ext_id.startswith("#"):
continue
r = lookup(ext_id)
print("\t".join([
ext_id,
r.get("state", ""),
r.get("version", ""),
r.get("published_by", ""),
r.get("access", ""),
r.get("note", ""),
]))
time.sleep(0.4) # 公開 API への連続アクセスを避けるための間隔です
if __name__ == "__main__":
main(sys.argv[1] if len(sys.argv) > 1 else "installed.txt")期待する出力は次の形です。
ext_id state version published_by access note
esbenp.prettier-vscode present 11.0.0 open-vsx-bot restricted
dbaeumer.vscode-eslint present 3.0.10 microsoft restricted
ms-azuretools.vscode-docker absent Open VSX に同じ ID がありませんtime.sleep を挟んでいるのは、公開 API へ短時間に何十回も投げないためです。三十本程度なら十数秒で終わります。この TSV がそのまま、あとの判断の土台になります。