التثبيت وتسجيل الدخول
تأخذك هذه الصفحة من جهاز جديد إلى طلب ترسله إلى واجهة OktoID API بعد تسجيل
الدخول. وفي نهايتها يكون الحساب الجذري قد أنشأ مؤسسةً باسم acme، وسجّل شخصًا
تحتها، وجعل ذلك الشخص مسؤول المؤسسة. ثم يكون ذلك الشخص قد سجّل الدخول وأنشأ
مؤسسةً داخل acme، ولا يستطيع الوصول إلى أي شيء خارجها.
يعمل كل شيء على جهازك، في Docker، تحت الأسماء *.local.oktoid.app. ولا توجد
وحدة تحكم بعد: تسجّل الدخول عبر صفحات مزوّد الهوية نفسه في المتصفح، وكل خطوة
أخرى أمر curl.
أما ما المؤسسات والأشخاص والحزم والتفويضات، ولماذا صيغت الطلبات أدناه على هذا النحو، فتجده في دليل الحوكمة.
قبل أن تبدأ
تحتاج إلى جهاز مُعَدّ كما يصف الملف docs/development/getting-started.md في
المستودع:
- Node 24 وpnpm، مع تنفيذ
pnpm installفي المستودع؛ - Docker مع Compose؛
- أن تُحَلّ الأسماء
*.local.oktoid.appإلى127.0.0.1؛ - تطبيق مصادقة على هاتف، لعامل التحقق الثاني (أي تطبيق يقرأ رمز QR الخاص بـ TOTP).
نفّذ كل أمر في هذه الصفحة من جذر المستودع.
يجب أن يثق متصفحك بجذر التطوير الخاص ببيئة التشغيل، لأن بيئة التشغيل هي التي
تقدّم صفحات تسجيل الدخول تحت https://id.local.oktoid.app. ينشئ أول
pnpm stack:up هذا الجذر، ويبيّن getting-started.md كيف تثق به على macOS
وLinux.
1. شغّل بيئة التشغيل
pnpm stack:up
يبني التشغيل الأول صور المنصة ويستغرق عدة دقائق. وينتهي بما يلي:
healthy in <n>s
https://id.local.oktoid.app/realms/oktoid-staff/.well-known/openid-configuration
https://api.local.oktoid.app/api/v1/version
https://docs.local.oktoid.app/
sign in: pnpm oktoid login
CA: <repository>/infra/compose/.state/local/ca/root.pem
تليه الأوامر التي تجعل المتصفح يثق بجذر التطوير. إن لم يكن المتصفح يثق به بعد، فافعل ذلك الآن.
pnpm stack:reset، ثِق بالجذر الجديدتحذف إعادة الضبط جذر التطوير، وينشئ pnpm stack:up التالي جذرًا جديدًا. وإلى
أن تثق بالجذر الجديد، يرفض المتصفح كل عنوان من عناوين *.local.oktoid.app،
بما في ذلك صفحات تسجيل الدخول في الخطوة 3. ويُبلغ Chrome عن ذلك بالرمز
ERR_CERT_AUTHORITY_INVALID.
على macOS، من جذر المستودع:
sudo security add-trusted-cert -d -r trustRoot -k /Library/Keychains/System.keychain "$PWD/infra/compose/.state/local/ca/root.pem"
ثم أغلق Chrome إغلاقًا تامًّا بالاختصار Cmd+Q وافتحه من جديد. إغلاق نوافذه لا
يكفي. ويحتاج Firefox أيضًا إلى استيراد root.pem في إعدادات الشهادات الخاصة
به.
أوامر Linux، وكيف تعثر على الجذر القديم وتزيله بعد إعادة الضبط، موجودة في الملف
docs/development/getting-started.md في المستودع، تحت القسم Trust the
development root.
تشغّل بيئة التشغيل Keycloak بوصفه مزوّد الهوية، ما لم تطلب WSO2 Identity Server:
pnpm stack:up --idp wso2
تشغّل بيئة التشغيل مزوّدًا واحدًا في كل مرة. وبقية هذه الصفحة واحدة على المزوّدين كليهما، إلا حيث تنص خطوة على غير ذلك.
آخر ما يفعله stack:up هو تشغيل الإعداد. فهو ينشئ النطاقين، ومؤسسة الارتكاز
الجذرية /oktoid، والحساب الجذري: حساب موظف لصندوق البريد
root@local.oktoid.app، يحمل الحزمة ENTITY_ADMIN على مؤسسة الارتكاز
الجذرية. وتأتي هذه كلها من ملف الإجابات الخاص بالنشر،
infra/environments/local/answers.yaml (answers.wso2.yaml لـ WSO2).
عيّن متغيرين لبقية الصفحة:
CA=infra/compose/.state/local/ca/root.pem
API=https://api.local.oktoid.app/api/v1
ثم تحقّق من أن الواجهة تجيب:
curl -sS --cacert "$CA" "$API/version"
فتُعيد {"service":"gateway","version":"..."}.
2. اعثر على دعوة الحساب الجذري
أرسل تشغيل الإعداد دعوةً إلى الحساب الجذري. وفي بيئة التشغيل المحلية لا يغادر البريد جهازك: يلتقط Mailpit كل رسالة يرسلها مزوّد الهوية.
على WSO2، يُبلغ ناتج تشغيل الإعداد عن الدعوة بوصفها ملاحظة
invitation-unconfirmed للحساب root-account. هذا متوقع وليس خطأً: يرسل WSO2
البريد في الخلفية ولا يخبر تشغيل الإعداد بما إذا كان خادم البريد قد قبل
الرسالة. أما Keycloak فيرسل وتشغيل الإعداد ينتظر، لذا يُبلَّغ على Keycloak عن
الرسالة المرفوضة بوصفها invitation-failed بدلًا من ذلك. وعلى أيٍّ من
المزوّدين، إن لم تصل أي رسالة إلى Mailpit، فتحقّق من identityProvider.mail
في ملف الإجابات ومن سجل تشغيل المزوّد (pnpm stack:logs wso2 أو
pnpm stack:logs keycloak).
- افتح http://127.0.0.1:18025 في المتصفح.
- افتح الرسالة الموجهة إلى
root@local.oktoid.appمنno-reply@local.oktoid.app. - اتبع الرابط الموجود فيها.
يحتفظ Mailpit برسائله في الذاكرة، فتختفي إذا أُعيد تشغيل حاويته. كما تنتهي
صلاحية رابط الدعوة. وفي الحالتين يكون الحساب موجودًا لكن لا يمكن إعداده من
صندوق البريد هذا؛ وتنفيذ pnpm stack:reset ثم pnpm stack:up يبدأ من جديد من
بيئة تشغيل فارغة ويرسل دعوة جديدة.
3. عيّن كلمة المرور وعامل التحقق الثاني
يفتح الرابط صفحات مزوّد الهوية نفسه. ويطلب نطاق الموظفين ثلاثة أشياء قبل أن يتمكن الحساب من تسجيل الدخول:
- التحقق من عنوان البريد الإلكتروني. يتم ذلك باتباع الرابط.
- تعيين كلمة المرور. يجب أن تستوفي سياسة كلمات المرور في نطاق الموظفين، وتذكرها الصفحة. لا يوجد رابط «نسيت كلمة المرور» في صفحة تسجيل الدخول، لذا احفظ كلمة المرور في مكان آمن.
- إعداد عامل التحقق الثاني. امسح رمز QR بتطبيق المصادقة، وأدخل الرمز المكوّن من ستة أرقام الذي يعرضه.
من الآن فصاعدًا، يطلب تسجيل الدخول بالحساب الجذري اسم المستخدم root (أو
عنوان البريد الإلكتروني)، وكلمة المرور، ورمزًا حاليًا من التطبيق.
4. سجّل الدخول من سطر الأوامر
TOKEN=$(pnpm -s oktoid login)
يفتح الأمر صفحة تسجيل الدخول لدى المزوّد في متصفحك. سجّل الدخول بالحساب
الجذري. يعرض المتصفح عندها Signed in. You can close this tab.، ويضع الأمر
رمز وصول في TOKEN. تذهب الرسائل إلى الطرفية، ولا يذهب إلى المتغير إلا الرمز.
بعد تسجيل الدخول، يسأل الأمر الواجهة عن المؤسسات التي تحمل تفويضًا لها
(GET /api/v1/me، الخطوة 5)، ويطبعها في الطرفية على هيئة جدول. وللحساب الجذري
في بيئة تشغيل جديدة، يكون الجدول صفًّا واحدًا:
PATH BUNDLE ORGANIZATION ID
/oktoid ENTITY_ADMIN <a UUID>
إذا فشل ذلك الطلب، يطبع الأمر تحذيرًا بدلًا من الجدول، ويضع الرمز في TOKEN
مع ذلك.
الرمز صالح لخمس دقائق. وعندما يجيب طلبٌ بـ 401، نفّذ الأمر نفسه من جديد. وما
دام المتصفح يحتفظ بجلسة مع المزوّد، يُعيد الأمر رمزًا جديدًا دون أن يطلب كلمة
المرور مرة أخرى.
| الخيار | متى تستخدمه |
|---|---|
--no-open | تريد أن يطبع الأمر عنوان تسجيل الدخول فقط. افتحه بنفسك، في نافذة خاصة مثلًا (الخطوة 9) |
--idp keycloak أو --idp wso2 | لا حاجة إليه عادةً: يسجّل الأمر الدخول لدى المزوّد الذي شغّله آخر pnpm stack:up |
--env ci | تسجّل الدخول إلى بيئة التشغيل ci بدلًا من local |
--acting-for <path> | تريد أيضًا معرّف المؤسسة الموجودة على ذلك المسار، مثل /oktoid. يطبع الأمر سطرًا ثانيًا، ORG=<id>، بعد الرمز (الخطوة 5). وإن لم تكن تحمل تفويضًا على ذلك المسار، يطبع خطأً ويخرج بالحالة 1، بعد سطر الرمز |
يسجّل الأمر الدخول بإجراء رمز التخويل مع PKCE، بوصفه العميل العام
operator-cli. لا تمر أي كلمة مرور عبر الأمر، ولا يتيح المزوّد أي طريقة
لاستبدال كلمة المرور برمز مباشرةً.
5. اعثر على معرّف مؤسسة الارتكاز الجذرية
كل خطوة من هنا فصاعدًا تعيد استخدام متغيرات الصدفة التي عيّنتها خطوة سابقة:
CA وAPI من الخطوة 1، وTOKEN وANCHOR من هذه الخطوة، وSLUG وACME
وPERSON من الخطوتين 6 و7، والدالتين المساعدتين key وfield المعرّفتين
أدناه. الصق الأوامر في نافذة الطرفية نفسها، بالترتيب. فأي نافذة أو تبويب جديد
يبدأ من دونها.
تعني 401 أن رمز الوصول قد انتهت صلاحيته، فهو يدوم خمس دقائق. أعد تنفيذ سطر
تسجيل الدخول للمستخدم الذي كان الرمز له: الأسطر OUT= وTOKEN= وANCHOR= في
هذه الخطوة للحساب الجذري، أو السطر PERSON_TOKEN= في الخطوة 9 للشخص. ثم أعد
الطلب الذي فشل. تحتفظ المتغيرات الأخرى بقيمها.
يذكر الشخص دائمًا المؤسسة التي يتصرّف نيابةً عنها، بالترويسة
Oktoid-Acting-For ومعرّف المؤسسة. والحساب الجذري يتصرّف نيابةً عن مؤسسة
الارتكاز الجذرية، /oktoid.
GET /api/v1/me هو الطلب الوحيد الذي لا يحتاج إلى Oktoid-Acting-For. فهو
يسرد المؤسسات التي تحمل تفويضًا نشطًا لها، ومع كل منها المعرّف الذي ترسله في
تلك الترويسة:
curl -sS --cacert "$CA" "$API/me" -H "Authorization: Bearer $TOKEN"
وللحساب الجذري يُعيد:
{"grants":[{"organizationId":"<a UUID>","path":"/oktoid","bundle":"ENTITY_ADMIN"}]}
القائمة الفارغة ليست خطأً: معناها أنك لا تحمل أي تفويض نشط في أي مكان.
يُجري pnpm oktoid login الطلب نفسه، ومع --acting-for يعطيك المعرّف مباشرةً.
سجّل الدخول من جديد بهذه الطريقة لتعيين المتغيرين معًا:
OUT=$(pnpm -s oktoid login --acting-for /oktoid)
TOKEN=$(echo "$OUT" | sed -n 1p)
ANCHOR=$(echo "$OUT" | sed -n 2p); ANCHOR=${ANCHOR#ORG=}
echo "$ANCHOR"
السطر الأول من الناتج هو الرمز، والثاني هو ORG=<id>؛ أما جدول التفويضات فيذهب
إلى الطرفية، لا إلى OUT. ويطبع الأمر الأخير UUID واحدًا. وما دام المتصفح
يحتفظ بجلسته مع المزوّد، لا يطلب تسجيل الدخول الثاني هذا كلمة المرور مرة أخرى.
كل طلب يغيّر شيئًا من هنا فصاعدًا يحمل أيضًا Idempotency-Key: قيمة جديدة لكل
تغيير جديد، والقيمة نفسها مرة أخرى إذا أعدت محاولة ذلك التغيير. وتنشئ هذه
الصفحة مفتاح عدم التكرار بـ Node:
key() { node -p 'crypto.randomUUID()'; }
وتقرأ حقلًا من ردّ JSON بما يلي:
field() { node -e 'let s="";process.stdin.on("data",d=>s+=d).on("end",()=>{let v;try{v=JSON.parse(s)[process.argv[1]]}catch{}if(v===undefined){console.error(s||"no reply");process.exit(1)}console.log(v)})' "$1"; }
عندما لا يحتوي الرد على ذلك الحقل، لأن الطلب رُفض، يطبع field الرد كاملًا في
الطرفية، ولا يعيّن شيئًا، ويفشل. وعندها تتوقف الخطوات التالية عند سطر يذكر
المتغير الناقص، بدلًا من أن تمضي بمتغير فارغ.
6. أنشئ مؤسسة
SLUG=acme
ACME=$(curl -sS --fail-with-body --cacert "$CA" "$API/subjects" \
-H "Authorization: Bearer $TOKEN" \
-H "Oktoid-Acting-For: $ANCHOR" \
-H "Idempotency-Key: $(key)" \
-H 'Content-Type: application/json' \
-d "{\"kind\":\"organization\",\"slug\":\"$SLUG\",\"displayName\":\"Acme\"}" \
| field id)
echo "${ACME:?step 6 made no organization: read the reply above}"
تجيب الواجهة بـ 201 مع المؤسسة الجديدة. ومسارها path هو /oktoid/acme:
تُنشأ المؤسسة الجديدة تحت المؤسسة التي تتصرّف نيابةً عنها، ما لم يذكر متن
الطلب parentPath آخر داخلها.
إذا طبع السطر الأخير ACME: step 6 made no organization بدلًا من UUID، فقد
رُفض الطلب، والرد الظاهر فوقه يذكر السبب. لا تنتقل إلى الخطوة 7.
عندما تكون acme موجودة من قبل، لأنك نفّذت هذه الخطوة سابقًا مثلًا، يكون
الرد 409 مع "code": "subject.slug_taken". الاسم المختصر فريد في المنصة
كلها، أينما وقعت المؤسسة، لأنه يصير جزءًا من اسم مضيف (hostname). يذكر الرد أن الاسم
مستخدم فحسب، ولا يوجد في هذا الإصدار طلب يعثر على مؤسسة باسمها المختصر، فاختر
اسمًا مختصرًا آخر: عيّن SLUG بقيمة جديدة، مثل
SLUG=acme2، ونفّذ الطلب من جديد. تستخدم الخطوات من 7 إلى 9 المتغير $SLUG،
فتتبع القيمة الجديدة. وإذا كانت هذه الطرفية تحتفظ بالمعرّف من التنفيذ الذي أنشأ
acme، يطبعه echo "$ACME" ويمكنك أن تتابع من الخطوة 7 بدلًا من ذلك. وللبدء
من جديد ببيئة تشغيل فارغة، نفّذ pnpm stack:reset ثم pnpm stack:up.
7. سجّل شخصًا وادعُه
PERSON=$(curl -sS --fail-with-body --cacert "$CA" "$API/persons" \
-H "Authorization: Bearer $TOKEN" \
-H "Oktoid-Acting-For: $ANCHOR" \
-H "Idempotency-Key: $(key)" \
-H 'Content-Type: application/json' \
-d "{\"givenName\":\"Amal\",\"familyName\":\"Haddad\",\"email\":\"amal.haddad@acme.example\",\"parentPath\":\"/oktoid/${SLUG:?run step 6 first}\",\"invite\":true}" \
| field id)
echo "${PERSON:?step 7 registered no person: read the reply above}"
تجيب الواجهة بـ 201 مع الشخص. ومساره path هو /oktoid/acme/<id>: يقع
الشخص تحت المؤسسة التي سجّلته.
تطلب "invite": true من المنصة أن تنشئ حساب موظف للشخص لدى مزوّد الهوية وأن
ترسل إليه دعوة. يحدث ذلك بعد الرد مباشرةً، لا أثناءه، ويُعاد إن كان المزوّد غير
متاح لوقت قصير. وفي غضون ثوانٍ تظهر الدعوة في Mailpit، موجهةً إلى
amal.haddad@acme.example. يلتقط Mailpit البريد الموجه إلى أي عنوان، لذا لا
يلزم أن يكون اسم النطاق للبريد الإلكتروني في العنوان موجودًا.
نفّذ هذه الخطوة مرة واحدة. فكل تنفيذ يسجّل شخصًا آخر، حتى إن كان PERSON معيّنًا
من قبل. وإذا ذكر السطر الأخير PERSON بدلًا من أن يطبع UUID، فلم يُسجَّل أحد،
ويمكنك تنفيذها من جديد.
8. اجعل الشخص مسؤول المؤسسة
curl -sS --fail-with-body --cacert "$CA" "$API/grants" \
-H "Authorization: Bearer $TOKEN" \
-H "Oktoid-Acting-For: $ANCHOR" \
-H "Idempotency-Key: $(key)" \
-H 'Content-Type: application/json' \
-d "{\"granteeId\":\"${PERSON:?run step 7 first}\",\"organizationId\":\"${ACME:?run step 6 first}\",\"bundle\":\"ENTITY_ADMIN\"}"
تجيب الواجهة بـ 201 مع التفويض، "status":"active". ويستطيع الشخص الآن أن
يتصرّف نيابةً عن acme وكل ما تحتها، بصلاحيات ENTITY_ADMIN.
إذا لم يكن PERSON أو ACME معيّنًا، تتوقف الصدفة مع run step 7 first أو
run step 6 first ولا ترسل شيئًا. وتنفيذ هذه الخطوة مرة ثانية يجيب بـ 409 مع
"code": "grant.exists": الشخص يحمل التفويض من قبل، ولم يتغير شيء.
9. يسجّل الشخص الدخول
يتبع الشخص دعوته، كما في الخطوتين 2 و3: يتحقق من العنوان، ويعيّن كلمة المرور، ويُعِدّ عامل التحقق الثاني.
لا يزال متصفحك مسجّل الدخول بالحساب الجذري، وسيُدخل المزوّد الشخص بوصفه root دون أن يسأل. استخدم نافذة خاصة للشخص:
PERSON_TOKEN=$(pnpm -s oktoid login --no-open)
انسخ العنوان الذي يطبعه الأمر إلى نافذة خاصة، وسجّل الدخول بحساب الشخص. يعرض
الجدول الذي يطبعه الأمر تفويضه الوحيد، /oktoid/acme مع ENTITY_ADMIN،
ومعرّف مؤسسته هو القيمة نفسها التي في $ACME. والشخص الذي لم يُعطَ ذلك المعرّف
يعرفه بهذه الطريقة، أو من GET /api/v1/me (الخطوة 5)، أو بالخيار
--acting-for /oktoid/acme.
ثم أنشئ مؤسسة داخل acme، متصرّفًا نيابةً عن acme:
curl -sS -i --cacert "$CA" "$API/subjects" \
-H "Authorization: Bearer $PERSON_TOKEN" \
-H "Oktoid-Acting-For: ${ACME:?run step 6 first}" \
-H "Idempotency-Key: $(key)" \
-H 'Content-Type: application/json' \
-d '{"kind":"organization","slug":"branch","displayName":"Branch"}'
تجيب الواجهة بـ 201، ومسار path المؤسسة الجديدة هو /oktoid/acme/branch.
ولا يستطيع الشخص نفسه الوصول إلى ما هو خارج acme. فطلب مؤسسة تحت مؤسسة
الارتكاز الجذرية:
curl -sS -i --cacert "$CA" "$API/subjects" \
-H "Authorization: Bearer $PERSON_TOKEN" \
-H "Oktoid-Acting-For: $ACME" \
-H "Idempotency-Key: $(key)" \
-H 'Content-Type: application/json' \
-d '{"kind":"organization","slug":"elsewhere","displayName":"Elsewhere","parentPath":"/oktoid"}'
يجيب بـ 404، كما لو كانت المؤسسة الأم غير موجودة. أما التصرّف نيابةً عن مؤسسة
الارتكاز الجذرية بدلًا من ذلك (Oktoid-Acting-For: $ANCHOR) فيجيب بـ 403،
لأن الشخص لا يحمل تفويضًا هناك.
عندما يُرفض طلب
كل رفض متنٌ من نوع application/problem+json يحمل code وcorrelationId.
اذكر معرّف الارتباط عندما تطلب المساعدة: فهو يدلّ على الطلب في سجل تشغيل كل
خدمة.
| ما تراه | معناه | ما تفعله |
|---|---|---|
401 | لا يوجد رمز، أو انتهت صلاحية الرمز | نفّذ pnpm -s oktoid login من جديد |
403 على أي طلب | لا توجد الترويسة Oktoid-Acting-For، أو لا تحمل تفويضًا للمؤسسة التي تذكرها | أرسل الترويسة، مع معرّف مؤسسة تحمل تفويضًا لها. يسردها GET /api/v1/me |
404 | الشيء خارج المؤسسة التي تتصرّف نيابةً عنها، أو غير موجود، أو أن تفويضك هناك يفتقر إلى الصلاحية التي يحتاجها الطلب. مثال ذلك أن GET /api/v1/grants يجيب بـ 404 لحامل ORGANIZATION_CLIENT، لأنها لا تتيح قراءة التفويضات. ولا يمكن التمييز بين الحالات الثلاث، عن قصد | تحقّق من المعرّف، ومن المؤسسة التي تتصرّف نيابةً عنها، ومن أن حزمتك تحمل الصلاحية (الحزم) |
409 مع "code": "subject.slug_taken" | توجد مؤسسة بذلك الاسم المختصر في مكان ما من المنصة. قد تكون مؤسسة لا تستطيع قراءتها، ولذلك لا يذكر الرد موضعها | اختر اسمًا مختصرًا آخر. تبيّن الخطوة 6 كيف |
400 مع "code": "subject.slug_reserved" | الاسم المختصر اسم تستخدمه المنصة لأحد مضيفاتها، مثل api أو admin أو setup | اختر اسمًا مختصرًا آخر |
409 مع "code": "grant.exists" | يحمل الشخص من قبل تفويضًا نشطًا لتلك المؤسسة | اقرأه بـ GET /api/v1/grants?grantee=<id>، أو اسحب التفويض أولًا لتغيير الحزمة |
409 مع "code": "idempotency.key_reused" | أُرسل Idempotency-Key نفسه مع متن مختلف | استخدم مفتاح عدم تكرار جديدًا لكل تغيير جديد |
409 مع "code": "person.invitations_unavailable" | لم يُهيَّأ للنشر مزوّد هوية للدعوات. لم يُنشأ أحد | أرسل الطلب دون invite، أو اطلب من المشغّل تهيئة المزوّد |
400 | المتن أو إحدى الترويسات غير سليمة الصيغة. يذكر المتن الحقل | صحّح الحقل |
429 | طلبات كثيرة جدًّا في دقيقة واحدة | انتظر عدد الثواني المذكور في Retry-After |
502 من api.local.oktoid.app | البوابة لا تعمل، وسبب ذلك عادةً أن بيئة التشغيل في وضع البنية التحتية | نفّذ pnpm stack:up |
تجيب المنصة بـ 404 لا بـ 403 عند غياب الصلاحية، كي لا يكشف الرفض أبدًا ما
هو موجود. فالرد 403 بمعنى «لا يحق لك قراءة هذا» يؤكد أن الشيء موجود ويمكن
قراءته؛ أما 404 فيُقرأ بالطريقة نفسها سواء كان الشيء غير موجود، أو خارج
متناولك، أو ليس لك أن تراه. ولا تتعلق 403 إلا بالطالب نفسه، كغياب الترويسة
Oktoid-Acting-For، ولا تتعلق أبدًا بالشيء المطلوب.
لإيقاف بيئة التشغيل، نفّذ pnpm stack:down؛ تبقى بياناتك. أما
pnpm stack:reset فيزيل كل شيء، بما في ذلك الحساب الجذري وجذر التطوير.