ds-01 1.0.10 → 1.0.12
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/dist/commands/add.d.ts.map +1 -1
- package/dist/commands/add.js +63 -14
- package/dist/native/macos-key-helper.swift +206 -0
- package/package.json +2 -2
- package/scripts/copy-native.js +15 -0
- package/src/commands/add.ts +209 -52
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"add.d.ts","sourceRoot":"","sources":["../../src/commands/add.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;
|
|
1
|
+
{"version":3,"file":"add.d.ts","sourceRoot":"","sources":["../../src/commands/add.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAsGpC,eAAO,MAAM,GAAG,SAwTZ,CAAC"}
|
package/dist/commands/add.js
CHANGED
|
@@ -9,7 +9,9 @@ import { signWithDeviceKey } from "../utils/secureKeyStore.js";
|
|
|
9
9
|
import machineIdPkg from "node-machine-id";
|
|
10
10
|
const { machineIdSync } = machineIdPkg;
|
|
11
11
|
const API_BASE_URL = process.env.DS01_API_URL || "https://ds-01.vercel.app";
|
|
12
|
-
//
|
|
12
|
+
// ---------------------------------------------------------
|
|
13
|
+
// Helper: Verify Tailwind exists
|
|
14
|
+
// ---------------------------------------------------------
|
|
13
15
|
function checkTailwindInstallation() {
|
|
14
16
|
const targetDir = process.cwd();
|
|
15
17
|
const pkgJsonPath = path.join(targetDir, "package.json");
|
|
@@ -32,6 +34,24 @@ function checkTailwindInstallation() {
|
|
|
32
34
|
process.exit(1);
|
|
33
35
|
}
|
|
34
36
|
}
|
|
37
|
+
// ---------------------------------------------------------
|
|
38
|
+
// Helper: Find dependencies that are actually missing
|
|
39
|
+
// ---------------------------------------------------------
|
|
40
|
+
function getMissingDependencies(dependencies) {
|
|
41
|
+
const packageJsonPath = path.join(process.cwd(), "package.json");
|
|
42
|
+
if (!fs.existsSync(packageJsonPath)) {
|
|
43
|
+
return dependencies;
|
|
44
|
+
}
|
|
45
|
+
const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, "utf-8"));
|
|
46
|
+
const installedDependencies = {
|
|
47
|
+
...(packageJson.dependencies || {}),
|
|
48
|
+
...(packageJson.devDependencies || {}),
|
|
49
|
+
};
|
|
50
|
+
return dependencies.filter((dependency) => !installedDependencies[dependency]);
|
|
51
|
+
}
|
|
52
|
+
// ---------------------------------------------------------
|
|
53
|
+
// ADD COMMAND
|
|
54
|
+
// ---------------------------------------------------------
|
|
35
55
|
export const add = new Command()
|
|
36
56
|
.name("add")
|
|
37
57
|
.description("Add a component from DS01 to your project")
|
|
@@ -39,6 +59,9 @@ export const add = new Command()
|
|
|
39
59
|
.action(async (componentName) => {
|
|
40
60
|
console.log();
|
|
41
61
|
intro(`${pc.bgWhite(pc.black(pc.bold(" DS01 ")))} ${pc.gray("Adding Component")}`);
|
|
62
|
+
// -----------------------------------------------------
|
|
63
|
+
// 1. Validate project
|
|
64
|
+
// -----------------------------------------------------
|
|
42
65
|
checkTailwindInstallation();
|
|
43
66
|
const cwd = process.cwd();
|
|
44
67
|
const configPath = path.join(cwd, "ds01.config.json");
|
|
@@ -47,6 +70,9 @@ export const add = new Command()
|
|
|
47
70
|
process.exit(1);
|
|
48
71
|
}
|
|
49
72
|
const config = JSON.parse(fs.readFileSync(configPath, "utf-8"));
|
|
73
|
+
// -----------------------------------------------------
|
|
74
|
+
// 2. Authentication
|
|
75
|
+
// -----------------------------------------------------
|
|
50
76
|
const token = getToken();
|
|
51
77
|
const machineId = machineIdSync();
|
|
52
78
|
if (!token || !machineId) {
|
|
@@ -57,7 +83,7 @@ export const add = new Command()
|
|
|
57
83
|
s.start(`Fetching ${pc.cyan(`<${componentName} />`)} from registry...`);
|
|
58
84
|
try {
|
|
59
85
|
// --------------------------------------------------
|
|
60
|
-
//
|
|
86
|
+
// 3. Create request-specific signed message
|
|
61
87
|
// --------------------------------------------------
|
|
62
88
|
const timestamp = Date.now().toString();
|
|
63
89
|
const message = [
|
|
@@ -67,12 +93,12 @@ export const add = new Command()
|
|
|
67
93
|
machineId,
|
|
68
94
|
].join("\n");
|
|
69
95
|
// --------------------------------------------------
|
|
70
|
-
//
|
|
96
|
+
// 4. Sign request with secure device private key
|
|
71
97
|
// --------------------------------------------------
|
|
72
98
|
const signature = await signWithDeviceKey(new TextEncoder().encode(message));
|
|
73
99
|
const signatureBase64 = Buffer.from(signature).toString("base64");
|
|
74
100
|
// --------------------------------------------------
|
|
75
|
-
//
|
|
101
|
+
// 5. Request component from registry
|
|
76
102
|
// --------------------------------------------------
|
|
77
103
|
const response = await fetch(`${API_BASE_URL}/api/registry/${componentName}`, {
|
|
78
104
|
method: "GET",
|
|
@@ -83,15 +109,19 @@ export const add = new Command()
|
|
|
83
109
|
"X-DS01-Signature": signatureBase64,
|
|
84
110
|
},
|
|
85
111
|
});
|
|
112
|
+
// --------------------------------------------------
|
|
113
|
+
// 6. Handle registry errors
|
|
114
|
+
// --------------------------------------------------
|
|
86
115
|
if (!response.ok) {
|
|
87
116
|
const errorData = await response.json().catch(() => null);
|
|
88
117
|
s.stop(pc.red("Component fetch failed."));
|
|
89
|
-
cancel(errorData?.error ||
|
|
118
|
+
cancel(errorData?.error ||
|
|
119
|
+
`Server returned HTTP ${response.status}`);
|
|
90
120
|
process.exit(1);
|
|
91
121
|
}
|
|
92
122
|
const componentData = await response.json();
|
|
93
123
|
// --------------------------------------------------
|
|
94
|
-
//
|
|
124
|
+
// 7. Create component directory
|
|
95
125
|
// --------------------------------------------------
|
|
96
126
|
const targetDir = path.join(cwd, config.componentsPath, componentName);
|
|
97
127
|
if (!fs.existsSync(targetDir)) {
|
|
@@ -100,11 +130,11 @@ export const add = new Command()
|
|
|
100
130
|
});
|
|
101
131
|
}
|
|
102
132
|
// --------------------------------------------------
|
|
103
|
-
//
|
|
133
|
+
// 8. Extract bundled file names
|
|
104
134
|
// --------------------------------------------------
|
|
105
135
|
const bundledFileNames = componentData.files.map((file) => file.name.replace(/\.[^/.]+$/, ""));
|
|
106
136
|
// --------------------------------------------------
|
|
107
|
-
//
|
|
137
|
+
// 9. Write component files
|
|
108
138
|
// --------------------------------------------------
|
|
109
139
|
for (const file of componentData.files) {
|
|
110
140
|
const filePath = path.join(targetDir, file.name);
|
|
@@ -117,10 +147,14 @@ export const add = new Command()
|
|
|
117
147
|
}
|
|
118
148
|
s.stop(pc.green(`Downloaded ${componentData.files.length} files into ${pc.white(`/${config.componentsPath}/${componentName}`)}`));
|
|
119
149
|
// --------------------------------------------------
|
|
120
|
-
//
|
|
150
|
+
// 10. Install ONLY missing dependencies
|
|
121
151
|
// --------------------------------------------------
|
|
122
|
-
|
|
123
|
-
|
|
152
|
+
const registryDependencies = Array.isArray(componentData.dependencies)
|
|
153
|
+
? componentData.dependencies
|
|
154
|
+
: [];
|
|
155
|
+
const missingDependencies = getMissingDependencies(registryDependencies);
|
|
156
|
+
if (missingDependencies.length > 0) {
|
|
157
|
+
const depsToInstall = missingDependencies.join(" ");
|
|
124
158
|
s.start(`Installing missing dependencies: ${pc.cyan(depsToInstall)}...`);
|
|
125
159
|
try {
|
|
126
160
|
execSync(`npm install ${depsToInstall}`, {
|
|
@@ -134,16 +168,31 @@ export const add = new Command()
|
|
|
134
168
|
}
|
|
135
169
|
}
|
|
136
170
|
// --------------------------------------------------
|
|
137
|
-
//
|
|
171
|
+
// 11. No dependencies needed
|
|
172
|
+
// --------------------------------------------------
|
|
173
|
+
// We intentionally don't show a spinner here.
|
|
174
|
+
//
|
|
175
|
+
// Example:
|
|
176
|
+
//
|
|
177
|
+
// DS01 requires ["motion"]
|
|
178
|
+
// package.json already contains "motion"
|
|
179
|
+
//
|
|
180
|
+
// → Nothing happens.
|
|
181
|
+
// → No npm install.
|
|
182
|
+
// → No unnecessary package changes.
|
|
183
|
+
// --------------------------------------------------
|
|
184
|
+
// 12. Success
|
|
138
185
|
// --------------------------------------------------
|
|
139
|
-
const mainFile = bundledFileNames.find((name) => name.toLowerCase() ===
|
|
186
|
+
const mainFile = bundledFileNames.find((name) => name.toLowerCase() ===
|
|
187
|
+
componentName.toLowerCase()) || bundledFileNames[0];
|
|
140
188
|
outro(`${pc.white("✔")} ${pc.bold(`Component <${componentName} /> is ready!`)}\n` +
|
|
141
189
|
pc.gray("Import it: ") +
|
|
142
190
|
pc.cyan(`import { ${mainFile} } from "@/${config.componentsPath}/${componentName}/${mainFile}"`));
|
|
143
191
|
}
|
|
144
192
|
catch (error) {
|
|
145
193
|
s.stop(pc.red("An error occurred during injection."));
|
|
146
|
-
cancel(pc.gray(error?.message ||
|
|
194
|
+
cancel(pc.gray(error?.message ||
|
|
195
|
+
"Unknown CLI Error"));
|
|
147
196
|
process.exit(1);
|
|
148
197
|
}
|
|
149
198
|
});
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
import Foundation
|
|
2
|
+
import Security
|
|
3
|
+
|
|
4
|
+
let keyTag = "com.ds01.cli.device-key"
|
|
5
|
+
|
|
6
|
+
func fail(_ message: String) -> Never {
|
|
7
|
+
fputs(message + "\n", stderr)
|
|
8
|
+
exit(1)
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
func getAccessControl() -> SecAccessControl {
|
|
12
|
+
var error: Unmanaged<CFError>?
|
|
13
|
+
|
|
14
|
+
guard let access = SecAccessControlCreateWithFlags(
|
|
15
|
+
nil,
|
|
16
|
+
kSecAttrAccessibleWhenUnlockedThisDeviceOnly,
|
|
17
|
+
.privateKeyUsage,
|
|
18
|
+
&error
|
|
19
|
+
) else {
|
|
20
|
+
if let error {
|
|
21
|
+
fail(error.takeRetainedValue().localizedDescription)
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
fail("Unable to create Secure Enclave access control.")
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
return access
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
func findPrivateKey() -> SecKey? {
|
|
31
|
+
let query: [String: Any] = [
|
|
32
|
+
kSecClass as String: kSecClassKey,
|
|
33
|
+
kSecAttrApplicationTag as String: keyTag.data(using: .utf8)!,
|
|
34
|
+
kSecAttrKeyType as String: kSecAttrKeyTypeECSECPrimeRandom,
|
|
35
|
+
kSecReturnRef as String: true
|
|
36
|
+
]
|
|
37
|
+
|
|
38
|
+
var result: CFTypeRef?
|
|
39
|
+
|
|
40
|
+
let status = SecItemCopyMatching(
|
|
41
|
+
query as CFDictionary,
|
|
42
|
+
&result
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
guard status == errSecSuccess else {
|
|
46
|
+
return nil
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
return (result as! SecKey)
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
func createKey() {
|
|
53
|
+
if findPrivateKey() != nil {
|
|
54
|
+
print("EXISTS")
|
|
55
|
+
return
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
let access = getAccessControl()
|
|
59
|
+
|
|
60
|
+
let attributes: [String: Any] = [
|
|
61
|
+
kSecAttrKeyType as String:
|
|
62
|
+
kSecAttrKeyTypeECSECPrimeRandom,
|
|
63
|
+
|
|
64
|
+
kSecAttrKeySizeInBits as String:
|
|
65
|
+
256,
|
|
66
|
+
|
|
67
|
+
kSecAttrTokenID as String:
|
|
68
|
+
kSecAttrTokenIDSecureEnclave,
|
|
69
|
+
|
|
70
|
+
kSecPrivateKeyAttrs as String: [
|
|
71
|
+
kSecAttrIsPermanent as String:
|
|
72
|
+
true,
|
|
73
|
+
|
|
74
|
+
kSecAttrApplicationTag as String:
|
|
75
|
+
keyTag.data(using: .utf8)!,
|
|
76
|
+
|
|
77
|
+
kSecAttrAccessControl as String:
|
|
78
|
+
access
|
|
79
|
+
]
|
|
80
|
+
]
|
|
81
|
+
|
|
82
|
+
var error: Unmanaged<CFError>?
|
|
83
|
+
|
|
84
|
+
guard SecKeyCreateRandomKey(
|
|
85
|
+
attributes as CFDictionary,
|
|
86
|
+
&error
|
|
87
|
+
) != nil else {
|
|
88
|
+
if let error {
|
|
89
|
+
fail(error.takeRetainedValue().localizedDescription)
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
fail("Unable to create Secure Enclave key.")
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
print("CREATED")
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
func publicKey() {
|
|
99
|
+
guard let privateKey = findPrivateKey() else {
|
|
100
|
+
fail("DS01 Secure Enclave key not found.")
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
guard let publicKey = SecKeyCopyPublicKey(privateKey) else {
|
|
104
|
+
fail("Unable to obtain public key.")
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
var error: Unmanaged<CFError>?
|
|
108
|
+
|
|
109
|
+
guard let data = SecKeyCopyExternalRepresentation(
|
|
110
|
+
publicKey,
|
|
111
|
+
&error
|
|
112
|
+
) else {
|
|
113
|
+
if let error {
|
|
114
|
+
fail(error.takeRetainedValue().localizedDescription)
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
fail("Unable to export public key.")
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
let base64 = (data as Data).base64EncodedString()
|
|
121
|
+
|
|
122
|
+
print(base64)
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
func sign(_ base64Data: String) {
|
|
126
|
+
guard let privateKey = findPrivateKey() else {
|
|
127
|
+
fail("DS01 Secure Enclave key not found.")
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
guard let data = Data(base64Encoded: base64Data) else {
|
|
131
|
+
fail("Invalid input data.")
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
let algorithm = SecKeyAlgorithm.ecdsaSignatureMessageX962SHA256
|
|
135
|
+
|
|
136
|
+
guard SecKeyIsAlgorithmSupported(
|
|
137
|
+
privateKey,
|
|
138
|
+
.sign,
|
|
139
|
+
algorithm
|
|
140
|
+
) else {
|
|
141
|
+
fail("Secure Enclave key does not support signing.")
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
var error: Unmanaged<CFError>?
|
|
145
|
+
|
|
146
|
+
guard let signature = SecKeyCreateSignature(
|
|
147
|
+
privateKey,
|
|
148
|
+
algorithm,
|
|
149
|
+
data as CFData,
|
|
150
|
+
&error
|
|
151
|
+
) else {
|
|
152
|
+
if let error {
|
|
153
|
+
fail(error.takeRetainedValue().localizedDescription)
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
fail("Secure Enclave signing failed.")
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
print((signature as Data).base64EncodedString())
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
func deleteKey() {
|
|
163
|
+
let query: [String: Any] = [
|
|
164
|
+
kSecClass as String: kSecClassKey,
|
|
165
|
+
kSecAttrApplicationTag as String:
|
|
166
|
+
keyTag.data(using: .utf8)!,
|
|
167
|
+
kSecAttrKeyType as String:
|
|
168
|
+
kSecAttrKeyTypeECSECPrimeRandom
|
|
169
|
+
]
|
|
170
|
+
|
|
171
|
+
let status = SecItemDelete(
|
|
172
|
+
query as CFDictionary
|
|
173
|
+
)
|
|
174
|
+
|
|
175
|
+
if status != errSecSuccess &&
|
|
176
|
+
status != errSecItemNotFound {
|
|
177
|
+
fail("Unable to delete DS01 Secure Enclave key.")
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
print("DELETED")
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
guard CommandLine.arguments.count >= 2 else {
|
|
184
|
+
fail("Missing operation.")
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
switch CommandLine.arguments[1] {
|
|
188
|
+
case "create":
|
|
189
|
+
createKey()
|
|
190
|
+
|
|
191
|
+
case "public":
|
|
192
|
+
publicKey()
|
|
193
|
+
|
|
194
|
+
case "sign":
|
|
195
|
+
guard CommandLine.arguments.count >= 3 else {
|
|
196
|
+
fail("Missing data to sign.")
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
sign(CommandLine.arguments[2])
|
|
200
|
+
|
|
201
|
+
case "delete":
|
|
202
|
+
deleteKey()
|
|
203
|
+
|
|
204
|
+
default:
|
|
205
|
+
fail("Unknown operation.")
|
|
206
|
+
}
|
package/package.json
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ds-01",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.12",
|
|
4
4
|
"description": "Make your site best with DS01",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
7
7
|
"ds01": "dist/index.js"
|
|
8
8
|
},
|
|
9
9
|
"scripts": {
|
|
10
|
-
"build": "tsc"
|
|
10
|
+
"build": "tsc && node scripts/copy-native.js"
|
|
11
11
|
},
|
|
12
12
|
"publishConfig": {
|
|
13
13
|
"access": "public"
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
|
|
4
|
+
const source = path.resolve("src/native/macos-key-helper.swift");
|
|
5
|
+
const destinationDir = path.resolve("dist/native");
|
|
6
|
+
const destination = path.join(
|
|
7
|
+
destinationDir,
|
|
8
|
+
"macos-key-helper.swift",
|
|
9
|
+
);
|
|
10
|
+
|
|
11
|
+
fs.mkdirSync(destinationDir, { recursive: true });
|
|
12
|
+
|
|
13
|
+
fs.copyFileSync(source, destination);
|
|
14
|
+
|
|
15
|
+
console.log("✅ Copied macOS native helper.");
|
package/src/commands/add.ts
CHANGED
|
@@ -12,9 +12,13 @@ import machineIdPkg from "node-machine-id";
|
|
|
12
12
|
|
|
13
13
|
const { machineIdSync } = machineIdPkg;
|
|
14
14
|
|
|
15
|
-
const API_BASE_URL =
|
|
15
|
+
const API_BASE_URL =
|
|
16
|
+
process.env.DS01_API_URL || "https://ds-01.vercel.app";
|
|
17
|
+
|
|
18
|
+
// ---------------------------------------------------------
|
|
19
|
+
// Helper: Verify Tailwind exists
|
|
20
|
+
// ---------------------------------------------------------
|
|
16
21
|
|
|
17
|
-
// Helper to verify Tailwind exists before doing anything
|
|
18
22
|
function checkTailwindInstallation() {
|
|
19
23
|
const targetDir = process.cwd();
|
|
20
24
|
const pkgJsonPath = path.join(targetDir, "package.json");
|
|
@@ -25,10 +29,13 @@ function checkTailwindInstallation() {
|
|
|
25
29
|
"No package.json found. Please run this command inside a Node.js project.",
|
|
26
30
|
),
|
|
27
31
|
);
|
|
32
|
+
|
|
28
33
|
process.exit(1);
|
|
29
34
|
}
|
|
30
35
|
|
|
31
|
-
const pkgJson = JSON.parse(
|
|
36
|
+
const pkgJson = JSON.parse(
|
|
37
|
+
fs.readFileSync(pkgJsonPath, "utf-8"),
|
|
38
|
+
);
|
|
32
39
|
|
|
33
40
|
const allDeps = {
|
|
34
41
|
...pkgJson.dependencies,
|
|
@@ -37,26 +44,69 @@ function checkTailwindInstallation() {
|
|
|
37
44
|
|
|
38
45
|
if (!allDeps["tailwindcss"]) {
|
|
39
46
|
cancel(
|
|
40
|
-
pc.red(
|
|
47
|
+
pc.red(
|
|
48
|
+
"Tailwind CSS is missing from your project dependencies.\n",
|
|
49
|
+
) +
|
|
41
50
|
pc.gray(
|
|
42
51
|
"DS01 components rely strictly on Tailwind CSS for styling.\n\n",
|
|
43
52
|
) +
|
|
44
53
|
pc.white(
|
|
45
54
|
"Kindly install it and configure your project, then retry:\n",
|
|
46
55
|
) +
|
|
47
|
-
pc.cyan(
|
|
56
|
+
pc.cyan(
|
|
57
|
+
" npm install tailwindcss @tailwindcss/postcss postcss\n\n",
|
|
58
|
+
) +
|
|
48
59
|
pc.gray("Official Setup Guide: ") +
|
|
49
|
-
pc.underline(
|
|
60
|
+
pc.underline(
|
|
61
|
+
"https://tailwindcss.com/docs/installation",
|
|
62
|
+
),
|
|
50
63
|
);
|
|
51
64
|
|
|
52
65
|
process.exit(1);
|
|
53
66
|
}
|
|
54
67
|
}
|
|
55
68
|
|
|
69
|
+
// ---------------------------------------------------------
|
|
70
|
+
// Helper: Find dependencies that are actually missing
|
|
71
|
+
// ---------------------------------------------------------
|
|
72
|
+
|
|
73
|
+
function getMissingDependencies(
|
|
74
|
+
dependencies: string[],
|
|
75
|
+
): string[] {
|
|
76
|
+
const packageJsonPath = path.join(
|
|
77
|
+
process.cwd(),
|
|
78
|
+
"package.json",
|
|
79
|
+
);
|
|
80
|
+
|
|
81
|
+
if (!fs.existsSync(packageJsonPath)) {
|
|
82
|
+
return dependencies;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const packageJson = JSON.parse(
|
|
86
|
+
fs.readFileSync(packageJsonPath, "utf-8"),
|
|
87
|
+
);
|
|
88
|
+
|
|
89
|
+
const installedDependencies = {
|
|
90
|
+
...(packageJson.dependencies || {}),
|
|
91
|
+
...(packageJson.devDependencies || {}),
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
return dependencies.filter(
|
|
95
|
+
(dependency) => !installedDependencies[dependency],
|
|
96
|
+
);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// ---------------------------------------------------------
|
|
100
|
+
// ADD COMMAND
|
|
101
|
+
// ---------------------------------------------------------
|
|
102
|
+
|
|
56
103
|
export const add = new Command()
|
|
57
104
|
.name("add")
|
|
58
105
|
.description("Add a component from DS01 to your project")
|
|
59
|
-
.argument(
|
|
106
|
+
.argument(
|
|
107
|
+
"<component>",
|
|
108
|
+
"The name of the component (e.g., premium-section)",
|
|
109
|
+
)
|
|
60
110
|
.action(async (componentName: string) => {
|
|
61
111
|
console.log();
|
|
62
112
|
|
|
@@ -66,36 +116,61 @@ export const add = new Command()
|
|
|
66
116
|
)}`,
|
|
67
117
|
);
|
|
68
118
|
|
|
119
|
+
// -----------------------------------------------------
|
|
120
|
+
// 1. Validate project
|
|
121
|
+
// -----------------------------------------------------
|
|
122
|
+
|
|
69
123
|
checkTailwindInstallation();
|
|
70
124
|
|
|
71
125
|
const cwd = process.cwd();
|
|
72
126
|
|
|
73
|
-
const configPath = path.join(
|
|
127
|
+
const configPath = path.join(
|
|
128
|
+
cwd,
|
|
129
|
+
"ds01.config.json",
|
|
130
|
+
);
|
|
74
131
|
|
|
75
132
|
if (!fs.existsSync(configPath)) {
|
|
76
|
-
cancel(
|
|
133
|
+
cancel(
|
|
134
|
+
pc.red(
|
|
135
|
+
"ds01.config.json not found. Run 'npx ds-01 init' first.",
|
|
136
|
+
),
|
|
137
|
+
);
|
|
77
138
|
|
|
78
139
|
process.exit(1);
|
|
79
140
|
}
|
|
80
141
|
|
|
81
|
-
const config = JSON.parse(
|
|
142
|
+
const config = JSON.parse(
|
|
143
|
+
fs.readFileSync(configPath, "utf-8"),
|
|
144
|
+
);
|
|
145
|
+
|
|
146
|
+
// -----------------------------------------------------
|
|
147
|
+
// 2. Authentication
|
|
148
|
+
// -----------------------------------------------------
|
|
82
149
|
|
|
83
150
|
const token = getToken();
|
|
84
151
|
const machineId = machineIdSync();
|
|
85
152
|
|
|
86
153
|
if (!token || !machineId) {
|
|
87
|
-
cancel(
|
|
154
|
+
cancel(
|
|
155
|
+
pc.red(
|
|
156
|
+
"You are not authenticated. Run 'npx ds-01 login' first.",
|
|
157
|
+
),
|
|
158
|
+
);
|
|
88
159
|
|
|
89
160
|
process.exit(1);
|
|
90
161
|
}
|
|
91
162
|
|
|
92
163
|
const s = spinner();
|
|
93
164
|
|
|
94
|
-
s.start(
|
|
165
|
+
s.start(
|
|
166
|
+
`Fetching ${pc.cyan(
|
|
167
|
+
`<${componentName} />`,
|
|
168
|
+
)} from registry...`,
|
|
169
|
+
);
|
|
95
170
|
|
|
96
171
|
try {
|
|
97
172
|
// --------------------------------------------------
|
|
98
|
-
//
|
|
173
|
+
// 3. Create request-specific signed message
|
|
99
174
|
// --------------------------------------------------
|
|
100
175
|
|
|
101
176
|
const timestamp = Date.now().toString();
|
|
@@ -108,17 +183,18 @@ export const add = new Command()
|
|
|
108
183
|
].join("\n");
|
|
109
184
|
|
|
110
185
|
// --------------------------------------------------
|
|
111
|
-
//
|
|
186
|
+
// 4. Sign request with secure device private key
|
|
112
187
|
// --------------------------------------------------
|
|
113
188
|
|
|
114
189
|
const signature = await signWithDeviceKey(
|
|
115
190
|
new TextEncoder().encode(message),
|
|
116
191
|
);
|
|
117
192
|
|
|
118
|
-
const signatureBase64 =
|
|
193
|
+
const signatureBase64 =
|
|
194
|
+
Buffer.from(signature).toString("base64");
|
|
119
195
|
|
|
120
196
|
// --------------------------------------------------
|
|
121
|
-
//
|
|
197
|
+
// 5. Request component from registry
|
|
122
198
|
// --------------------------------------------------
|
|
123
199
|
|
|
124
200
|
const response = await fetch(
|
|
@@ -138,23 +214,38 @@ export const add = new Command()
|
|
|
138
214
|
},
|
|
139
215
|
);
|
|
140
216
|
|
|
217
|
+
// --------------------------------------------------
|
|
218
|
+
// 6. Handle registry errors
|
|
219
|
+
// --------------------------------------------------
|
|
220
|
+
|
|
141
221
|
if (!response.ok) {
|
|
142
|
-
const errorData =
|
|
222
|
+
const errorData =
|
|
223
|
+
await response.json().catch(() => null);
|
|
143
224
|
|
|
144
|
-
s.stop(
|
|
225
|
+
s.stop(
|
|
226
|
+
pc.red("Component fetch failed."),
|
|
227
|
+
);
|
|
145
228
|
|
|
146
|
-
cancel(
|
|
229
|
+
cancel(
|
|
230
|
+
errorData?.error ||
|
|
231
|
+
`Server returned HTTP ${response.status}`,
|
|
232
|
+
);
|
|
147
233
|
|
|
148
234
|
process.exit(1);
|
|
149
235
|
}
|
|
150
236
|
|
|
151
|
-
const componentData =
|
|
237
|
+
const componentData =
|
|
238
|
+
await response.json();
|
|
152
239
|
|
|
153
240
|
// --------------------------------------------------
|
|
154
|
-
//
|
|
241
|
+
// 7. Create component directory
|
|
155
242
|
// --------------------------------------------------
|
|
156
243
|
|
|
157
|
-
const targetDir = path.join(
|
|
244
|
+
const targetDir = path.join(
|
|
245
|
+
cwd,
|
|
246
|
+
config.componentsPath,
|
|
247
|
+
componentName,
|
|
248
|
+
);
|
|
158
249
|
|
|
159
250
|
if (!fs.existsSync(targetDir)) {
|
|
160
251
|
fs.mkdirSync(targetDir, {
|
|
@@ -163,32 +254,48 @@ export const add = new Command()
|
|
|
163
254
|
}
|
|
164
255
|
|
|
165
256
|
// --------------------------------------------------
|
|
166
|
-
//
|
|
257
|
+
// 8. Extract bundled file names
|
|
167
258
|
// --------------------------------------------------
|
|
168
259
|
|
|
169
|
-
const bundledFileNames =
|
|
170
|
-
|
|
171
|
-
|
|
260
|
+
const bundledFileNames =
|
|
261
|
+
componentData.files.map(
|
|
262
|
+
(file: any) =>
|
|
263
|
+
file.name.replace(
|
|
264
|
+
/\.[^/.]+$/,
|
|
265
|
+
"",
|
|
266
|
+
),
|
|
267
|
+
);
|
|
172
268
|
|
|
173
269
|
// --------------------------------------------------
|
|
174
|
-
//
|
|
270
|
+
// 9. Write component files
|
|
175
271
|
// --------------------------------------------------
|
|
176
272
|
|
|
177
273
|
for (const file of componentData.files) {
|
|
178
|
-
const filePath = path.join(
|
|
274
|
+
const filePath = path.join(
|
|
275
|
+
targetDir,
|
|
276
|
+
file.name,
|
|
277
|
+
);
|
|
179
278
|
|
|
180
279
|
let content = file.content;
|
|
181
280
|
|
|
182
|
-
bundledFileNames.forEach(
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
281
|
+
bundledFileNames.forEach(
|
|
282
|
+
(fileName: string) => {
|
|
283
|
+
const regex = new RegExp(
|
|
284
|
+
`from\\s+["']\\.[^"']*?\\/${fileName}["']`,
|
|
285
|
+
"g",
|
|
286
|
+
);
|
|
287
|
+
|
|
288
|
+
content = content.replace(
|
|
289
|
+
regex,
|
|
290
|
+
`from "./${fileName}"`,
|
|
291
|
+
);
|
|
292
|
+
},
|
|
293
|
+
);
|
|
190
294
|
|
|
191
|
-
fs.writeFileSync(
|
|
295
|
+
fs.writeFileSync(
|
|
296
|
+
filePath,
|
|
297
|
+
content,
|
|
298
|
+
);
|
|
192
299
|
}
|
|
193
300
|
|
|
194
301
|
s.stop(
|
|
@@ -200,43 +307,84 @@ export const add = new Command()
|
|
|
200
307
|
);
|
|
201
308
|
|
|
202
309
|
// --------------------------------------------------
|
|
203
|
-
//
|
|
310
|
+
// 10. Install ONLY missing dependencies
|
|
204
311
|
// --------------------------------------------------
|
|
205
312
|
|
|
206
|
-
|
|
207
|
-
|
|
313
|
+
const registryDependencies =
|
|
314
|
+
Array.isArray(componentData.dependencies)
|
|
315
|
+
? componentData.dependencies
|
|
316
|
+
: [];
|
|
317
|
+
|
|
318
|
+
const missingDependencies =
|
|
319
|
+
getMissingDependencies(
|
|
320
|
+
registryDependencies,
|
|
321
|
+
);
|
|
322
|
+
|
|
323
|
+
if (missingDependencies.length > 0) {
|
|
324
|
+
const depsToInstall =
|
|
325
|
+
missingDependencies.join(" ");
|
|
208
326
|
|
|
209
327
|
s.start(
|
|
210
|
-
`Installing missing dependencies: ${pc.cyan(
|
|
328
|
+
`Installing missing dependencies: ${pc.cyan(
|
|
329
|
+
depsToInstall,
|
|
330
|
+
)}...`,
|
|
211
331
|
);
|
|
212
332
|
|
|
213
333
|
try {
|
|
214
|
-
execSync(
|
|
215
|
-
|
|
216
|
-
|
|
334
|
+
execSync(
|
|
335
|
+
`npm install ${depsToInstall}`,
|
|
336
|
+
{
|
|
337
|
+
stdio: "ignore",
|
|
338
|
+
},
|
|
339
|
+
);
|
|
217
340
|
|
|
218
341
|
s.stop(
|
|
219
342
|
pc.green(
|
|
220
|
-
`Dependencies installed successfully: ${pc.gray(
|
|
343
|
+
`Dependencies installed successfully: ${pc.gray(
|
|
344
|
+
depsToInstall,
|
|
345
|
+
)}`,
|
|
221
346
|
),
|
|
222
347
|
);
|
|
223
348
|
} catch {
|
|
224
|
-
s.stop(
|
|
349
|
+
s.stop(
|
|
350
|
+
pc.red(
|
|
351
|
+
"Failed to auto-install dependencies.",
|
|
352
|
+
),
|
|
353
|
+
);
|
|
225
354
|
|
|
226
355
|
note(
|
|
227
|
-
`Please run: ${pc.cyan(
|
|
356
|
+
`Please run: ${pc.cyan(
|
|
357
|
+
`npm install ${depsToInstall}`,
|
|
358
|
+
)} manually.`,
|
|
228
359
|
"Manual Action Required",
|
|
229
360
|
);
|
|
230
361
|
}
|
|
231
362
|
}
|
|
232
363
|
|
|
233
364
|
// --------------------------------------------------
|
|
234
|
-
//
|
|
365
|
+
// 11. No dependencies needed
|
|
366
|
+
// --------------------------------------------------
|
|
367
|
+
|
|
368
|
+
// We intentionally don't show a spinner here.
|
|
369
|
+
//
|
|
370
|
+
// Example:
|
|
371
|
+
//
|
|
372
|
+
// DS01 requires ["motion"]
|
|
373
|
+
// package.json already contains "motion"
|
|
374
|
+
//
|
|
375
|
+
// → Nothing happens.
|
|
376
|
+
// → No npm install.
|
|
377
|
+
// → No unnecessary package changes.
|
|
378
|
+
|
|
379
|
+
// --------------------------------------------------
|
|
380
|
+
// 12. Success
|
|
235
381
|
// --------------------------------------------------
|
|
236
382
|
|
|
237
383
|
const mainFile =
|
|
238
384
|
bundledFileNames.find(
|
|
239
|
-
(name: string) =>
|
|
385
|
+
(name: string) =>
|
|
386
|
+
name.toLowerCase() ===
|
|
387
|
+
componentName.toLowerCase(),
|
|
240
388
|
) || bundledFileNames[0];
|
|
241
389
|
|
|
242
390
|
outro(
|
|
@@ -249,10 +397,19 @@ export const add = new Command()
|
|
|
249
397
|
),
|
|
250
398
|
);
|
|
251
399
|
} catch (error: any) {
|
|
252
|
-
s.stop(
|
|
400
|
+
s.stop(
|
|
401
|
+
pc.red(
|
|
402
|
+
"An error occurred during injection.",
|
|
403
|
+
),
|
|
404
|
+
);
|
|
253
405
|
|
|
254
|
-
cancel(
|
|
406
|
+
cancel(
|
|
407
|
+
pc.gray(
|
|
408
|
+
error?.message ||
|
|
409
|
+
"Unknown CLI Error",
|
|
410
|
+
),
|
|
411
|
+
);
|
|
255
412
|
|
|
256
413
|
process.exit(1);
|
|
257
414
|
}
|
|
258
|
-
});
|
|
415
|
+
});
|