ds-01 1.0.9 → 1.0.11

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
  });
@@ -1 +1 @@
1
- {"version":3,"file":"secureKeyStore.d.ts","sourceRoot":"","sources":["../../src/utils/secureKeyStore.ts"],"names":[],"mappings":"AAmZA;;;;GAIG;AAEH,wBAAsB,eAAe,IAAI,OAAO,CAAC,IAAI,CAAC,CASrD;AAED,wBAAsB,kBAAkB,IAAI,OAAO,CAAC,MAAM,CAAC,CAQ1D;AAED,wBAAsB,iBAAiB,CACrC,IAAI,EAAE,UAAU,GACf,OAAO,CAAC,UAAU,CAAC,CAQrB;AAED,wBAAsB,eAAe,IAAI,OAAO,CAAC,IAAI,CAAC,CASrD"}
1
+ {"version":3,"file":"secureKeyStore.d.ts","sourceRoot":"","sources":["../../src/utils/secureKeyStore.ts"],"names":[],"mappings":"AA2WA;;;;GAIG;AAEH,wBAAsB,eAAe,IAAI,OAAO,CAAC,IAAI,CAAC,CASrD;AAED,wBAAsB,kBAAkB,IAAI,OAAO,CAAC,MAAM,CAAC,CAQ1D;AAED,wBAAsB,iBAAiB,CACrC,IAAI,EAAE,UAAU,GACf,OAAO,CAAC,UAAU,CAAC,CAQrB;AAED,wBAAsB,eAAe,IAAI,OAAO,CAAC,IAAI,CAAC,CASrD"}
@@ -9,8 +9,6 @@ const KEY_NAME = "DS01-CLI-Device-Key";
9
9
  const __filename = fileURLToPath(import.meta.url);
10
10
  const __dirname = path.dirname(__filename);
11
11
  function getMacHelperPath() {
12
- // dist/utils/secureKeyStore.js
13
- // ../native/macos-key-helper.swift
14
12
  return path.resolve(__dirname, "../native/macos-key-helper.swift");
15
13
  }
16
14
  function ensureSupportedOS() {
@@ -23,52 +21,16 @@ function ensureSupportedOS() {
23
21
  * ---------------------------------------------------------
24
22
  * WINDOWS
25
23
  * ---------------------------------------------------------
26
- *
27
- * Windows uses:
28
- *
29
- * Microsoft Platform Crypto Provider
30
- * RSA 2048
31
- * NonExportable private key
32
- *
33
- * The private key never enters Node.js.
34
- *
35
- * Node -> PowerShell -> Windows crypto provider
36
- * ↓
37
- * private key
38
- * ↓
39
- * signature
40
- * ↓
41
- * Node
42
- *
43
- * IMPORTANT:
44
- *
45
- * We identify the certificate using its properties rather
46
- * than blindly taking the first certificate with the same
47
- * subject.
48
24
  */
49
- /**
50
- * PowerShell expression used to identify a valid DS01
51
- * certificate.
52
- *
53
- * Requirements:
54
- *
55
- * - Correct subject
56
- * - Has private key
57
- * - RSA certificate
58
- * - Platform Crypto Provider
59
- */
60
- const WINDOWS_CERT_FILTER = `
61
- $_.Subject -eq "CN=${KEY_NAME}" -and
62
- $_.HasPrivateKey -and
63
- $_.PublicKey.Oid.Value -eq "1.2.840.113549.1.1.1"
64
- `;
65
25
  async function createWindowsDeviceKey() {
66
26
  const script = `
67
27
  $ErrorActionPreference = "Stop"
68
28
 
69
29
  $certificates = @(Get-ChildItem Cert:\\CurrentUser\\My |
70
30
  Where-Object {
71
- ${WINDOWS_CERT_FILTER}
31
+ $_.Subject -eq "CN=${KEY_NAME}" -and
32
+ $_.HasPrivateKey -and
33
+ $_.PublicKey.Oid.Value -eq "1.2.840.113549.1.1.1"
72
34
  })
73
35
 
74
36
  if ($certificates.Count -gt 1) {
@@ -82,7 +44,6 @@ if ($certificates.Count -eq 1) {
82
44
 
83
45
  $params = @{
84
46
  Type = "Custom"
85
-
86
47
  Subject = "CN=${KEY_NAME}"
87
48
 
88
49
  Provider = "Microsoft Platform Crypto Provider"
@@ -90,7 +51,6 @@ $params = @{
90
51
  KeyAlgorithm = "RSA"
91
52
  KeyLength = 2048
92
53
 
93
- # Private key cannot be exported.
94
54
  KeyExportPolicy = "NonExportable"
95
55
 
96
56
  KeyUsage = "DigitalSignature"
@@ -125,18 +85,21 @@ Write-Output "CREATED"
125
85
  }
126
86
  }
127
87
  /**
128
- * Find the exact DS01 certificate.
88
+ * Find exactly one valid DS01 certificate.
129
89
  *
130
- * We deliberately DO NOT use Select-Object -First 1.
90
+ * We intentionally do not use Select-Object -First 1.
131
91
  *
132
- * If multiple matching certificates exist, we fail instead
133
- * of potentially using the wrong private key.
92
+ * 0 certificates -> error
93
+ * 1 certificate -> use it
94
+ * 2+ certificates -> error
134
95
  */
135
- function windowsCertificateLookupScript() {
96
+ function getWindowsCertificateLookup() {
136
97
  return `
137
98
  $certificates = @(Get-ChildItem Cert:\\CurrentUser\\My |
138
99
  Where-Object {
139
- ${WINDOWS_CERT_FILTER}
100
+ $_.Subject -eq "CN=${KEY_NAME}" -and
101
+ $_.HasPrivateKey -and
102
+ $_.PublicKey.Oid.Value -eq "1.2.840.113549.1.1.1"
140
103
  })
141
104
 
142
105
  if ($certificates.Count -eq 0) {
@@ -147,19 +110,15 @@ if ($certificates.Count -gt 1) {
147
110
  throw "Multiple valid DS01 device certificates found."
148
111
  }
149
112
 
150
- $certificates[0]
113
+ $cert = [System.Security.Cryptography.X509Certificates.X509Certificate2]$certificates[0]
151
114
  `;
152
115
  }
153
116
  async function getWindowsPublicKey() {
154
117
  const script = `
155
118
  $ErrorActionPreference = "Stop"
156
119
 
157
- $cert = ${windowsCertificateLookupScript()}
120
+ ${getWindowsCertificateLookup()}
158
121
 
159
- # Return the complete public certificate.
160
- #
161
- # The certificate contains the public key and certificate
162
- # metadata, but never the private key.
163
122
  [Convert]::ToBase64String($cert.RawData)
164
123
  `;
165
124
  const { stdout } = await execFileAsync("powershell.exe", [
@@ -183,7 +142,7 @@ async function signWindowsDeviceKey(data) {
183
142
  const script = `
184
143
  $ErrorActionPreference = "Stop"
185
144
 
186
- $cert = ${windowsCertificateLookupScript()}
145
+ ${getWindowsCertificateLookup()}
187
146
 
188
147
  if (-not $cert.HasPrivateKey) {
189
148
  throw "DS01 device certificate has no private key."
@@ -233,7 +192,9 @@ $ErrorActionPreference = "Stop"
233
192
 
234
193
  $certificates = @(Get-ChildItem Cert:\\CurrentUser\\My |
235
194
  Where-Object {
236
- ${WINDOWS_CERT_FILTER}
195
+ $_.Subject -eq "CN=${KEY_NAME}" -and
196
+ $_.HasPrivateKey -and
197
+ $_.PublicKey.Oid.Value -eq "1.2.840.113549.1.1.1"
237
198
  })
238
199
 
239
200
  if ($certificates.Count -gt 1) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ds-01",
3
- "version": "1.0.9",
3
+ "version": "1.0.11",
4
4
  "description": "Make your site best with DS01",
5
5
  "type": "module",
6
6
  "bin": {
@@ -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
+ });
@@ -14,8 +14,6 @@ const __filename = fileURLToPath(import.meta.url);
14
14
  const __dirname = path.dirname(__filename);
15
15
 
16
16
  function getMacHelperPath(): string {
17
- // dist/utils/secureKeyStore.js
18
- // ../native/macos-key-helper.swift
19
17
  return path.resolve(
20
18
  __dirname,
21
19
  "../native/macos-key-helper.swift",
@@ -36,54 +34,17 @@ function ensureSupportedOS(): void {
36
34
  * ---------------------------------------------------------
37
35
  * WINDOWS
38
36
  * ---------------------------------------------------------
39
- *
40
- * Windows uses:
41
- *
42
- * Microsoft Platform Crypto Provider
43
- * RSA 2048
44
- * NonExportable private key
45
- *
46
- * The private key never enters Node.js.
47
- *
48
- * Node -> PowerShell -> Windows crypto provider
49
- * ↓
50
- * private key
51
- * ↓
52
- * signature
53
- * ↓
54
- * Node
55
- *
56
- * IMPORTANT:
57
- *
58
- * We identify the certificate using its properties rather
59
- * than blindly taking the first certificate with the same
60
- * subject.
61
37
  */
62
38
 
63
- /**
64
- * PowerShell expression used to identify a valid DS01
65
- * certificate.
66
- *
67
- * Requirements:
68
- *
69
- * - Correct subject
70
- * - Has private key
71
- * - RSA certificate
72
- * - Platform Crypto Provider
73
- */
74
- const WINDOWS_CERT_FILTER = `
75
- $_.Subject -eq "CN=${KEY_NAME}" -and
76
- $_.HasPrivateKey -and
77
- $_.PublicKey.Oid.Value -eq "1.2.840.113549.1.1.1"
78
- `;
79
-
80
39
  async function createWindowsDeviceKey(): Promise<void> {
81
40
  const script = `
82
41
  $ErrorActionPreference = "Stop"
83
42
 
84
43
  $certificates = @(Get-ChildItem Cert:\\CurrentUser\\My |
85
44
  Where-Object {
86
- ${WINDOWS_CERT_FILTER}
45
+ $_.Subject -eq "CN=${KEY_NAME}" -and
46
+ $_.HasPrivateKey -and
47
+ $_.PublicKey.Oid.Value -eq "1.2.840.113549.1.1.1"
87
48
  })
88
49
 
89
50
  if ($certificates.Count -gt 1) {
@@ -97,7 +58,6 @@ if ($certificates.Count -eq 1) {
97
58
 
98
59
  $params = @{
99
60
  Type = "Custom"
100
-
101
61
  Subject = "CN=${KEY_NAME}"
102
62
 
103
63
  Provider = "Microsoft Platform Crypto Provider"
@@ -105,7 +65,6 @@ $params = @{
105
65
  KeyAlgorithm = "RSA"
106
66
  KeyLength = 2048
107
67
 
108
- # Private key cannot be exported.
109
68
  KeyExportPolicy = "NonExportable"
110
69
 
111
70
  KeyUsage = "DigitalSignature"
@@ -150,18 +109,21 @@ Write-Output "CREATED"
150
109
  }
151
110
 
152
111
  /**
153
- * Find the exact DS01 certificate.
112
+ * Find exactly one valid DS01 certificate.
154
113
  *
155
- * We deliberately DO NOT use Select-Object -First 1.
114
+ * We intentionally do not use Select-Object -First 1.
156
115
  *
157
- * If multiple matching certificates exist, we fail instead
158
- * of potentially using the wrong private key.
116
+ * 0 certificates -> error
117
+ * 1 certificate -> use it
118
+ * 2+ certificates -> error
159
119
  */
160
- function windowsCertificateLookupScript(): string {
120
+ function getWindowsCertificateLookup(): string {
161
121
  return `
162
122
  $certificates = @(Get-ChildItem Cert:\\CurrentUser\\My |
163
123
  Where-Object {
164
- ${WINDOWS_CERT_FILTER}
124
+ $_.Subject -eq "CN=${KEY_NAME}" -and
125
+ $_.HasPrivateKey -and
126
+ $_.PublicKey.Oid.Value -eq "1.2.840.113549.1.1.1"
165
127
  })
166
128
 
167
129
  if ($certificates.Count -eq 0) {
@@ -172,7 +134,7 @@ if ($certificates.Count -gt 1) {
172
134
  throw "Multiple valid DS01 device certificates found."
173
135
  }
174
136
 
175
- $certificates[0]
137
+ $cert = [System.Security.Cryptography.X509Certificates.X509Certificate2]$certificates[0]
176
138
  `;
177
139
  }
178
140
 
@@ -180,12 +142,8 @@ async function getWindowsPublicKey(): Promise<string> {
180
142
  const script = `
181
143
  $ErrorActionPreference = "Stop"
182
144
 
183
- $cert = ${windowsCertificateLookupScript()}
145
+ ${getWindowsCertificateLookup()}
184
146
 
185
- # Return the complete public certificate.
186
- #
187
- # The certificate contains the public key and certificate
188
- # metadata, but never the private key.
189
147
  [Convert]::ToBase64String($cert.RawData)
190
148
  `;
191
149
 
@@ -224,7 +182,7 @@ async function signWindowsDeviceKey(
224
182
  const script = `
225
183
  $ErrorActionPreference = "Stop"
226
184
 
227
- $cert = ${windowsCertificateLookupScript()}
185
+ ${getWindowsCertificateLookup()}
228
186
 
229
187
  if (-not $cert.HasPrivateKey) {
230
188
  throw "DS01 device certificate has no private key."
@@ -287,7 +245,9 @@ $ErrorActionPreference = "Stop"
287
245
 
288
246
  $certificates = @(Get-ChildItem Cert:\\CurrentUser\\My |
289
247
  Where-Object {
290
- ${WINDOWS_CERT_FILTER}
248
+ $_.Subject -eq "CN=${KEY_NAME}" -and
249
+ $_.HasPrivateKey -and
250
+ $_.PublicKey.Oid.Value -eq "1.2.840.113549.1.1.1"
291
251
  })
292
252
 
293
253
  if ($certificates.Count -gt 1) {