@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.
- package/bin/cfb.js +202 -0
- package/package.json +64 -0
- package/src/commands/baseline.js +198 -0
- package/src/commands/init.js +309 -0
- package/src/commands/login.js +71 -0
- package/src/commands/logout.js +44 -0
- package/src/commands/scan.js +1547 -0
- package/src/commands/snapshot.js +191 -0
- package/src/commands/sync.js +127 -0
- package/src/config/baseUrl.js +49 -0
- package/src/data/tailwind-core-spec.js +149 -0
- package/src/engine/runRules.js +210 -0
- package/src/lib/collectDeclaredTokensAuto.js +67 -0
- package/src/lib/collectTokenMatches.js +330 -0
- package/src/lib/collectTokenMatches.js.regex +252 -0
- package/src/lib/loadRules.js +73 -0
- package/src/rules/core/no-hardcoded-colors.js +28 -0
- package/src/rules/core/no-hardcoded-spacing.js +29 -0
- package/src/rules/core/no-inline-styles.js +28 -0
- package/src/utils/authorId.js +106 -0
- package/src/utils/buildAIContributions.js +224 -0
- package/src/utils/buildBlameData.js +388 -0
- package/src/utils/buildDeclaredCssVars.js +185 -0
- package/src/utils/buildDeclaredJson.js +214 -0
- package/src/utils/buildFileChanges.js +372 -0
- package/src/utils/buildRuntimeUsage.js +337 -0
- package/src/utils/detectDeclaredDrift.js +59 -0
- package/src/utils/extractImports.js +178 -0
- package/src/utils/fileExtensions.js +65 -0
- package/src/utils/generateInsights.js +332 -0
- package/src/utils/getAllFiles.js +63 -0
- package/src/utils/getCommitMetaData.js +102 -0
- package/src/utils/getLine.js +14 -0
- package/src/utils/resolveProjectForFolder/index.js +47 -0
- 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
|
+
}
|