@jadmadi/hasan-ui-cli 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +148 -0
- package/dist/index.js +663 -0
- package/package.json +32 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
## Hasan-UI License Declaration
|
|
2
|
+
|
|
3
|
+
هذا العمل هو وَقْفٌ لله تعالى تحت شروط رُخصة وَقْف الرَّقْمِيَّة العامَّة - الإصدار الأول.
|
|
4
|
+
المُستوى المُطَبَّق هو: وَقْفٌ خَيْرِيٌّ مُلْزِمٌ بالإِسْنَادِ (WaqfDPL-Khayri-Mulzim 1.0).
|
|
5
|
+
|
|
6
|
+
Full license text below.
|
|
7
|
+
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
# **رُخصة وَقْف الرَّقْمِيَّة العامَّة - الإصدار الأول (Waqf-DPL 1.0)**
|
|
11
|
+
|
|
12
|
+
الإصدار الأول من رُخصة وَقْف الرَّقْمِيَّة العامَّة، 1447هـ
|
|
13
|
+
|
|
14
|
+
## **ديباجة: مفهوم الوَقْف في هذه الرُخصة**
|
|
15
|
+
|
|
16
|
+
هذه الرُخصة هي أداةٌ شرعيَّة وقانونيَّة لتطبيق مفهوم الوَقْف الإسلامي على الأعمال الفكرية والرقمية. الوَقْف هو "حَبْس الأصل وتَسْبِيل المنفعة"، أي جَعْل العمل الفكري بحد ذاته وَقْفًا لله تعالى لا يُباع ولا يُوهَب، مع إتاحة منفعته لعموم الناس كصَدَقَةٍ جاريةٍ يرجو بها الواقف الأجر من الله.
|
|
17
|
+
|
|
18
|
+
باستخدامك أو توزيعك للعمل المُرخَّص بموجب هذه الرُخصة، فإنك تُقِر وتوافق على شروطها باعتبارها عَقْدًا مُلْزِمًا بينك وبين الواقف. نحن نؤمن بأنّ العِلم والمعرفة النافعة يجب أن تكون مُتاحة للجميع، وهذه الرُخصة هي بديلٌ إسلاميٌّ يُوازِن بين حق المُبتكِر في الأجر الدائم وحق الأُمَّة في الانتفاع.
|
|
19
|
+
|
|
20
|
+
## **1\. تعريفات**
|
|
21
|
+
|
|
22
|
+
* **العمل**: أيُّ مادَّة فكرية أو رقمية (مثل نصوص بَرْمَجِيَّة، كُتُب، ملفات صوتية، مرئية، صُوَر، وغيرها) يضعها صاحبها تحت هذه الرُخصة.
|
|
23
|
+
* **الواقف**: هو صاحب العمل الأصلي الذي يملك الحقوق الفكرية ويختار طَوْعًا أن يُوقِف عمله تحت هذه الرُخصة.
|
|
24
|
+
* **المُنتفِع**: هو أيُّ شخص أو جهة تستخدم العمل أو تستفيد منه بأي شكلٍ من الأشكال.
|
|
25
|
+
* **الناظر**: هو الشخص الطبيعي أو الاعتباري المعيَّن من قِبل الواقف (أو من تؤول إليه نظارة الوقف شرعاً وقانوناً بعد وفاة الواقف أو عجزه) لإدارة الوقف وحمايته وإنفاذ شروطه، ويمثل الجهة المخولة باتخاذ الإجراءات الإدارية والقانونية لحماية الوقف (مثل إلغاء الرخصة عن المخالفين أو إعادة الترخيص لهم). ويُثبت التعيين بقرار مكتوب من الواقف، أو توقيع رقمي معتمد، أو بقرار من الجهة الشرعية/القضائية المختصة عند شغور النظارة. ويلتزم الناظر بالرعاية والرقابة المستمرة؛ وبما أن أتمتة المهام الوقفية برمجياً (كالفحص التلقائي والفسخ الوقائي الآلي للأكواد) جائزة ومندوبة شرعاً لتسهيل العمل وتوفير الوقت وضبط الالتزام، فإن مسؤولية الناظر تتركز في التدخل البشري لمعالجة الأعطال وإصلاح الثغرات البرمجية والأمنية فور حدوثها، ويُعتبر إهمال الأنظمة البرمجية الوقفية بعد تعطلها أو تجاهل معالجة ثغراتها تفريطاً يوجب المسؤولية والضمان شرعاً. ويُعفى الناظر من الضمان والمسؤولية في حالات القوة القاهرة والأعطال التقنية الخارجة عن إرادته (مثل تعطل خوادم شركة الاستضافة الخارجية، أو الاختراقات السيبرانية القاهرة بالرغم من اتخاذه معايير الحماية الأمنية المناسبة لقدرات الوقف). ويملك الناظر صلاحية استبدال الوقف الرقمي (الاستبدال والتعويض الفني) بنقله لبيئات تقنية أحدث أو تصفية أصوله وتوجيهها لمشروع برمجي آخر يحقق مصلحة وقفية راجحة عند تقادم التقنية الأصلية أو كثرة تكاليف تشغيلها.
|
|
26
|
+
* **العمل** **المُشْتَقّ**: هو أيُّ عمل جديد ينتج عن تعديل العمل الأصلي أو ترجمته أو الاقتباس منه أو البناء عليه.
|
|
27
|
+
* **الترجمة** **المجهولة**: هي ترجمة العمل إلى لغة أخرى دون الإسناد الصريح والواضح إلى الواقف الأصلي أو دون ذكر الرخصة الأصلية التي يخضع لها العمل (مثل نشر الترجمة باسم المترجم فقط دون ذكر الواقف أو الرخصة).
|
|
28
|
+
* **الاستخدام** **التجاري**: هو أي استخدام للعمل أو لمشتقاته يكون الغرض الأساسي منه تحقيق ربح مادي مباشر أو غير مباشر، أو تحقيق مكاسب مالية وتجارية خاصة، بصرف النظر عن الطبيعة القانونية للجهة المستخدمة للعمل (سواء كانت تجارية، حكومية، أو غير ربحية). ويُستثنى من ذلك المكاسب الثانوية التبعية غير المقصودة للمنتفع (مثل زيادة زوار الموقع الشخصي للمنتفع نتيجة لعرض المصنف الوقفي فيه بما يزيد من أرباح إعلانات موقعه الإجمالية بصفة تبعية غير مقصودة بالذات للمصنف)، كما يُستثنى من ذلك آليات "الدعم الذاتي" (Self-Funding) التي تهدف حصراً لتغطية تكاليف التشغيل والصيانة الفنية للوقف (مثل وضع روابط التبرعات، أو تفعيل وسيط إعلاني منضبط بضوابط الشريعة لتغطية فواتير الخوادم والتشغيل فقط دون تحقيق أرباح شخصية خاصة)، طالما كان أصل عرض المصنف مجانياً لعموم الناس ولم يتم بيعه أو استغلاله للتربح بشكل مباشر مقصود.
|
|
29
|
+
* **حماية بيانات المستفيدين وخصوصيتهم (أمانة البيانات)**: تُعد البيانات الشخصية والسرية للمستفيدين من الوقف الرقمي جزءاً لا يتجزأ من أمانة الوقف المعظّمة شرعاً وقانوناً؛ وبناءً عليه يلتزم المنتفع بوضع تدابير الحماية الأمنية المناسبة لخصوصية المستخدمين، ويُمنع منعاً باتاً استغلال بياناتهم تجارياً أو تسريبها أو التفريط في حمايتها، ويُعتبر خرق خصوصية بيانات المستفيدين أو بيعها بمثابة خرق جسيم لشروط الوقف يوجب بطلان رخصة الانتفاع وسقوطها فوراً.
|
|
30
|
+
* **الرُخصة**: هذه الوثيقة بجميع بنودها ومستوياتها.
|
|
31
|
+
|
|
32
|
+
## **2\. مستويات الرُخصة**
|
|
33
|
+
|
|
34
|
+
يمنح الواقف الحقوق التالية للمُنتفِع شريطة اختيار أحد المستويات السبعة التالية وذكره بوضوح مع العمل:
|
|
35
|
+
|
|
36
|
+
### **المستوى الأول: وَقْفٌ بالإِسْنَادِ (WaqfDPL-Isnad)**
|
|
37
|
+
|
|
38
|
+
*الإسناد (Attribution): هذا المستوى هو الأكثر تساهلاً، ويسمح بالانتفاع المُطْلَق مع حفظ الحق الأدبي.*
|
|
39
|
+
يحق لك بحرية:
|
|
40
|
+
|
|
41
|
+
* **المشاركة**: نَسْخ وتوزيع العمل بأي وسيط أو شكل.
|
|
42
|
+
* **التعديل**: تعديل العمل وإعادة دمجه والبناء عليه.
|
|
43
|
+
* **الاستخدام**: استخدام العمل لأي غرض، بما في ذلك الأغراض التجارية.
|
|
44
|
+
|
|
45
|
+
**بالشرط الوحيد التالي:**
|
|
46
|
+
|
|
47
|
+
* **الإسناد**: يجب عليك نِسْبَة العمل إلى الواقف الأصلي وذِكْر هذه الرُخصة (WaqfDPL-Isnad 1.0) ورابطها إن أمكن، دون الإيحاء بأنّ الواقف يتبنَّى عملَك المُشْتَقّ.
|
|
48
|
+
|
|
49
|
+
### **المستوى الثاني: وَقْفٌ مُلْزِمٌ بالإِسْنَادِ (WaqfDPL-Mulzim)**
|
|
50
|
+
|
|
51
|
+
*المُلْزِم (Binding/ShareAlike): هذا المستوى يضمن استمرارية الوقف في الأعمال المشتقة (Copyleft - حقوق متروكة).*
|
|
52
|
+
**يحق لك بحرية:**
|
|
53
|
+
|
|
54
|
+
* **المشاركة**: نَسْخ وتوزيع العمل بأي وسيط أو شكل.
|
|
55
|
+
* **التعديل**: تعديل العمل وإعادة دمجه والبناء عليه.
|
|
56
|
+
* **الاستخدام**: استخدام العمل لأي غرض، بما في ذلك الأغراض التجارية.
|
|
57
|
+
|
|
58
|
+
**بالشروط التالية:**
|
|
59
|
+
|
|
60
|
+
* **الإسناد**: يجب عليك نِسْبَة العمل إلى الواقف الأصلي وذِكْر هذه الرُخصة (WaqfDPL-Mulzim 1.0).
|
|
61
|
+
* **الإلزام**: إذا قمت بتعديل العمل أو بنيت عليه، فيجب عليك توزيع عملك المُشْتَقّ تحت نفس الرُخصة (WaqfDPL-Mulzim 1.0) حَصْرًا. هذا الشرط يُلْزِم باستمرارية الوقف ويُحقِّق مبدأ "حَبْس الأصل".
|
|
62
|
+
|
|
63
|
+
### **المستوى الثالث: وَقْفٌ خَيْرِيٌّ بالإِسْنَادِ (WaqfDPL-Khayri)**
|
|
64
|
+
|
|
65
|
+
*الخَيْرِيّ (NonCommercial): هذا المستوى يَقْصُر الانتفاع على الأغراض غير الربحية والخيرية.*
|
|
66
|
+
**يحق لك بحرية:**
|
|
67
|
+
|
|
68
|
+
* **المشاركة**: نَسْخ وتوزيع العمل بأي وسيط أو شكل.
|
|
69
|
+
* **التعديل**: تعديل العمل وإعادة دمجه والبناء عليه، على ألا تكون رخصة العمل المشتق أكثر تقييداً من رخصة الأصل لضمان عدم تضييق المنفعة الوقفية.
|
|
70
|
+
|
|
71
|
+
**بالشروط التالية:**
|
|
72
|
+
|
|
73
|
+
* **الإسناد**: يجب عليك نِسْبَة العمل إلى الواقف الأصلي وذِكْر هذه الرُخصة (WaqfDPL-Khayri 1.0).
|
|
74
|
+
* **الاستخدام** **الخيري**: لا يجوز لك استخدام العمل لأغراض تجارية أو ربحية بشكلٍ أساسي، ويقتصر على الأغراض الشخصية والخيرية والتعليمية والدعوية.
|
|
75
|
+
|
|
76
|
+
###
|
|
77
|
+
|
|
78
|
+
### **المستوى الرابع: وَقْفٌ خَيْرِيٌّ مُلْزِمٌ بالإِسْنَادِ (WaqfDPL-Khayri-Mulzim)**
|
|
79
|
+
|
|
80
|
+
*الخيري الملزم (NonCommercial-ShareAlike): هذا المستوى يدمج حظر الربح المادي مع اشتراط استمرارية النية الوقفية في جميع التعديلات والأعمال المشتقة.*
|
|
81
|
+
**يحق لك بحرية:**
|
|
82
|
+
|
|
83
|
+
* **المشاركة**: نَسْخ وتوزيع العمل بأي وسيط أو شكل.
|
|
84
|
+
* **التعديل**: تعديل العمل وإعادة دمجه والبناء عليه.
|
|
85
|
+
|
|
86
|
+
**بالشروط التالية:**
|
|
87
|
+
|
|
88
|
+
* **الإسناد**: يجب عليك نِسْبَة العمل إلى الواقف الأصلي وذِكْر هذه الرُخصة (WaqfDPL-Khayri-Mulzim 1.0).
|
|
89
|
+
* **الاستخدام** **الخيري**: لا يجوز لك استخدام العمل لأغراض تجارية أو ربحية مادية مباشرة.
|
|
90
|
+
* **الإلزام** **الخيري**: إذا قمت بتعديل العمل أو بنيت عليه، فيجب عليك توزيع عملك المُشْتَقّ تحت نفس الرُخصة (WaqfDPL-Khayri-Mulzim 1.0) حَصْرًا وبشكل خيري ومفتوح للجميع.
|
|
91
|
+
|
|
92
|
+
### **المستوى الخامس: وَقْفٌ مَحْفُوظٌ بالإِسْنَادِ (WaqfDPL-Mahfudh)**
|
|
93
|
+
|
|
94
|
+
*المَحْفُوظ (NoDerivatives): هذا المستوى يحفظ العمل على صورته الأصلية ويمنع تعديله أو ترجمته أو اقتباسه، مع تعميم منفعته بنشره ككتلة صلبة واحدة.*
|
|
95
|
+
**يحق لك بحرية:**
|
|
96
|
+
|
|
97
|
+
* **المشاركة**: نَسْخ وتوزيع العمل بصورته الأصلية فقط بأي وسيط أو شكل ولأي غرض، بما في ذلك التجاري.
|
|
98
|
+
|
|
99
|
+
**بالشروط التالية:**
|
|
100
|
+
|
|
101
|
+
* **الإسناد**: يجب عليك نِسْبَة العمل إلى الواقف الأصلي وذِكْر هذه الرُخصة (WaqfDPL-Mahfudh 1.0).
|
|
102
|
+
* **الحِفْظ**: لا يجوز للمنتفعين تعديل العمل أو البناء عليه، ويُمنع توزيع أي عمل مُشْتَقٍّ منه، ويُمنع منعاً باتاً إجراء أي ترجمة أو اقتباس للعمل بأي شكلٍ من الأشكال. ويُستثنى من ذلك التحديثات الفنية أو أعمال الصيانة البرمجية اللازمة التي يقوم بها الواقف أو الناظر المعتمد حمايةً للوقف من الموت التقني والتقادم.
|
|
103
|
+
|
|
104
|
+
### **المستوى السادس: وَقْفٌ خَيْرِيٌّ مَحْفُوظٌ بالإِسْنَادِ (WaqfDPL-Khayri-Mahfudh)**
|
|
105
|
+
|
|
106
|
+
*الخيري المحفوظ (NonCommercial-NoDerivatives): هذا المستوى يجمع بين حظر الربح المادي ومنع أي تعديل أو تحوير للعمل لحماية سلامته التامة وموثوقيته.*
|
|
107
|
+
**يحق لك بحرية:**
|
|
108
|
+
|
|
109
|
+
* **المشاركة**: نَسْخ وتوزيع العمل بصورته الأصلية فقط مجاناً في جميع القطاعات الخيرية والدعوية والتعليمية والشخصية.
|
|
110
|
+
|
|
111
|
+
**بالشروط التالية:**
|
|
112
|
+
|
|
113
|
+
* الإسناد: يجب عليك نِسْبَة العمل إلى الواقف الأصلي وذِكْر هذه الرُخصة (WaqfDPL-Khayri-Mahfudh 1.0).
|
|
114
|
+
* **الاستخدام** **الخيري**: يمنع منعاً باتاً البيع أو الاستخدام التجاري الربحي للعمل.
|
|
115
|
+
* **الحفظ** **التام**: لا يجوز للمنتفعين تعديل العمل أو دمج أجزاء منه أو قصها وتجزئتها، ويُمنع منعاً باتاً إجراء أي ترجمة أو اقتباس للعمل بأي شكلٍ من الأشكال. ويُستثنى من ذلك التحديثات الفنية أو أعمال الصيانة البرمجية اللازمة التي يقوم بها الواقف أو الناظر المعتمد حمايةً للوقف من الموت التقني والتقادم.
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
### **المستوى السابع: وَقْفٌ مُقَيَّدٌ بالإِسْنَادِ (WaqfDPL-Muqayyad)**
|
|
120
|
+
|
|
121
|
+
*المُقَيَّد (Restricted): هذا المستوى يسمح للواقف بوضع شرط إضافي خاص به، وهو بمثابة وعاء مرن (Wrapper) يُطبق إلزاماً استناداً إلى أحد المستويات الستة القياسية السابقة.*
|
|
122
|
+
**صيغة التطبيق الإلزامية للمستوى السابع:**
|
|
123
|
+
|
|
124
|
+
* **المستوى** **الأساسي** **المطبَّق:** \[يجب ذكر أحد المستويات الستة الأولى حصراً كأساس للرخصة، مثلاً: المستوى الثالث: الوقف الخيري (WaqfDPL-Khayri 1.0)\].
|
|
125
|
+
* **الشرط** **الخاص** **الإضافي** **للواقف**: \[اكتب شرطك الخاص هنا بوضوح تام، مثال: "يُقصر منفعة هذا العمل على الأغراض التعليمية لجمعيات ودور رعاية الأيتام فقط"\].
|
|
126
|
+
|
|
127
|
+
**قواعد التوافق ومنع التناقض الهيكلي (Consistency & Non-Contradiction Rules)**: للحفاظ على تماسك الرخصة وصلاحيتها القانونية، يخضع الشرط الخاص لقاعدة عدم التناقض مع المستوى الأساسي المختار:
|
|
128
|
+
|
|
129
|
+
1. **قاعدة حظر التجارة**: إذا أراد الواقف منع الاستخدام التجاري بالشرط الخاص، يجب عليه اختيار مستوى أساسي يمنع التجارة أصلاً (المستويات 3 أو 4 أو 6). ويُمنع منعاً باتاً اختيار أساس يبيح التجارة (مثل المستويات 1 أو 2\) ثم حظرها بالشرط الخاص.
|
|
130
|
+
2. **قاعدة حظر التعديل**: إذا أراد الواقف منع التعديل بالشرط الخاص، يجب عليه اختيار مستوى أساسي محفوظ يمنع التعديل (المستويات 5 أو 6). ويُمنع اختيار أساس يبيح التعديل (مثل المستويات 1 أو 2\) ثم حظره بالشرط الخاص.
|
|
131
|
+
3. **قاعدة الأولوية والفساد**: في حال وجود أي تعارض أو تناقض صريح بين الشرط الخاص والمستوى الأساسي المختار، يُعتبر الشرط الخاص باطلاً ومُلغى وتُطبق شروط المستوى القياسي الأساسي المختار حمايةً لاستقرار الانتفاع ومصلحة الوقف العامة.
|
|
132
|
+
|
|
133
|
+
## **3\. أحكام عامَّة (تنطبق على جميع المستويات)**
|
|
134
|
+
|
|
135
|
+
* **الغرض الشرعي (سقوط الإذن بالاستخدام المحرم)**: هذا العمل هو وَقْفٌ لله تعالى لخدمة المجتمع ونشر النفع العام. ويُشترط شرعاً وقانوناً ألا يُسخر العمل أو مشتقاته في أي أغراض تخالف مبادئ الشريعة الإسلامية ومحرماتها الصارخة (مثل استخدام العمل أو دمجه في منصات القمار، أو المحتوى الإباحي، أو ترويج الشبهات العقدية الهدامة، أو المعاملات الربوية الصريحة). والواقف والناظر غير مسؤولين عن أي استخدام غير مشروع يقوم به المُنتفِع، ويُعتبر توجيه العمل لهذه المحرمات خروجاً صريحاً عن غاية الوقف يبطل عقد الإذن ويسقط رخصة الاستخدام فوراً وبشكل تلقائي عن المخالف.
|
|
136
|
+
* **الدوام** **والأبديَّة**: هذه الرُخصة أبديَّةٌ ولا تُلغَى. إذا انتهت مُدَّة حماية حقوق المِلكية الفكرية وِفْقًا للقوانين المحلية، ينتقل العمل إلى المِلْك العامّ (Public Domain)، مع بقاء نيَّة الوَقْف كصَدَقَةٍ جاريةٍ قائمةً شرعًا.
|
|
137
|
+
* **إلغاء الرخصة التلقائي عند المخالفة (الفسخ الوقائي التلقائي)**: تسقط هذه الرخصة وتُلغى حقوق الاستخدام والتوزيع الممنوحة بموجبها تلقائياً وفوراً بمجرد وقوع أي مخالفة أو خرق لشروطها من قِبل المُنتفِع (مثل عدم الإسناد، أو الاستخدام التجاري في المستويات الخيرية، أو التعديل والترجمة والاقتباس المحظور في المستويات المحفوظة، أو الترجمة دون إسناد في المستويات الأخرى، أو خرق خصوصية بيانات المستفيدين واستغلالها تجارياً، أو توجيه العمل في المحرمات الشرعية الصارخة). ويترتب على هذا الإلغاء التلقائي سقوط كافة الحقوق فوراً وتوقف المخالف عن أي منفعة جديدة، مع منحه مهلة تقنية لا تتجاوز 30 يوماً فقط للإزالة المادية والحذف لكافة النسخ التي قام بنشرها أو توزيعها مسبقاً، دون أن يؤثر ذلك على بقية المنتفعين الملتزمين. وللواقف أو الناظر إخطار المخالف رسمياً بالإلغاء، ولا يجوز إعادة الترخيص للمخالف إلا بطلب جديد وموافقة صريحة من الواقف أو الناظر بعد تصحيح المخالفة تماماً.
|
|
138
|
+
* **عدم الضمان وإخلاء المسؤولية**: يُقدِّم الواقف العمل "كما هو" دون أي ضمانات من أي نوع، صريحةً أو ضمنيَّة. لا يتحمَّل الواقف أي مسؤولية عن أي أضرار مباشرة أو غير مباشرة قد تنشأ عن استخدام العمل، إلى أقصى حدٍّ يسمح به القانون.
|
|
139
|
+
* **استخدامات الذكاء الاصطناعي والتعلُّم الآلي**: يخضع استخدام العمل في تدريب النماذج البرمجية أو أنظمة الذكاء الاصطناعي وتوليد البيانات لنفس شروط الوقف؛ حيث يلتزم المُنتفِع بالحفاظ على الإسناد للواقف، كما يُمنع منعاً باتاً استخدام العمل أو أجزائه أو مشتقاته في تدريب أي نماذج تُوزَّع تجارياً أو تُستخدم للتربح المادي والربح التجاري الخاص في جميع المستويات الخيرية (المستويات 3 و4 و6).
|
|
140
|
+
* **اقتصار الرخصة على الصيغة الرقمية**: تنطبق هذه الرخصة على المصنف في صيغته الرقمية والبرمجية والإلكترونية فقط (سواء كان كوداً، أو تطبيقاً، أو كتاباً إلكترونياً، أو وثيقة رقمية). ونشير إلى أن نسخ المصنف الرقمي أو حفظه على وسيط مادي ملموس (كأجهزة الحاسب، أو الخوادم، أو الأقراص المدمجة، أو بطاقات الذاكرة) لا يُخرجه عن كونه وقفاً رقمياً خاضعاً لأحكام هذه الرخصة؛ حيث يظل المحتوى الرقمي المخزن داخلها وقفاً رقمياً، بينما يُعتبر الوعاء المادي الحامل له (القرص أو الجهاز المادي) وقفاً منقولاً موازياً. ويخرج المصنف عن النطاق الرقمي للرخصة فقط في حال طباعته مادية على ورق فيزيائي، حيث يخضع حينها للوقف المنقول التقليدي.
|
|
141
|
+
* **دمج الأوقاف الرقمية (Waqf Integration)**: يجوز دمج المصنف الموقوف بموجب هذه الرخصة مع مصنف وقفي رقمي آخر خاضع لرخصة وقف (أو رخصة متوافقة)، لتتحد إدارتهما تحت ناظر أو إدارة واحدة، إذا كان ذلك يحقق مصلحة راجحة، كزيادة النفع العام وتخفيض التكاليف والجهود التقنية، بشرط ألا يؤدي هذا الدمج إلى نزاع أو تصادم مع الشروط الجوهرية للواقفين الأصليين.
|
|
142
|
+
* الإصدارات المستقبلية: قد تُصدِر "وَقْف" إصدارات جديدة من هذه الرُخصة. يبقى العمل مُرخَّصًا بالإصدار الذي تم نشره به، وللواقف الخيار في إعادة ترخيصه بالإصدارات الأحدث.
|
|
143
|
+
|
|
144
|
+
## **ثانياً: كيفية تطبيق الرخصة وتخصيصها للمستويات الأخرى (قالب التخصيص)**
|
|
145
|
+
|
|
146
|
+
لتطبيق هذه الرُخصة وتخصيصها لمستوى مختلف من المستويات السبعة على عملك، قُمْ بتضمين النص التالي في مكانٍ بارز (مثل ملف README في مشروع بَرْمَجِي، أو في صفحة حقوق النشر الإلكترونية في كتاب رقمي):
|
|
147
|
+
|
|
148
|
+
"هذا العمل هو وَقْفٌ لله تعالى تحت شروط رُخصة وَقْف الرَّقْمِيَّة العامَّة - الإصدار الأول. المُستوى المُطَبَّق هو: \[اذكر هنا اسم المستوى، مثلاً: وَقْفٌ مُلْزِمٌ بالإِسْنَادِ (WaqfDPL-Mulzim 1.0)\]. \[في حال اختيار "الوقف المقيد"، أضف السطر التالي:\] الشرط الخاص للواقف هو: \[اكتب شرطك هنا\]. يمكن الاطلاع على نسخة كاملة من الرُخصة هنا: \[رابط إلى نص الرُخصة\]"
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,663 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/index.ts
|
|
4
|
+
import { Command } from "commander";
|
|
5
|
+
|
|
6
|
+
// src/add.ts
|
|
7
|
+
import { mkdirSync, writeFileSync, existsSync } from "fs";
|
|
8
|
+
import { join as join2, dirname } from "path";
|
|
9
|
+
import { execSync } from "child_process";
|
|
10
|
+
|
|
11
|
+
// src/registry.ts
|
|
12
|
+
import { readFileSync } from "fs";
|
|
13
|
+
import { join } from "path";
|
|
14
|
+
var GITHUB_API = "https://api.github.com/repos/WaqfTech/hasan-ui/contents";
|
|
15
|
+
function getHeaders() {
|
|
16
|
+
const token = process.env.GITHUB_TOKEN || process.env.GH_TOKEN;
|
|
17
|
+
if (token) {
|
|
18
|
+
return { Authorization: `token ${token}` };
|
|
19
|
+
}
|
|
20
|
+
return {};
|
|
21
|
+
}
|
|
22
|
+
async function fetchGithubJson(path) {
|
|
23
|
+
const normalizedPath = path.replace(/^\.\//, "");
|
|
24
|
+
const response = await fetch(`${GITHUB_API}/${normalizedPath}`, { headers: getHeaders() });
|
|
25
|
+
if (!response.ok) throw new Error(`GitHub API error: ${response.status}`);
|
|
26
|
+
const data = await response.json();
|
|
27
|
+
const content = Buffer.from(data.content, "base64").toString("utf-8");
|
|
28
|
+
return JSON.parse(content);
|
|
29
|
+
}
|
|
30
|
+
async function fetchGithubText(path) {
|
|
31
|
+
const normalizedPath = path.replace(/^\.\//, "");
|
|
32
|
+
const response = await fetch(`${GITHUB_API}/${normalizedPath}`, { headers: getHeaders() });
|
|
33
|
+
if (!response.ok) throw new Error(`GitHub API error: ${response.status}`);
|
|
34
|
+
const data = await response.json();
|
|
35
|
+
return Buffer.from(data.content, "base64").toString("utf-8");
|
|
36
|
+
}
|
|
37
|
+
async function fetchRegistryFromGithub() {
|
|
38
|
+
const allItems = [];
|
|
39
|
+
async function fetchWithIncludes(path) {
|
|
40
|
+
try {
|
|
41
|
+
const registry = await fetchGithubJson(path);
|
|
42
|
+
if (registry.items) {
|
|
43
|
+
const itemsWithPath = registry.items.map((item) => ({
|
|
44
|
+
...item,
|
|
45
|
+
_registryPath: path
|
|
46
|
+
}));
|
|
47
|
+
allItems.push(...itemsWithPath);
|
|
48
|
+
}
|
|
49
|
+
if (registry.include) {
|
|
50
|
+
const basePath = path.substring(0, path.lastIndexOf("/") + 1);
|
|
51
|
+
for (const inc of registry.include) {
|
|
52
|
+
let resolvedPath;
|
|
53
|
+
if (inc.startsWith("registry/")) {
|
|
54
|
+
resolvedPath = inc;
|
|
55
|
+
} else {
|
|
56
|
+
const normalizedInc = inc.replace(/^\.\//, "");
|
|
57
|
+
resolvedPath = `${basePath}${normalizedInc}`;
|
|
58
|
+
}
|
|
59
|
+
await fetchWithIncludes(resolvedPath);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
} catch {
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
await fetchWithIncludes("registry/registry.json");
|
|
66
|
+
return allItems.filter(
|
|
67
|
+
(item) => item.type !== "registry:collection" && item.name && item.titleEn
|
|
68
|
+
);
|
|
69
|
+
}
|
|
70
|
+
async function fetchRegistry() {
|
|
71
|
+
try {
|
|
72
|
+
const items = await fetchRegistryFromGithub();
|
|
73
|
+
if (items.length > 0) return items;
|
|
74
|
+
} catch {
|
|
75
|
+
}
|
|
76
|
+
try {
|
|
77
|
+
let resolveLocal2 = function(path) {
|
|
78
|
+
const fullPath = join(process.cwd(), path);
|
|
79
|
+
if (visited.has(fullPath)) return;
|
|
80
|
+
visited.add(fullPath);
|
|
81
|
+
let data;
|
|
82
|
+
try {
|
|
83
|
+
data = JSON.parse(readFileSync(fullPath, "utf-8"));
|
|
84
|
+
} catch {
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
if (data.items) {
|
|
88
|
+
const itemsWithPath = data.items.map((item) => ({
|
|
89
|
+
...item,
|
|
90
|
+
_registryPath: path
|
|
91
|
+
}));
|
|
92
|
+
allItems.push(...itemsWithPath);
|
|
93
|
+
}
|
|
94
|
+
if (data.include) {
|
|
95
|
+
const basePath = path.substring(0, path.lastIndexOf("/") + 1);
|
|
96
|
+
for (const inc of data.include) {
|
|
97
|
+
let resolvedPath;
|
|
98
|
+
if (inc.startsWith("registry/")) {
|
|
99
|
+
resolvedPath = inc;
|
|
100
|
+
} else {
|
|
101
|
+
resolvedPath = `${basePath}${inc.replace(/^\.\//, "")}`;
|
|
102
|
+
}
|
|
103
|
+
resolveLocal2(resolvedPath);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
};
|
|
107
|
+
var resolveLocal = resolveLocal2;
|
|
108
|
+
const allItems = [];
|
|
109
|
+
const visited = /* @__PURE__ */ new Set();
|
|
110
|
+
resolveLocal2("registry/registry.json");
|
|
111
|
+
const filtered = allItems.filter(
|
|
112
|
+
(item) => item.type !== "registry:collection" && item.name && item.titleEn
|
|
113
|
+
);
|
|
114
|
+
if (filtered.length > 0) return filtered;
|
|
115
|
+
} catch {
|
|
116
|
+
}
|
|
117
|
+
console.error("Could not fetch registry. Options:");
|
|
118
|
+
console.error(" 1. Set GITHUB_TOKEN env var for private repo access");
|
|
119
|
+
console.error(" 2. Run from the hasan-ui directory");
|
|
120
|
+
console.error(" 3. Make the repo public");
|
|
121
|
+
process.exit(1);
|
|
122
|
+
}
|
|
123
|
+
async function fetchFileContent(filePath, registryPath) {
|
|
124
|
+
let fullPath = filePath;
|
|
125
|
+
if (registryPath) {
|
|
126
|
+
const registryDir = registryPath.substring(0, registryPath.lastIndexOf("/") + 1);
|
|
127
|
+
fullPath = `${registryDir}${filePath}`;
|
|
128
|
+
}
|
|
129
|
+
try {
|
|
130
|
+
return await fetchGithubText(fullPath);
|
|
131
|
+
} catch {
|
|
132
|
+
}
|
|
133
|
+
try {
|
|
134
|
+
const localPath = join(process.cwd(), "registry", filePath);
|
|
135
|
+
return readFileSync(localPath, "utf-8");
|
|
136
|
+
} catch {
|
|
137
|
+
}
|
|
138
|
+
return null;
|
|
139
|
+
}
|
|
140
|
+
function findItem(items, name) {
|
|
141
|
+
return items.find((item) => item.name === name);
|
|
142
|
+
}
|
|
143
|
+
function findItemsByPattern(items, pattern) {
|
|
144
|
+
return items.filter((item) => item.name.startsWith(pattern));
|
|
145
|
+
}
|
|
146
|
+
function filterByType(items, type) {
|
|
147
|
+
return items.filter((item) => item.tags?.type?.includes(type));
|
|
148
|
+
}
|
|
149
|
+
function filterByPurpose(items, purpose) {
|
|
150
|
+
return items.filter((item) => item.tags?.purposes?.includes(purpose));
|
|
151
|
+
}
|
|
152
|
+
async function fetchCollections() {
|
|
153
|
+
try {
|
|
154
|
+
const collections = [];
|
|
155
|
+
async function fetchCollectionFiles(path) {
|
|
156
|
+
try {
|
|
157
|
+
const registry = await fetchGithubJson(path);
|
|
158
|
+
if (registry.items?.length) {
|
|
159
|
+
collections.push(...registry.items.map((c) => ({ ...c, _registryPath: path })));
|
|
160
|
+
}
|
|
161
|
+
if (registry.include) {
|
|
162
|
+
const basePath = path.substring(0, path.lastIndexOf("/") + 1);
|
|
163
|
+
for (const inc of registry.include) {
|
|
164
|
+
let resolvedPath;
|
|
165
|
+
if (inc.startsWith("registry/")) {
|
|
166
|
+
resolvedPath = inc;
|
|
167
|
+
} else {
|
|
168
|
+
resolvedPath = `${basePath}${inc.replace(/^\.\//, "")}`;
|
|
169
|
+
}
|
|
170
|
+
try {
|
|
171
|
+
const collection = await fetchGithubJson(resolvedPath);
|
|
172
|
+
collections.push({ ...collection, _registryPath: resolvedPath });
|
|
173
|
+
} catch {
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
} catch {
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
await fetchCollectionFiles("registry/collections/registry.json");
|
|
181
|
+
if (collections.length > 0) return collections;
|
|
182
|
+
} catch {
|
|
183
|
+
}
|
|
184
|
+
try {
|
|
185
|
+
const collections = [];
|
|
186
|
+
const collectionsDir = join(process.cwd(), "registry", "collections");
|
|
187
|
+
const registryPath = join(collectionsDir, "registry.json");
|
|
188
|
+
const registry = JSON.parse(readFileSync(registryPath, "utf-8"));
|
|
189
|
+
if (registry.include) {
|
|
190
|
+
for (const inc of registry.include) {
|
|
191
|
+
const incPath = join(collectionsDir, inc.replace(/^\.\//, ""));
|
|
192
|
+
try {
|
|
193
|
+
const collection = JSON.parse(readFileSync(incPath, "utf-8"));
|
|
194
|
+
collections.push(collection);
|
|
195
|
+
} catch {
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
if (registry.items?.length) {
|
|
200
|
+
collections.push(...registry.items.map((c) => ({ ...c, _registryPath: "registry/collections/registry.json" })));
|
|
201
|
+
}
|
|
202
|
+
return collections;
|
|
203
|
+
} catch {
|
|
204
|
+
}
|
|
205
|
+
return [];
|
|
206
|
+
}
|
|
207
|
+
function findCollection(collections, name) {
|
|
208
|
+
return collections.find((c) => c.name === name);
|
|
209
|
+
}
|
|
210
|
+
function decodeCartUrl(url) {
|
|
211
|
+
try {
|
|
212
|
+
const match = url.match(/\/c\/([A-Za-z0-9_-]+)$/);
|
|
213
|
+
if (!match) return null;
|
|
214
|
+
const json = Buffer.from(match[1], "base64url").toString("utf-8");
|
|
215
|
+
const payload = JSON.parse(json);
|
|
216
|
+
return payload.items;
|
|
217
|
+
} catch {
|
|
218
|
+
return null;
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// src/add.ts
|
|
223
|
+
function ensureDir(filePath) {
|
|
224
|
+
const dir = dirname(filePath);
|
|
225
|
+
if (!existsSync(dir)) {
|
|
226
|
+
mkdirSync(dir, { recursive: true });
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
function installDependencies(deps) {
|
|
230
|
+
if (deps.length === 0) return;
|
|
231
|
+
const hasYarn = existsSync("yarn.lock");
|
|
232
|
+
const hasPnpm = existsSync("pnpm-lock.yaml");
|
|
233
|
+
let cmd;
|
|
234
|
+
if (hasPnpm) {
|
|
235
|
+
cmd = `pnpm add ${deps.join(" ")}`;
|
|
236
|
+
} else if (hasYarn) {
|
|
237
|
+
cmd = `yarn add ${deps.join(" ")}`;
|
|
238
|
+
} else {
|
|
239
|
+
cmd = `npm install ${deps.join(" ")}`;
|
|
240
|
+
}
|
|
241
|
+
console.log(`Installing dependencies: ${deps.join(", ")}`);
|
|
242
|
+
execSync(cmd, { stdio: "inherit" });
|
|
243
|
+
}
|
|
244
|
+
async function add(names, options) {
|
|
245
|
+
if (options.collection) {
|
|
246
|
+
const cartItems = decodeCartUrl(options.collection);
|
|
247
|
+
if (cartItems) {
|
|
248
|
+
console.log(`Installing ${cartItems.length} items from cart URL...`);
|
|
249
|
+
await addMultiple(cartItems, options);
|
|
250
|
+
return;
|
|
251
|
+
}
|
|
252
|
+
const collections2 = await fetchCollections();
|
|
253
|
+
const collection2 = findCollection(collections2, options.collection);
|
|
254
|
+
if (!collection2) {
|
|
255
|
+
console.error(`Collection "${options.collection}" not found`);
|
|
256
|
+
console.error(
|
|
257
|
+
`Available: ${collections2.map((c) => c.name).join(", ") || "none"}`
|
|
258
|
+
);
|
|
259
|
+
process.exit(1);
|
|
260
|
+
}
|
|
261
|
+
console.log(`Installing collection: ${collection2.name} (${collection2.titleEn})`);
|
|
262
|
+
console.log(` ${collection2.descriptionEn}`);
|
|
263
|
+
console.log(` Items: ${collection2.items.join(", ")}
|
|
264
|
+
`);
|
|
265
|
+
await addMultiple(collection2.items, options);
|
|
266
|
+
return;
|
|
267
|
+
}
|
|
268
|
+
const nameList = Array.isArray(names) ? names : [names];
|
|
269
|
+
if (nameList.length > 1) {
|
|
270
|
+
await addMultiple(nameList, options);
|
|
271
|
+
return;
|
|
272
|
+
}
|
|
273
|
+
const singleName = nameList[0];
|
|
274
|
+
const collections = await fetchCollections();
|
|
275
|
+
const collection = findCollection(collections, singleName);
|
|
276
|
+
if (collection) {
|
|
277
|
+
console.log(`Installing collection: ${collection.name} (${collection.titleEn})`);
|
|
278
|
+
console.log(` ${collection.descriptionEn}`);
|
|
279
|
+
console.log(` Items: ${collection.items.join(", ")}
|
|
280
|
+
`);
|
|
281
|
+
await addMultiple(collection.items, options);
|
|
282
|
+
return;
|
|
283
|
+
}
|
|
284
|
+
const items = await fetchRegistry();
|
|
285
|
+
const item = findItem(items, singleName);
|
|
286
|
+
if (!item) {
|
|
287
|
+
const matches = findItemsByPattern(items, singleName);
|
|
288
|
+
if (matches.length === 0) {
|
|
289
|
+
console.error(`Component "${singleName}" not found in registry`);
|
|
290
|
+
process.exit(1);
|
|
291
|
+
}
|
|
292
|
+
for (const match of matches) {
|
|
293
|
+
await addSingle(match, options);
|
|
294
|
+
}
|
|
295
|
+
return;
|
|
296
|
+
}
|
|
297
|
+
await addSingle(item, options);
|
|
298
|
+
}
|
|
299
|
+
async function addMultiple(names, options) {
|
|
300
|
+
const items = await fetchRegistry();
|
|
301
|
+
const installed = /* @__PURE__ */ new Set();
|
|
302
|
+
for (const name of names) {
|
|
303
|
+
if (installed.has(name)) continue;
|
|
304
|
+
const item = findItem(items, name);
|
|
305
|
+
if (!item) {
|
|
306
|
+
console.error(`Component "${name}" not found in registry, skipping`);
|
|
307
|
+
continue;
|
|
308
|
+
}
|
|
309
|
+
await addSingle(item, options);
|
|
310
|
+
installed.add(name);
|
|
311
|
+
}
|
|
312
|
+
console.log(`
|
|
313
|
+
\u2713 Installed ${installed.size} components`);
|
|
314
|
+
}
|
|
315
|
+
async function addSingle(item, options) {
|
|
316
|
+
console.log(`Adding ${item.name} (${item.titleEn})...`);
|
|
317
|
+
if (item.dependencies?.length) {
|
|
318
|
+
installDependencies(item.dependencies);
|
|
319
|
+
}
|
|
320
|
+
for (const file of item.files) {
|
|
321
|
+
const targetPath = join2(process.cwd(), file.target);
|
|
322
|
+
if (existsSync(targetPath) && !options.overwrite) {
|
|
323
|
+
console.log(` Skipping ${file.target} (already exists)`);
|
|
324
|
+
continue;
|
|
325
|
+
}
|
|
326
|
+
ensureDir(targetPath);
|
|
327
|
+
const sourceContent = await fetchFileContent(file.path, item._registryPath);
|
|
328
|
+
if (sourceContent) {
|
|
329
|
+
writeFileSync(targetPath, sourceContent);
|
|
330
|
+
} else {
|
|
331
|
+
const content = `// ${item.name} - ${item.titleEn}
|
|
332
|
+
// ${item.descriptionEn}
|
|
333
|
+
// Source: ${item.title}
|
|
334
|
+
|
|
335
|
+
export default function ${item.name.replace(/-/g, "")}() {
|
|
336
|
+
return <div>${item.title}</div>
|
|
337
|
+
}
|
|
338
|
+
`;
|
|
339
|
+
writeFileSync(targetPath, content);
|
|
340
|
+
}
|
|
341
|
+
console.log(` Created ${file.target}`);
|
|
342
|
+
}
|
|
343
|
+
console.log(`\u2713 Added ${item.name}`);
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
// src/list.ts
|
|
347
|
+
async function list(options) {
|
|
348
|
+
if (options.collections) {
|
|
349
|
+
await listCollections();
|
|
350
|
+
return;
|
|
351
|
+
}
|
|
352
|
+
let items = await fetchRegistry();
|
|
353
|
+
if (items.length === 0) {
|
|
354
|
+
console.log("No components found in registry.");
|
|
355
|
+
return;
|
|
356
|
+
}
|
|
357
|
+
if (options.type) {
|
|
358
|
+
items = filterByType(items, options.type);
|
|
359
|
+
if (items.length === 0) {
|
|
360
|
+
console.log(`No components with type "${options.type}".`);
|
|
361
|
+
return;
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
if (options.purpose) {
|
|
365
|
+
items = filterByPurpose(items, options.purpose);
|
|
366
|
+
if (items.length === 0) {
|
|
367
|
+
console.log(`No components with purpose "${options.purpose}".`);
|
|
368
|
+
return;
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
const grouped = {};
|
|
372
|
+
for (const item of items) {
|
|
373
|
+
const path = item._registryPath || "";
|
|
374
|
+
let category = "other";
|
|
375
|
+
if (path.includes("/forms/")) category = "forms";
|
|
376
|
+
else if (path.includes("/navigation/")) category = "navigation";
|
|
377
|
+
else if (path.includes("/data-display/")) category = "data-display";
|
|
378
|
+
else if (path.includes("/feedback/")) category = "feedback";
|
|
379
|
+
else if (path.includes("/modals/")) category = "modals";
|
|
380
|
+
else if (path.includes("/layout/")) category = "layout";
|
|
381
|
+
else if (path.includes("/media/")) category = "media";
|
|
382
|
+
else if (path.includes("/blocks/")) category = "blocks";
|
|
383
|
+
else if (path.includes("/intents/")) category = "intents";
|
|
384
|
+
if (!grouped[category]) grouped[category] = [];
|
|
385
|
+
grouped[category].push(item);
|
|
386
|
+
}
|
|
387
|
+
const categories = options.category ? [options.category] : Object.keys(grouped).sort();
|
|
388
|
+
console.log("\n hasan-ui components\n");
|
|
389
|
+
for (const cat of categories) {
|
|
390
|
+
const catItems = grouped[cat];
|
|
391
|
+
if (!catItems || catItems.length === 0) {
|
|
392
|
+
console.log(` ${cat}: no components found`);
|
|
393
|
+
continue;
|
|
394
|
+
}
|
|
395
|
+
console.log(` ${cat} (${catItems.length})`);
|
|
396
|
+
for (const item of catItems) {
|
|
397
|
+
const tags = item.tags ? ` [${item.tags.type.join(",")}/${item.tags.purposes.join(",")}]` : "";
|
|
398
|
+
console.log(` ${item.name.padEnd(20)} ${item.titleEn}${tags}`);
|
|
399
|
+
}
|
|
400
|
+
console.log();
|
|
401
|
+
}
|
|
402
|
+
console.log(` Total: ${items.length} components
|
|
403
|
+
`);
|
|
404
|
+
console.log(" Usage: hasan-ui add <component-name>");
|
|
405
|
+
console.log(" hasan-ui add <name1> <name2> <name3>");
|
|
406
|
+
console.log(" hasan-ui add collection <name>\n");
|
|
407
|
+
}
|
|
408
|
+
async function listCollections() {
|
|
409
|
+
const collections = await fetchCollections();
|
|
410
|
+
if (collections.length === 0) {
|
|
411
|
+
console.log("\n No collections found in registry.\n");
|
|
412
|
+
return;
|
|
413
|
+
}
|
|
414
|
+
console.log("\n hasan-ui collections\n");
|
|
415
|
+
for (const col of collections) {
|
|
416
|
+
console.log(` ${col.name.padEnd(25)} ${col.titleEn}`);
|
|
417
|
+
console.log(` ${col.descriptionEn}`);
|
|
418
|
+
console.log(` Items: ${col.items.join(", ")}`);
|
|
419
|
+
console.log(` Purposes: ${col.tags?.purposes?.join(", ") || "n/a"}`);
|
|
420
|
+
console.log();
|
|
421
|
+
}
|
|
422
|
+
console.log(` Total: ${collections.length} collections
|
|
423
|
+
`);
|
|
424
|
+
console.log(" Usage: hasan-ui add collection <name>");
|
|
425
|
+
console.log(" hasan-ui add --collection <name>\n");
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
// src/init.ts
|
|
429
|
+
import { existsSync as existsSync2, mkdirSync as mkdirSync2, writeFileSync as writeFileSync2 } from "fs";
|
|
430
|
+
import { join as join3 } from "path";
|
|
431
|
+
import { execSync as execSync2 } from "child_process";
|
|
432
|
+
var DEFAULT_GLOBALS_CSS = `@import "tailwindcss";
|
|
433
|
+
|
|
434
|
+
@custom-variant dark (&:is(.dark *));
|
|
435
|
+
|
|
436
|
+
@theme {
|
|
437
|
+
--color-background: hsl(var(--background));
|
|
438
|
+
--color-foreground: hsl(var(--foreground));
|
|
439
|
+
--color-card: hsl(var(--card));
|
|
440
|
+
--color-card-foreground: hsl(var(--card-foreground));
|
|
441
|
+
--color-popover: hsl(var(--popover));
|
|
442
|
+
--color-popover-foreground: hsl(var(--popover-foreground));
|
|
443
|
+
--color-primary: hsl(var(--primary));
|
|
444
|
+
--color-primary-foreground: hsl(var(--primary-foreground));
|
|
445
|
+
--color-secondary: hsl(var(--secondary));
|
|
446
|
+
--color-secondary-foreground: hsl(var(--secondary-foreground));
|
|
447
|
+
--color-muted: hsl(var(--muted));
|
|
448
|
+
--color-muted-foreground: hsl(var(--muted-foreground));
|
|
449
|
+
--color-accent: hsl(var(--accent));
|
|
450
|
+
--color-accent-foreground: hsl(var(--accent-foreground));
|
|
451
|
+
--color-destructive: hsl(var(--destructive));
|
|
452
|
+
--color-destructive-foreground: hsl(var(--destructive-foreground));
|
|
453
|
+
--color-border: hsl(var(--border));
|
|
454
|
+
--color-input: hsl(var(--input));
|
|
455
|
+
--color-ring: hsl(var(--ring));
|
|
456
|
+
--radius-sm: calc(var(--radius) - 4px);
|
|
457
|
+
--radius-md: calc(var(--radius) - 2px);
|
|
458
|
+
--radius-lg: var(--radius);
|
|
459
|
+
--radius-xl: calc(var(--radius) + 4px);
|
|
460
|
+
--font-sans: var(--font-arabic), ui-sans-serif, system-ui, sans-serif;
|
|
461
|
+
--font-mono: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
|
|
462
|
+
--font-arabic: 'Noto Kufi Arabic', ui-sans-serif, system-ui, sans-serif;
|
|
463
|
+
--font-latin: 'Inter', ui-sans-serif, system-ui, sans-serif;
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
:root {
|
|
467
|
+
--radius: 0.625rem;
|
|
468
|
+
--background: 40 15% 97%;
|
|
469
|
+
--foreground: 30 7% 21%;
|
|
470
|
+
--card: 40 15% 95%;
|
|
471
|
+
--card-foreground: 30 7% 21%;
|
|
472
|
+
--popover: 40 15% 95%;
|
|
473
|
+
--popover-foreground: 30 7% 21%;
|
|
474
|
+
--primary: 28 78% 60%;
|
|
475
|
+
--primary-foreground: 0 0% 100%;
|
|
476
|
+
--secondary: 40 15% 94%;
|
|
477
|
+
--secondary-foreground: 30 7% 21%;
|
|
478
|
+
--muted: 30 10% 90%;
|
|
479
|
+
--muted-foreground: 30 7% 50%;
|
|
480
|
+
--accent: 36 95% 66%;
|
|
481
|
+
--accent-foreground: 30 7% 21%;
|
|
482
|
+
--destructive: 0 72% 51%;
|
|
483
|
+
--destructive-foreground: 0 0% 100%;
|
|
484
|
+
--border: 30 10% 88%;
|
|
485
|
+
--input: 30 10% 88%;
|
|
486
|
+
--ring: 28 78% 60%;
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
.dark {
|
|
490
|
+
--background: 30 7% 15%;
|
|
491
|
+
--foreground: 40 15% 92%;
|
|
492
|
+
--card: 30 7% 18%;
|
|
493
|
+
--card-foreground: 40 15% 92%;
|
|
494
|
+
--popover: 30 7% 18%;
|
|
495
|
+
--popover-foreground: 40 15% 92%;
|
|
496
|
+
--primary: 28 78% 60%;
|
|
497
|
+
--primary-foreground: 30 7% 15%;
|
|
498
|
+
--secondary: 30 7% 22%;
|
|
499
|
+
--secondary-foreground: 40 15% 92%;
|
|
500
|
+
--muted: 30 7% 25%;
|
|
501
|
+
--muted-foreground: 30 7% 60%;
|
|
502
|
+
--accent: 36 95% 66%;
|
|
503
|
+
--accent-foreground: 30 7% 15%;
|
|
504
|
+
--destructive: 0 62% 50%;
|
|
505
|
+
--destructive-foreground: 0 0% 100%;
|
|
506
|
+
--border: 30 7% 28%;
|
|
507
|
+
--input: 30 7% 28%;
|
|
508
|
+
--ring: 28 78% 60%;
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
@layer base {
|
|
512
|
+
* {
|
|
513
|
+
@apply border-border;
|
|
514
|
+
}
|
|
515
|
+
html {
|
|
516
|
+
direction: rtl;
|
|
517
|
+
text-align: start;
|
|
518
|
+
}
|
|
519
|
+
body {
|
|
520
|
+
@apply bg-background text-foreground;
|
|
521
|
+
font-family: var(--font-arabic), var(--font-latin), sans-serif;
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
@utility rtl-flip {
|
|
526
|
+
transform: scaleX(-1);
|
|
527
|
+
}
|
|
528
|
+
`;
|
|
529
|
+
var DEFAULT_COMPONENTS_JSON = `{
|
|
530
|
+
"$schema": "https://ui.shadcn.com/schema.json",
|
|
531
|
+
"style": "new-york",
|
|
532
|
+
"rsc": true,
|
|
533
|
+
"tsx": true,
|
|
534
|
+
"tailwind": {
|
|
535
|
+
"css": "src/globals.css",
|
|
536
|
+
"baseColor": "neutral",
|
|
537
|
+
"cssVariables": true,
|
|
538
|
+
"prefix": ""
|
|
539
|
+
},
|
|
540
|
+
"aliases": {
|
|
541
|
+
"components": "@/components",
|
|
542
|
+
"utils": "@/lib/utils",
|
|
543
|
+
"ui": "@/components/ui",
|
|
544
|
+
"lib": "@/lib",
|
|
545
|
+
"hooks": "@/hooks"
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
`;
|
|
549
|
+
var DEFAULT_UTILS_TS = `import { type ClassValue, clsx } from 'clsx'
|
|
550
|
+
import { twMerge } from 'tailwind-merge'
|
|
551
|
+
|
|
552
|
+
export function cn(...inputs: ClassValue[]) {
|
|
553
|
+
return twMerge(clsx(inputs))
|
|
554
|
+
}
|
|
555
|
+
`;
|
|
556
|
+
function detectPackageManager() {
|
|
557
|
+
if (existsSync2("pnpm-lock.yaml")) return "pnpm";
|
|
558
|
+
if (existsSync2("yarn.lock")) return "yarn";
|
|
559
|
+
if (existsSync2("package-lock.json")) return "npm";
|
|
560
|
+
return "npm";
|
|
561
|
+
}
|
|
562
|
+
function runCommand(cmd) {
|
|
563
|
+
console.log(` $ ${cmd}`);
|
|
564
|
+
execSync2(cmd, { stdio: "inherit" });
|
|
565
|
+
}
|
|
566
|
+
async function init() {
|
|
567
|
+
const cwd = process.cwd();
|
|
568
|
+
const pm = detectPackageManager();
|
|
569
|
+
console.log("Initializing hasan-ui in the current project...\n");
|
|
570
|
+
const dirs = ["src/components/ui", "src/lib"];
|
|
571
|
+
for (const dir of dirs) {
|
|
572
|
+
const fullPath = join3(cwd, dir);
|
|
573
|
+
if (!existsSync2(fullPath)) {
|
|
574
|
+
mkdirSync2(fullPath, { recursive: true });
|
|
575
|
+
console.log(` Created ${dir}/`);
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
const cssPath = join3(cwd, "src", "globals.css");
|
|
579
|
+
if (!existsSync2(cssPath)) {
|
|
580
|
+
writeFileSync2(cssPath, DEFAULT_GLOBALS_CSS);
|
|
581
|
+
console.log(" Created src/globals.css");
|
|
582
|
+
}
|
|
583
|
+
const configPath = join3(cwd, "components.json");
|
|
584
|
+
if (!existsSync2(configPath)) {
|
|
585
|
+
writeFileSync2(configPath, DEFAULT_COMPONENTS_JSON);
|
|
586
|
+
console.log(" Created components.json");
|
|
587
|
+
}
|
|
588
|
+
const utilsPath = join3(cwd, "src", "lib", "utils.ts");
|
|
589
|
+
if (!existsSync2(utilsPath)) {
|
|
590
|
+
writeFileSync2(utilsPath, DEFAULT_UTILS_TS);
|
|
591
|
+
console.log(" Created src/lib/utils.ts");
|
|
592
|
+
}
|
|
593
|
+
const deps = ["tailwindcss", "clsx", "tailwind-merge", "class-variance-authority"];
|
|
594
|
+
const radixDeps = ["@radix-ui/react-slot"];
|
|
595
|
+
console.log("\n Installing dependencies...");
|
|
596
|
+
if (pm === "pnpm") {
|
|
597
|
+
runCommand(`pnpm add ${[...deps, ...radixDeps].join(" ")}`);
|
|
598
|
+
} else if (pm === "yarn") {
|
|
599
|
+
runCommand(`yarn add ${[...deps, ...radixDeps].join(" ")}`);
|
|
600
|
+
} else {
|
|
601
|
+
runCommand(`npm install ${[...deps, ...radixDeps].join(" ")}`);
|
|
602
|
+
}
|
|
603
|
+
console.log("\n\u2713 hasan-ui initialized");
|
|
604
|
+
console.log(" Next: add components with `hasan-ui add <component>`");
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
// src/diff.ts
|
|
608
|
+
import { existsSync as existsSync3, readFileSync as readFileSync2 } from "fs";
|
|
609
|
+
async function diff(name) {
|
|
610
|
+
const items = await fetchRegistry();
|
|
611
|
+
const item = findItem(items, name);
|
|
612
|
+
if (!item) {
|
|
613
|
+
console.error(`Component "${name}" not found in registry`);
|
|
614
|
+
process.exit(1);
|
|
615
|
+
}
|
|
616
|
+
console.log(`
|
|
617
|
+
Diff for ${item.name} (${item.titleEn})
|
|
618
|
+
`);
|
|
619
|
+
for (const file of item.files) {
|
|
620
|
+
const targetPath = file.target;
|
|
621
|
+
if (!existsSync3(targetPath)) {
|
|
622
|
+
console.log(` ${targetPath}: not installed`);
|
|
623
|
+
continue;
|
|
624
|
+
}
|
|
625
|
+
const localContent = readFileSync2(targetPath, "utf-8");
|
|
626
|
+
const upstreamContent = await fetchFileContent(file.path, item._registryPath);
|
|
627
|
+
if (!upstreamContent) {
|
|
628
|
+
console.log(` ${targetPath}: could not fetch upstream`);
|
|
629
|
+
continue;
|
|
630
|
+
}
|
|
631
|
+
if (localContent.trim() === upstreamContent.trim()) {
|
|
632
|
+
console.log(` ${targetPath}: up to date`);
|
|
633
|
+
} else {
|
|
634
|
+
console.log(` ${targetPath}: modified`);
|
|
635
|
+
}
|
|
636
|
+
}
|
|
637
|
+
console.log();
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
// src/index.ts
|
|
641
|
+
var program = new Command();
|
|
642
|
+
program.name("hasan-ui").description("hasan-ui component installer").version("0.1.0");
|
|
643
|
+
program.command("init").description("Initialize hasan-ui in your project").action(async () => {
|
|
644
|
+
await init();
|
|
645
|
+
});
|
|
646
|
+
program.command("add").description("Add component(s) or a collection to your project").argument("[names...]", 'Component name(s) or "collection <name>"').option("--overwrite", "Overwrite existing files").option("--collection <name|url>", "Install a curated collection by name or a cart URL").action(async (names, options) => {
|
|
647
|
+
if (names[0] === "collection" && names[1]) {
|
|
648
|
+
await add(names[1], { ...options, collection: names[1] });
|
|
649
|
+
return;
|
|
650
|
+
}
|
|
651
|
+
if (names.length === 0 && !options.collection) {
|
|
652
|
+
console.error("Usage: hasan-ui add <name> | hasan-ui add <name1> <name2> | hasan-ui add collection <name> | hasan-ui add --collection <name|url>");
|
|
653
|
+
process.exit(1);
|
|
654
|
+
}
|
|
655
|
+
await add(names, options);
|
|
656
|
+
});
|
|
657
|
+
program.command("list").description("List all available components or collections").option("-c, --category <category>", "Filter by category (forms, navigation, data-display, feedback, blocks, intents)").option("--type <type>", "Filter by TYPE tag (primitive, form, navigation, data-display, feedback, block, template, layout, overlay, media)").option("--purpose <purpose>", "Filter by PURPOSE tag (ecommerce, education, quran, auth, admin, content, marketing, islamic, media, communication, geo, finance, general)").option("--collections", "List curated collections instead of components").action(async (options) => {
|
|
658
|
+
await list(options);
|
|
659
|
+
});
|
|
660
|
+
program.command("diff").description("Show diff between installed and upstream component").argument("<name>", "Component name").action(async (name) => {
|
|
661
|
+
await diff(name);
|
|
662
|
+
});
|
|
663
|
+
program.parse();
|
package/package.json
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@jadmadi/hasan-ui-cli",
|
|
3
|
+
"version": "0.3.0",
|
|
4
|
+
"license": "SEE LICENSE IN ../../LICENSE",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"hasan-ui": "./dist/index.js"
|
|
8
|
+
},
|
|
9
|
+
"files": [
|
|
10
|
+
"dist"
|
|
11
|
+
],
|
|
12
|
+
"dependencies": {
|
|
13
|
+
"commander": "^12.0.0"
|
|
14
|
+
},
|
|
15
|
+
"devDependencies": {
|
|
16
|
+
"@types/node": "^20",
|
|
17
|
+
"tsup": "^8.5.1",
|
|
18
|
+
"typescript": "^6",
|
|
19
|
+
"vitest": "^4.1.9"
|
|
20
|
+
},
|
|
21
|
+
"engines": {
|
|
22
|
+
"node": ">=18"
|
|
23
|
+
},
|
|
24
|
+
"publishConfig": {
|
|
25
|
+
"access": "public"
|
|
26
|
+
},
|
|
27
|
+
"scripts": {
|
|
28
|
+
"build": "tsup src/index.ts --format esm --clean",
|
|
29
|
+
"dev": "tsup src/index.ts --format esm --watch",
|
|
30
|
+
"test": "vitest run"
|
|
31
|
+
}
|
|
32
|
+
}
|