@controlfront/detect 0.0.1

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.
Files changed (35) hide show
  1. package/bin/cfb.js +202 -0
  2. package/package.json +64 -0
  3. package/src/commands/baseline.js +198 -0
  4. package/src/commands/init.js +309 -0
  5. package/src/commands/login.js +71 -0
  6. package/src/commands/logout.js +44 -0
  7. package/src/commands/scan.js +1547 -0
  8. package/src/commands/snapshot.js +191 -0
  9. package/src/commands/sync.js +127 -0
  10. package/src/config/baseUrl.js +49 -0
  11. package/src/data/tailwind-core-spec.js +149 -0
  12. package/src/engine/runRules.js +210 -0
  13. package/src/lib/collectDeclaredTokensAuto.js +67 -0
  14. package/src/lib/collectTokenMatches.js +330 -0
  15. package/src/lib/collectTokenMatches.js.regex +252 -0
  16. package/src/lib/loadRules.js +73 -0
  17. package/src/rules/core/no-hardcoded-colors.js +28 -0
  18. package/src/rules/core/no-hardcoded-spacing.js +29 -0
  19. package/src/rules/core/no-inline-styles.js +28 -0
  20. package/src/utils/authorId.js +106 -0
  21. package/src/utils/buildAIContributions.js +224 -0
  22. package/src/utils/buildBlameData.js +388 -0
  23. package/src/utils/buildDeclaredCssVars.js +185 -0
  24. package/src/utils/buildDeclaredJson.js +214 -0
  25. package/src/utils/buildFileChanges.js +372 -0
  26. package/src/utils/buildRuntimeUsage.js +337 -0
  27. package/src/utils/detectDeclaredDrift.js +59 -0
  28. package/src/utils/extractImports.js +178 -0
  29. package/src/utils/fileExtensions.js +65 -0
  30. package/src/utils/generateInsights.js +332 -0
  31. package/src/utils/getAllFiles.js +63 -0
  32. package/src/utils/getCommitMetaData.js +102 -0
  33. package/src/utils/getLine.js +14 -0
  34. package/src/utils/resolveProjectForFolder/index.js +47 -0
  35. package/src/utils/twClassify.js +138 -0
@@ -0,0 +1,309 @@
1
+ import fs from "fs";
2
+ import fetch from "node-fetch";
3
+ import os from "os";
4
+ import path from "path";
5
+ import prompts from "prompts";
6
+ import { fileURLToPath } from "url";
7
+
8
+ // Polyfill for __dirname in ES modules:
9
+ const __filename = fileURLToPath(import.meta.url);
10
+ const __dirname = path.dirname(__filename);
11
+
12
+ import { makeUrl, getStoredEnvironment } from "../config/baseUrl.js";
13
+
14
+ function findGitRepoRoot(startDir) {
15
+ let dir = startDir;
16
+ while (true) {
17
+ const gitDir = path.join(dir, ".git");
18
+ if (fs.existsSync(gitDir)) return dir;
19
+ const parent = path.dirname(dir);
20
+ if (parent === dir) return null;
21
+ dir = parent;
22
+ }
23
+ }
24
+
25
+ export async function runInit() {
26
+ console.log("šŸ”§ Initializing ControlFront CLI...");
27
+ const configPath = path.join(os.homedir(), ".cf", "config.json");
28
+
29
+ if (!fs.existsSync(configPath)) {
30
+ console.log("šŸ” You are not logged into ControlFront, running `cf login`...");
31
+ const { runLogin } = await import("./login.js");
32
+ await runLogin();
33
+ // After login, re-check config file exists
34
+ if (!fs.existsSync(configPath)) {
35
+ console.error(
36
+ "āŒ ~/.cf/config.json still not found after login. Please check your login and try again."
37
+ );
38
+ process.exit(1);
39
+ }
40
+ }
41
+
42
+ const { token, environment } = JSON.parse(fs.readFileSync(configPath, "utf8"));
43
+ const headers = {
44
+ Authorization: `Bearer ${token}`,
45
+ "x-cf-cli": "true"
46
+ };
47
+
48
+ // Log current environment
49
+ const envDisplay = environment === 'prod' ? 'Production' : 'Development';
50
+ console.log(`🌐 Using ${envDisplay} environment`);
51
+
52
+ // Fetch workspaces
53
+ const workspaceRes = await fetch(makeUrl(`/api/cli/workspaces`), {
54
+ method: "GET",
55
+ headers,
56
+ });
57
+ const raw = await workspaceRes.text();
58
+
59
+ if (!workspaceRes.ok) {
60
+ console.error(`āŒ API returned ${workspaceRes.status}: ${workspaceRes.statusText}`);
61
+ console.error("Response body:", raw);
62
+ process.exit(1);
63
+ }
64
+
65
+ let workspaces;
66
+ try {
67
+ workspaces = JSON.parse(raw);
68
+ } catch (err) {
69
+ console.error("āŒ Failed to parse workspaces response as JSON:", raw);
70
+ process.exit(1);
71
+ }
72
+
73
+ if (Array.isArray(workspaces) && workspaces.length > 0 && workspaces[0].workspace_id && !workspaces[0].id) {
74
+ workspaces = workspaces.map(w => ({ id: w.workspace_id, name: w.name }));
75
+ }
76
+
77
+ let workspace;
78
+
79
+ // Build workspace choices for prompt
80
+ const workspaceChoices = (workspaces || []).map(w => ({
81
+ title: w.name || "(unnamed)",
82
+ value: w.id
83
+ }));
84
+
85
+ workspaceChoices.push({ title: "āž• Create a new workspace", value: "__create__" });
86
+
87
+ // Use environment from config we already loaded
88
+ const envDisplayPrompt = environment === 'prod' ? 'Production' : 'Development';
89
+
90
+ // Prompt user to select existing or create new
91
+ const wsResponse = await prompts({
92
+ type: "select",
93
+ name: "selectedWorkspaceId",
94
+ message: `Select a ${envDisplayPrompt} workspace to use (or create a new one):`,
95
+ choices: workspaceChoices
96
+ });
97
+
98
+ if (!wsResponse.selectedWorkspaceId) {
99
+ console.error("āŒ No workspace selected. Aborting.");
100
+ process.exit(1);
101
+ }
102
+
103
+ if (wsResponse.selectedWorkspaceId === "__create__") {
104
+ const wsNameResponse = await prompts({
105
+ type: "text",
106
+ name: "workspaceName",
107
+ message: "Enter a name for the new workspace:",
108
+ initial: "Local"
109
+ });
110
+
111
+ if (!wsNameResponse.workspaceName) {
112
+ console.error("āŒ No workspace name entered. Aborting.");
113
+ process.exit(1);
114
+ }
115
+
116
+ const createWorkspaceRes = await fetch(makeUrl(`/api/cli/workspaces`), {
117
+ method: "POST",
118
+ headers: {
119
+ ...headers,
120
+ "Content-Type": "application/json",
121
+ },
122
+ body: JSON.stringify({ name: wsNameResponse.workspaceName.trim() }),
123
+ });
124
+
125
+ if (!createWorkspaceRes.ok) {
126
+ const errorText = await createWorkspaceRes.text();
127
+ console.error("āŒ Failed to create workspace:", errorText);
128
+ process.exit(1);
129
+ }
130
+
131
+ workspace = await createWorkspaceRes.json();
132
+ } else {
133
+ // Use existing workspace
134
+ workspace = workspaces.find(w => w.id === wsResponse.selectedWorkspaceId);
135
+ }
136
+
137
+ const workspaceId = workspace.id || workspace.workspace_id;
138
+
139
+ // Fetch projects for the workspace
140
+ const projRes = await fetch(makeUrl(`/api/cli/workspaces/${workspaceId}/projects`), {
141
+ method: "GET",
142
+ headers,
143
+ });
144
+
145
+ const projects = await projRes.json();
146
+
147
+ if (!projRes.ok || !Array.isArray(projects)) {
148
+ console.error("āŒ Failed to fetch projects:", projects);
149
+ process.exit(1);
150
+ }
151
+
152
+ const cwd = process.cwd();
153
+ const repoRoot = findGitRepoRoot(cwd);
154
+ if (!repoRoot) {
155
+ console.error("āŒ Cannot initialize: not inside a git repo.");
156
+ process.exit(1);
157
+ }
158
+ const cfGitFolder = path.join(repoRoot, ".git", "cf");
159
+ const projectJsonPath = path.join(cfGitFolder, "project.json");
160
+ if (!fs.existsSync(cfGitFolder)) {
161
+ fs.mkdirSync(cfGitFolder, { recursive: true });
162
+ }
163
+ const folderName = path.basename(cwd);
164
+
165
+ // If workspace exists but has no projects yet, provide helpful UX message
166
+ if (Array.isArray(projects) && projects.length === 0) {
167
+ let project;
168
+ console.log(`šŸ“­ Workspace "${workspace.name}" has no projects yet.`);
169
+ console.log("Let's create the first project for this workspace.\n");
170
+
171
+ const nameResponse = await prompts({
172
+ type: "text",
173
+ name: "projectName",
174
+ message: "Enter a name for the new project:",
175
+ initial: folderName,
176
+ });
177
+
178
+ if (!nameResponse.projectName) {
179
+ console.error("āŒ No project name entered. Aborting.");
180
+ process.exit(1);
181
+ }
182
+
183
+ const createRes = await fetch(makeUrl(`/api/cli/workspaces/${workspaceId}/projects`), {
184
+ method: "POST",
185
+ headers: {
186
+ ...headers,
187
+ "Content-Type": "application/json",
188
+ "x-cf-cli": "true"
189
+ },
190
+ body: JSON.stringify({ name: nameResponse.projectName }),
191
+ });
192
+
193
+ if (!createRes.ok) {
194
+ const errorText = await createRes.text();
195
+ console.error("āŒ Failed to create project:", errorText);
196
+ process.exit(1);
197
+ }
198
+
199
+ project = await createRes.json();
200
+
201
+ const normalizedWorkspace = { ...workspace, id: workspaceId };
202
+
203
+ // Write/update .git/cf/project.json
204
+ fs.writeFileSync(
205
+ projectJsonPath,
206
+ JSON.stringify({ workspace: normalizedWorkspace, project }, null, 2)
207
+ );
208
+
209
+ // Preserve environment when updating config
210
+ fs.writeFileSync(configPath, JSON.stringify({ token, environment }, null, 2));
211
+
212
+ console.log(`āœ… Project "${project.name}" created and workspace initialized.`);
213
+ return { workspace, project };
214
+ }
215
+
216
+ let project;
217
+ // Check existing project.json for folder/project name mismatch or missing
218
+ if (fs.existsSync(projectJsonPath)) {
219
+ try {
220
+ const existingProjectJson = JSON.parse(fs.readFileSync(projectJsonPath, "utf8"));
221
+
222
+ // If project name matches folder name and project ID is in fetched projects, use it
223
+ if (
224
+ existingProjectJson.project?.name === folderName &&
225
+ projects.some((p) => p.id === existingProjectJson.project?.id)
226
+ ) {
227
+ project = existingProjectJson.project;
228
+ needProjectSelection = false;
229
+ } else {
230
+ console.warn(
231
+ "āš ļø project.json project name does not match this folder name or project not found on server."
232
+ );
233
+ }
234
+ } catch {
235
+ // Ignore parse errors and prompt user below
236
+ }
237
+ }
238
+
239
+ // If no valid project found, prompt user to select one
240
+ if (typeof needProjectSelection === "undefined" || needProjectSelection) {
241
+ // Always prompt user to select project from list with option to create new
242
+ const choices = projects.map((p) => ({
243
+ title: p.name,
244
+ value: p.id,
245
+ }));
246
+ choices.push({ title: "āž• Create a new project", value: "__create__" });
247
+
248
+ const response = await prompts({
249
+ type: "select",
250
+ name: "selectedProjectId",
251
+ message: "Select the ControlFront project to use for this folder:",
252
+ choices,
253
+ });
254
+
255
+ if (!response.selectedProjectId) {
256
+ console.error("āŒ No project selected. Aborting.");
257
+ process.exit(1);
258
+ }
259
+
260
+ if (response.selectedProjectId === "__create__") {
261
+ const nameResponse = await prompts({
262
+ type: "text",
263
+ name: "projectName",
264
+ message: "Enter a name for the new project:",
265
+ initial: folderName,
266
+ });
267
+
268
+ if (!nameResponse.projectName) {
269
+ console.error("āŒ No project name entered. Aborting.");
270
+ process.exit(1);
271
+ }
272
+
273
+ const createRes = await fetch(makeUrl(`/api/cli/workspaces/${workspaceId}/projects`), {
274
+ method: "POST",
275
+ headers: {
276
+ ...headers,
277
+ "Content-Type": "application/json",
278
+ "x-cf-cli": "true"
279
+ },
280
+ body: JSON.stringify({ name: nameResponse.projectName }),
281
+ });
282
+
283
+ if (!createRes.ok) {
284
+ const errorText = await createRes.text();
285
+ console.error("āŒ Failed to create project:", errorText);
286
+ process.exit(1);
287
+ }
288
+
289
+ project = await createRes.json();
290
+ } else {
291
+ project = projects.find((p) => p.id === response.selectedProjectId);
292
+ }
293
+ }
294
+
295
+ const normalizedWorkspace = { ...workspace, id: workspaceId };
296
+
297
+ // Write/update .git/cf/project.json
298
+ fs.writeFileSync(
299
+ projectJsonPath,
300
+ JSON.stringify({ workspace: normalizedWorkspace, project }, null, 2)
301
+ );
302
+
303
+ // Preserve environment when updating config
304
+ fs.writeFileSync(configPath, JSON.stringify({ token, environment }, null, 2));
305
+
306
+
307
+ console.log(`āœ… ControlFront CLI is initialized.`);
308
+ return { workspace, project };
309
+ }
@@ -0,0 +1,71 @@
1
+ import crypto from "crypto";
2
+ import fs from "fs";
3
+ import fetch from "node-fetch";
4
+ import open from "open";
5
+ import os from "os";
6
+ import path from "path";
7
+ import { fileURLToPath } from "url";
8
+ import { resolveBaseUrlForEnv } from "../config/baseUrl.js";
9
+ import { runInit } from "./init.js";
10
+
11
+ // Polyfill for __dirname in ES modules:
12
+ const __filename = fileURLToPath(import.meta.url);
13
+ const __dirname = path.dirname(__filename);
14
+
15
+ export async function runLogin(options = {}) {
16
+ const isProd = options.prod === true;
17
+ const environment = isProd ? "prod" : "dev";
18
+
19
+ const code = crypto.randomBytes(16).toString("hex");
20
+ const cfDir = path.join(os.homedir(), ".cf");
21
+ const configPath = path.join(cfDir, "config.json");
22
+
23
+ if (!fs.existsSync(cfDir)) { fs.mkdirSync(cfDir, { recursive: true }); }
24
+
25
+ // Get the URL for the selected environment
26
+ const envUrl = resolveBaseUrlForEnv(environment);
27
+
28
+ console.log(`🌐 Environment: ${isProd ? '🌐 Production' : 'šŸ  Development'} - ${envUrl}`);
29
+ console.log("šŸ” Opening browser for login...");
30
+
31
+ const makeEnvUrl = (pathAndQuery) => new URL(pathAndQuery, envUrl + "/").toString();
32
+
33
+ const loginUrl = makeEnvUrl(`/sign-in?redirect_url=/cli-login?code=${code}`);
34
+ open(loginUrl);
35
+
36
+ console.log("ā³ Waiting for authentication...");
37
+
38
+ for (let i = 0; i < 30; i++) {
39
+ try {
40
+ const res = await fetch(makeEnvUrl(`/api/cli-login-status?code=${code}`));
41
+ const json = await res.json();
42
+
43
+ if (json.token) {
44
+ console.log("āœ… Login successful. Saving credentials...");
45
+
46
+ const configData = { token: json.token, environment };
47
+ fs.writeFileSync(
48
+ configPath,
49
+ JSON.stringify(configData, null, 2),
50
+ 'utf8'
51
+ );
52
+
53
+ // Verify the save worked
54
+ const saved = JSON.parse(fs.readFileSync(configPath, "utf8"));
55
+ console.log(`šŸ“ Credentials saved to ~/.cf/config.json (${saved.environment === 'prod' ? 'Production' : 'Development'})`);
56
+
57
+ // Auto-run init after successful login
58
+ console.log("\nšŸš€ Initializing project...\n");
59
+ await runInit();
60
+
61
+ return;
62
+ }
63
+ } catch (err) {
64
+ console.error("āŒ Polling error:", err);
65
+ }
66
+
67
+ await new Promise((r) => setTimeout(r, 2000));
68
+ }
69
+
70
+ console.log("āŒ Login timed out. Please try again.");
71
+ }
@@ -0,0 +1,44 @@
1
+ import fs from "fs";
2
+ import path from "path";
3
+ import os from "os";
4
+
5
+ function findGitRoot(startDir) {
6
+ let dir = startDir;
7
+ while (true) {
8
+ const gitPath = path.join(dir, ".git");
9
+ if (fs.existsSync(gitPath) && fs.lstatSync(gitPath).isDirectory()) {
10
+ return dir;
11
+ }
12
+ const parent = path.dirname(dir);
13
+ if (parent === dir) return null;
14
+ dir = parent;
15
+ }
16
+ }
17
+
18
+ export async function runLogout() {
19
+ const configPath = path.join(os.homedir(), ".cf", "config.json");
20
+
21
+ const gitRoot = findGitRoot(process.cwd());
22
+ let projectConfigPath = null;
23
+ if (gitRoot) {
24
+ projectConfigPath = path.join(gitRoot, ".git", "cf", "project.json");
25
+ }
26
+
27
+ if (fs.existsSync(configPath)) {
28
+ fs.unlinkSync(configPath);
29
+ console.log("āœ… Logged out and removed credentials.");
30
+ } else {
31
+ console.log("ā„¹ļø No credentials found. Already logged out.");
32
+ }
33
+
34
+ if (projectConfigPath && fs.existsSync(projectConfigPath)) {
35
+ try {
36
+ fs.unlinkSync(projectConfigPath);
37
+ console.log("āœ… Removed project link (.git/cf/project.json).");
38
+ } catch (err) {
39
+ console.error("āŒ Failed to remove project link:", err);
40
+ }
41
+ } else {
42
+ console.log("ā„¹ļø No project link found.");
43
+ }
44
+ }