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.
@@ -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;AAuDpC,eAAO,MAAM,GAAG,SA0MZ,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"}
@@ -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
- // Helper to verify Tailwind exists before doing anything
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
- // 1. Create request-specific message
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
- // 2. Sign message using the device private key
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
- // 3. Send token + machine ID + signature
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 || `Server returned HTTP ${response.status}`);
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
- // 4. Create component folder
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
- // 5. Extract bundled file names
133
+ // 8. Extract bundled file names
104
134
  // --------------------------------------------------
105
135
  const bundledFileNames = componentData.files.map((file) => file.name.replace(/\.[^/.]+$/, ""));
106
136
  // --------------------------------------------------
107
- // 6. Write component files
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
- // 7. Install dependencies
150
+ // 10. Install ONLY missing dependencies
121
151
  // --------------------------------------------------
122
- if (componentData.dependencies && componentData.dependencies.length > 0) {
123
- const depsToInstall = componentData.dependencies.join(" ");
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
- // 8. Success
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() === componentName.toLowerCase()) || bundledFileNames[0];
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 || "Unknown CLI Error"));
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.10",
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.");
@@ -12,9 +12,13 @@ import machineIdPkg from "node-machine-id";
12
12
 
13
13
  const { machineIdSync } = machineIdPkg;
14
14
 
15
- const API_BASE_URL = process.env.DS01_API_URL || "https://ds-01.vercel.app";
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(fs.readFileSync(pkgJsonPath, "utf-8"));
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("Tailwind CSS is missing from your project dependencies.\n") +
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(" npm install tailwindcss @tailwindcss/postcss postcss\n\n") +
56
+ pc.cyan(
57
+ " npm install tailwindcss @tailwindcss/postcss postcss\n\n",
58
+ ) +
48
59
  pc.gray("Official Setup Guide: ") +
49
- pc.underline("https://tailwindcss.com/docs/installation"),
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("<component>", "The name of the component (e.g., premium-section)")
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(cwd, "ds01.config.json");
127
+ const configPath = path.join(
128
+ cwd,
129
+ "ds01.config.json",
130
+ );
74
131
 
75
132
  if (!fs.existsSync(configPath)) {
76
- cancel(pc.red("ds01.config.json not found. Run 'npx ds-01 init' first."));
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(fs.readFileSync(configPath, "utf-8"));
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(pc.red("You are not authenticated. Run 'npx ds-01 login' first."));
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(`Fetching ${pc.cyan(`<${componentName} />`)} from registry...`);
165
+ s.start(
166
+ `Fetching ${pc.cyan(
167
+ `<${componentName} />`,
168
+ )} from registry...`,
169
+ );
95
170
 
96
171
  try {
97
172
  // --------------------------------------------------
98
- // 1. Create request-specific message
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
- // 2. Sign message using the device private key
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 = Buffer.from(signature).toString("base64");
193
+ const signatureBase64 =
194
+ Buffer.from(signature).toString("base64");
119
195
 
120
196
  // --------------------------------------------------
121
- // 3. Send token + machine ID + signature
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 = await response.json().catch(() => null);
222
+ const errorData =
223
+ await response.json().catch(() => null);
143
224
 
144
- s.stop(pc.red("Component fetch failed."));
225
+ s.stop(
226
+ pc.red("Component fetch failed."),
227
+ );
145
228
 
146
- cancel(errorData?.error || `Server returned HTTP ${response.status}`);
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 = await response.json();
237
+ const componentData =
238
+ await response.json();
152
239
 
153
240
  // --------------------------------------------------
154
- // 4. Create component folder
241
+ // 7. Create component directory
155
242
  // --------------------------------------------------
156
243
 
157
- const targetDir = path.join(cwd, config.componentsPath, componentName);
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
- // 5. Extract bundled file names
257
+ // 8. Extract bundled file names
167
258
  // --------------------------------------------------
168
259
 
169
- const bundledFileNames = componentData.files.map((file: any) =>
170
- file.name.replace(/\.[^/.]+$/, ""),
171
- );
260
+ const bundledFileNames =
261
+ componentData.files.map(
262
+ (file: any) =>
263
+ file.name.replace(
264
+ /\.[^/.]+$/,
265
+ "",
266
+ ),
267
+ );
172
268
 
173
269
  // --------------------------------------------------
174
- // 6. Write component files
270
+ // 9. Write component files
175
271
  // --------------------------------------------------
176
272
 
177
273
  for (const file of componentData.files) {
178
- const filePath = path.join(targetDir, file.name);
274
+ const filePath = path.join(
275
+ targetDir,
276
+ file.name,
277
+ );
179
278
 
180
279
  let content = file.content;
181
280
 
182
- bundledFileNames.forEach((fileName: string) => {
183
- const regex = new RegExp(
184
- `from\\s+["']\\.[^"']*?\\/${fileName}["']`,
185
- "g",
186
- );
187
-
188
- content = content.replace(regex, `from "./${fileName}"`);
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(filePath, content);
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
- // 7. Install dependencies
310
+ // 10. Install ONLY missing dependencies
204
311
  // --------------------------------------------------
205
312
 
206
- if (componentData.dependencies && componentData.dependencies.length > 0) {
207
- const depsToInstall = componentData.dependencies.join(" ");
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(depsToInstall)}...`,
328
+ `Installing missing dependencies: ${pc.cyan(
329
+ depsToInstall,
330
+ )}...`,
211
331
  );
212
332
 
213
333
  try {
214
- execSync(`npm install ${depsToInstall}`, {
215
- stdio: "ignore",
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(depsToInstall)}`,
343
+ `Dependencies installed successfully: ${pc.gray(
344
+ depsToInstall,
345
+ )}`,
221
346
  ),
222
347
  );
223
348
  } catch {
224
- s.stop(pc.red("Failed to auto-install dependencies."));
349
+ s.stop(
350
+ pc.red(
351
+ "Failed to auto-install dependencies.",
352
+ ),
353
+ );
225
354
 
226
355
  note(
227
- `Please run: ${pc.cyan(`npm install ${depsToInstall}`)} manually.`,
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
- // 8. Success
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) => name.toLowerCase() === componentName.toLowerCase(),
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(pc.red("An error occurred during injection."));
400
+ s.stop(
401
+ pc.red(
402
+ "An error occurred during injection.",
403
+ ),
404
+ );
253
405
 
254
- cancel(pc.gray(error?.message || "Unknown CLI Error"));
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
+ });