at-builder 1.4.4 → 1.5.0
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/README.md +38 -0
- package/bin/constants/config.js +1 -1
- package/bin/index.js +153 -66
- package/bin/services/telemetry.js +320 -0
- package/package.json +12 -1
- package/.claude/settings.local.json +0 -77
- package/.plop/index.js +0 -5
- package/.vscode/settings.json +0 -6
- package/CustomWrapperPlugin.js +0 -61
- package/DEVELOPMENT.md +0 -164
- package/at-builder-0.0.2.vsix +0 -0
- package/package/package.json +0 -86
- package/src/constants/config.ts +0 -321
- package/src/index.ts +0 -387
- package/src/services/doctor.ts +0 -724
- package/src/services/logger.ts +0 -84
package/src/services/doctor.ts
DELETED
|
@@ -1,724 +0,0 @@
|
|
|
1
|
-
import path from "path";
|
|
2
|
-
import fs from "fs";
|
|
3
|
-
import logger from "./logger";
|
|
4
|
-
import { GITIGNORE_TEMPLATE } from "../constants/config";
|
|
5
|
-
|
|
6
|
-
// lib/install-checks.js is hand-written runtime JS (not TS-compiled). The
|
|
7
|
-
// relative path resolves correctly from both src/services/doctor.ts (source)
|
|
8
|
-
// and bin/services/doctor.js (compiled output) because src/ and bin/ have
|
|
9
|
-
// parallel structure. Typed inline since the module isn't part of the TS graph.
|
|
10
|
-
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
11
|
-
const installChecks = require("../../lib/install-checks") as {
|
|
12
|
-
checkInstallEnvironment: (opts?: { localBinScript?: string; fixPermissions?: boolean }) => {
|
|
13
|
-
ok: boolean;
|
|
14
|
-
problems: string[];
|
|
15
|
-
globalBin: string;
|
|
16
|
-
shimPath: string;
|
|
17
|
-
shimExecutable: boolean;
|
|
18
|
-
isWin: boolean;
|
|
19
|
-
isGlobalInstall: boolean;
|
|
20
|
-
nodeMajor: number;
|
|
21
|
-
};
|
|
22
|
-
};
|
|
23
|
-
|
|
24
|
-
export interface DiagnosticIssue {
|
|
25
|
-
id: string;
|
|
26
|
-
severity: 'error' | 'warning';
|
|
27
|
-
message: string;
|
|
28
|
-
suggestion?: string;
|
|
29
|
-
fixable: boolean;
|
|
30
|
-
file?: string;
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
/**
|
|
34
|
-
* Run comprehensive diagnostics on the project
|
|
35
|
-
*/
|
|
36
|
-
export const runDiagnostics = async (projectPath: string, verbose: boolean = false): Promise<DiagnosticIssue[]> => {
|
|
37
|
-
const issues: DiagnosticIssue[] = [];
|
|
38
|
-
|
|
39
|
-
if (verbose) logger.info("runDiagnostics", `Running diagnostics on ${projectPath}`);
|
|
40
|
-
|
|
41
|
-
// Check for required files
|
|
42
|
-
await checkEnvFile(projectPath, issues);
|
|
43
|
-
await checkAdobeConfig(projectPath, issues);
|
|
44
|
-
await checkWatchConfig(projectPath, issues);
|
|
45
|
-
await checkGitignore(projectPath, issues);
|
|
46
|
-
await checkPackageJson(projectPath, issues);
|
|
47
|
-
await checkActivitiesFolder(projectPath, issues);
|
|
48
|
-
|
|
49
|
-
// Check environment variables
|
|
50
|
-
await checkEnvVariables(projectPath, issues);
|
|
51
|
-
|
|
52
|
-
// Check dependencies
|
|
53
|
-
await checkDependencies(projectPath, issues);
|
|
54
|
-
|
|
55
|
-
// Check build.config.json content for every activity
|
|
56
|
-
await checkBuildConfigs(projectPath, issues);
|
|
57
|
-
|
|
58
|
-
// Check install environment (PATH, atb shim, exec bit). Mirrors the
|
|
59
|
-
// postinstall validator so users who installed with --ignore-scripts
|
|
60
|
-
// (or who dismissed the postinstall warning) can still self-diagnose.
|
|
61
|
-
await checkInstallEnv(issues);
|
|
62
|
-
|
|
63
|
-
return issues;
|
|
64
|
-
};
|
|
65
|
-
|
|
66
|
-
/**
|
|
67
|
-
* Attempt to fix the provided issues
|
|
68
|
-
*/
|
|
69
|
-
export const fixIssues = async (issues: DiagnosticIssue[], projectPath: string, verbose: boolean = false): Promise<number> => {
|
|
70
|
-
let fixedCount = 0;
|
|
71
|
-
|
|
72
|
-
for (const issue of issues) {
|
|
73
|
-
if (!issue.fixable) continue;
|
|
74
|
-
|
|
75
|
-
try {
|
|
76
|
-
if (verbose) logger.info("fixIssues", `Attempting to fix: ${issue.id}`);
|
|
77
|
-
|
|
78
|
-
const success = await fixIssue(issue, projectPath, verbose);
|
|
79
|
-
if (success) {
|
|
80
|
-
console.log(`✅ Fixed: ${issue.message}`);
|
|
81
|
-
fixedCount++;
|
|
82
|
-
} else {
|
|
83
|
-
console.log(`❌ Could not fix: ${issue.message}`);
|
|
84
|
-
}
|
|
85
|
-
} catch (error) {
|
|
86
|
-
console.log(`❌ Error fixing ${issue.id}: ${error.message}`);
|
|
87
|
-
}
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
return fixedCount;
|
|
91
|
-
};
|
|
92
|
-
|
|
93
|
-
/**
|
|
94
|
-
* Fix a specific issue
|
|
95
|
-
*/
|
|
96
|
-
const fixIssue = async (issue: DiagnosticIssue, projectPath: string, verbose: boolean): Promise<boolean> => {
|
|
97
|
-
switch (issue.id) {
|
|
98
|
-
case 'missing-env':
|
|
99
|
-
return await createEnvFile(projectPath);
|
|
100
|
-
case 'missing-adobe-config':
|
|
101
|
-
return await createAdobeConfig(projectPath);
|
|
102
|
-
case 'legacy-watch-config':
|
|
103
|
-
return await migrateWatchConfig(projectPath);
|
|
104
|
-
case 'missing-gitignore':
|
|
105
|
-
return await createGitignore(projectPath);
|
|
106
|
-
case 'missing-activities-folder':
|
|
107
|
-
return await createActivitiesFolder(projectPath);
|
|
108
|
-
case 'missing-package-json':
|
|
109
|
-
return await createPackageJson(projectPath);
|
|
110
|
-
default:
|
|
111
|
-
return false;
|
|
112
|
-
}
|
|
113
|
-
};
|
|
114
|
-
|
|
115
|
-
/**
|
|
116
|
-
* Check if .env file exists and has required variables
|
|
117
|
-
*/
|
|
118
|
-
const checkEnvFile = async (projectPath: string, issues: DiagnosticIssue[]): Promise<void> => {
|
|
119
|
-
const envPath = path.join(projectPath, '.env');
|
|
120
|
-
|
|
121
|
-
if (!fs.existsSync(envPath)) {
|
|
122
|
-
issues.push({
|
|
123
|
-
id: 'missing-env',
|
|
124
|
-
severity: 'error',
|
|
125
|
-
message: '.env file is missing',
|
|
126
|
-
suggestion: 'Create .env file with required environment variables',
|
|
127
|
-
fixable: true,
|
|
128
|
-
file: '.env'
|
|
129
|
-
});
|
|
130
|
-
return;
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
// Check if .env has required variables
|
|
134
|
-
const envContent = fs.readFileSync(envPath, 'utf8');
|
|
135
|
-
const requiredVars = [
|
|
136
|
-
'ACTIVITIES_BASE_FOLDER',
|
|
137
|
-
'ACTIVITY_FOLDER_NAME',
|
|
138
|
-
'ADOBE_CLIENT_ID',
|
|
139
|
-
'ADOBE_CLIENT_SECRET'
|
|
140
|
-
];
|
|
141
|
-
|
|
142
|
-
const missingVars = requiredVars.filter(varName => !envContent.includes(varName));
|
|
143
|
-
|
|
144
|
-
if (missingVars.length > 0) {
|
|
145
|
-
issues.push({
|
|
146
|
-
id: 'incomplete-env',
|
|
147
|
-
severity: 'warning',
|
|
148
|
-
message: `.env file missing variables: ${missingVars.join(', ')}`,
|
|
149
|
-
suggestion: 'Add missing environment variables to .env file',
|
|
150
|
-
fixable: true,
|
|
151
|
-
file: '.env'
|
|
152
|
-
});
|
|
153
|
-
}
|
|
154
|
-
};
|
|
155
|
-
|
|
156
|
-
/**
|
|
157
|
-
* Check if adobe.config.js exists
|
|
158
|
-
*/
|
|
159
|
-
const checkAdobeConfig = async (projectPath: string, issues: DiagnosticIssue[]): Promise<void> => {
|
|
160
|
-
const configPath = path.join(projectPath, 'adobe.config.js');
|
|
161
|
-
|
|
162
|
-
if (!fs.existsSync(configPath)) {
|
|
163
|
-
issues.push({
|
|
164
|
-
id: 'missing-adobe-config',
|
|
165
|
-
severity: 'error',
|
|
166
|
-
message: 'adobe.config.js file is missing',
|
|
167
|
-
suggestion: 'Create Adobe Target API configuration file',
|
|
168
|
-
fixable: true,
|
|
169
|
-
file: 'adobe.config.js'
|
|
170
|
-
});
|
|
171
|
-
}
|
|
172
|
-
};
|
|
173
|
-
|
|
174
|
-
/**
|
|
175
|
-
* Flag a legacy watch-config.json. As of this release VARIATION/PAGE live in
|
|
176
|
-
* .env; watch-config.json is supported as a fallback (puppeteer.js prefers it
|
|
177
|
-
* if present with a deprecation warning) but new projects don't get one.
|
|
178
|
-
*
|
|
179
|
-
* The fix migrates VARIATION/PAGE into .env and deletes watch-config.json.
|
|
180
|
-
*/
|
|
181
|
-
const checkWatchConfig = async (projectPath: string, issues: DiagnosticIssue[]): Promise<void> => {
|
|
182
|
-
const configPath = path.join(projectPath, 'watch-config.json');
|
|
183
|
-
if (!fs.existsSync(configPath)) return;
|
|
184
|
-
|
|
185
|
-
issues.push({
|
|
186
|
-
id: 'legacy-watch-config',
|
|
187
|
-
severity: 'warning',
|
|
188
|
-
message: 'watch-config.json is deprecated — VARIATION/PAGE now live in .env',
|
|
189
|
-
suggestion: 'Run "atb doctor --fix" to migrate values into .env and delete watch-config.json',
|
|
190
|
-
fixable: true,
|
|
191
|
-
file: 'watch-config.json'
|
|
192
|
-
});
|
|
193
|
-
};
|
|
194
|
-
|
|
195
|
-
/**
|
|
196
|
-
* Check if .gitignore exists. Flags as a warning (not blocking) but fixable —
|
|
197
|
-
* `atb doctor --fix` writes the default template covering .env, .deploy-lock,
|
|
198
|
-
* dist/, etc. Does not modify an existing .gitignore.
|
|
199
|
-
*/
|
|
200
|
-
const checkGitignore = async (projectPath: string, issues: DiagnosticIssue[]): Promise<void> => {
|
|
201
|
-
const gitignorePath = path.join(projectPath, '.gitignore');
|
|
202
|
-
|
|
203
|
-
if (!fs.existsSync(gitignorePath)) {
|
|
204
|
-
issues.push({
|
|
205
|
-
id: 'missing-gitignore',
|
|
206
|
-
severity: 'warning',
|
|
207
|
-
message: '.gitignore file is missing',
|
|
208
|
-
suggestion: 'Create .gitignore with at-builder defaults (.env, .deploy-lock, dist/, etc.)',
|
|
209
|
-
fixable: true,
|
|
210
|
-
file: '.gitignore'
|
|
211
|
-
});
|
|
212
|
-
}
|
|
213
|
-
};
|
|
214
|
-
|
|
215
|
-
/**
|
|
216
|
-
* Check if package.json exists
|
|
217
|
-
*/
|
|
218
|
-
const checkPackageJson = async (projectPath: string, issues: DiagnosticIssue[]): Promise<void> => {
|
|
219
|
-
const packagePath = path.join(projectPath, 'package.json');
|
|
220
|
-
|
|
221
|
-
if (!fs.existsSync(packagePath)) {
|
|
222
|
-
issues.push({
|
|
223
|
-
id: 'missing-package-json',
|
|
224
|
-
severity: 'error',
|
|
225
|
-
message: 'package.json file is missing',
|
|
226
|
-
suggestion: 'Initialize npm project with package.json',
|
|
227
|
-
fixable: true,
|
|
228
|
-
file: 'package.json'
|
|
229
|
-
});
|
|
230
|
-
}
|
|
231
|
-
};
|
|
232
|
-
|
|
233
|
-
/**
|
|
234
|
-
* Resolve the configured activities folder name. Reads ACTIVITIES_BASE_FOLDER
|
|
235
|
-
* from the consumer .env (matching webpack/at-deploy/at-sync resolution), falls
|
|
236
|
-
* back to the default "Activities".
|
|
237
|
-
*/
|
|
238
|
-
const resolveActivitiesFolderName = (projectPath: string): string => {
|
|
239
|
-
const envPath = path.join(projectPath, '.env');
|
|
240
|
-
if (!fs.existsSync(envPath)) return 'Activities';
|
|
241
|
-
const envContent = fs.readFileSync(envPath, 'utf8');
|
|
242
|
-
const match = envContent.match(/ACTIVITIES_BASE_FOLDER=["']?([^"'\n\r]+)["']?/);
|
|
243
|
-
return match ? match[1] : 'Activities';
|
|
244
|
-
};
|
|
245
|
-
|
|
246
|
-
/**
|
|
247
|
-
* Check if Activities folder exists
|
|
248
|
-
*/
|
|
249
|
-
const checkActivitiesFolder = async (projectPath: string, issues: DiagnosticIssue[]): Promise<void> => {
|
|
250
|
-
const activitiesFolder = resolveActivitiesFolderName(projectPath);
|
|
251
|
-
const activitiesPath = path.join(projectPath, activitiesFolder);
|
|
252
|
-
|
|
253
|
-
if (!fs.existsSync(activitiesPath)) {
|
|
254
|
-
issues.push({
|
|
255
|
-
id: 'missing-activities-folder',
|
|
256
|
-
severity: 'warning',
|
|
257
|
-
message: `${activitiesFolder} folder is missing`,
|
|
258
|
-
suggestion: `Create ${activitiesFolder} folder for your activities`,
|
|
259
|
-
fixable: true,
|
|
260
|
-
file: activitiesFolder
|
|
261
|
-
});
|
|
262
|
-
}
|
|
263
|
-
};
|
|
264
|
-
|
|
265
|
-
/**
|
|
266
|
-
* Validate every shared/build.config.json under the activities folder.
|
|
267
|
-
*
|
|
268
|
-
* Catches the deploy-time errors before deploy:
|
|
269
|
-
* - missing or invalid activityType (must be "ab" or "xt")
|
|
270
|
-
* - null id (deploy will fail when it tries to GET the activity)
|
|
271
|
-
* - multi-page variation values without an activityInfo.pages map
|
|
272
|
-
* - variation/page folders referenced in config but absent on disk
|
|
273
|
-
*
|
|
274
|
-
* Issues are not auto-fixable (they're content problems requiring human
|
|
275
|
-
* judgment), so each one is reported with a clear suggestion.
|
|
276
|
-
*/
|
|
277
|
-
const checkBuildConfigs = async (projectPath: string, issues: DiagnosticIssue[]): Promise<void> => {
|
|
278
|
-
const activitiesFolder = resolveActivitiesFolderName(projectPath);
|
|
279
|
-
const activitiesPath = path.join(projectPath, activitiesFolder);
|
|
280
|
-
if (!fs.existsSync(activitiesPath)) return; // already flagged elsewhere
|
|
281
|
-
|
|
282
|
-
const activityDirs = fs.readdirSync(activitiesPath, { withFileTypes: true })
|
|
283
|
-
.filter(d => d.isDirectory() && !d.name.startsWith('.'));
|
|
284
|
-
|
|
285
|
-
for (const activityDir of activityDirs) {
|
|
286
|
-
const activityRoot = path.join(activitiesPath, activityDir.name);
|
|
287
|
-
const configCandidates = [
|
|
288
|
-
path.join(activityRoot, 'shared', 'build.config.json'),
|
|
289
|
-
path.join(activityRoot, 'Shared', 'build.config.json')
|
|
290
|
-
];
|
|
291
|
-
const configPath = configCandidates.find(p => fs.existsSync(p));
|
|
292
|
-
if (!configPath) continue; // missing build.config is its own concern
|
|
293
|
-
|
|
294
|
-
const relConfig = path.relative(projectPath, configPath);
|
|
295
|
-
|
|
296
|
-
let parsed: { activityInfo?: Record<string, unknown> };
|
|
297
|
-
try {
|
|
298
|
-
parsed = JSON.parse(fs.readFileSync(configPath, 'utf8'));
|
|
299
|
-
} catch (err) {
|
|
300
|
-
issues.push({
|
|
301
|
-
id: `invalid-build-config-json:${activityDir.name}`,
|
|
302
|
-
severity: 'error',
|
|
303
|
-
message: `${relConfig} is not valid JSON`,
|
|
304
|
-
suggestion: `Open ${relConfig} and fix the JSON syntax. Error: ${(err as Error).message}`,
|
|
305
|
-
fixable: false,
|
|
306
|
-
file: relConfig
|
|
307
|
-
});
|
|
308
|
-
continue;
|
|
309
|
-
}
|
|
310
|
-
|
|
311
|
-
const activityInfo = (parsed && parsed.activityInfo) as Record<string, unknown> | undefined;
|
|
312
|
-
if (!activityInfo || typeof activityInfo !== 'object') {
|
|
313
|
-
issues.push({
|
|
314
|
-
id: `missing-activity-info:${activityDir.name}`,
|
|
315
|
-
severity: 'error',
|
|
316
|
-
message: `${relConfig} is missing activityInfo`,
|
|
317
|
-
suggestion: 'Re-run "atb new" or restore activityInfo manually',
|
|
318
|
-
fixable: false,
|
|
319
|
-
file: relConfig
|
|
320
|
-
});
|
|
321
|
-
continue;
|
|
322
|
-
}
|
|
323
|
-
|
|
324
|
-
// activityType validation
|
|
325
|
-
const activityType = activityInfo.activityType as string | undefined;
|
|
326
|
-
if (!activityType) {
|
|
327
|
-
issues.push({
|
|
328
|
-
id: `missing-activity-type:${activityDir.name}`,
|
|
329
|
-
severity: 'warning',
|
|
330
|
-
message: `${relConfig} is missing activityType`,
|
|
331
|
-
suggestion: 'Add `"activityType": "ab"` (or "xt") to activityInfo',
|
|
332
|
-
fixable: false,
|
|
333
|
-
file: relConfig
|
|
334
|
-
});
|
|
335
|
-
} else if (!['ab', 'xt'].includes(String(activityType).toLowerCase())) {
|
|
336
|
-
issues.push({
|
|
337
|
-
id: `invalid-activity-type:${activityDir.name}`,
|
|
338
|
-
severity: 'error',
|
|
339
|
-
message: `${relConfig} has invalid activityType "${activityType}"`,
|
|
340
|
-
suggestion: 'activityType must be "ab" or "xt"',
|
|
341
|
-
fixable: false,
|
|
342
|
-
file: relConfig
|
|
343
|
-
});
|
|
344
|
-
}
|
|
345
|
-
|
|
346
|
-
// id check (warning only — null is fine until first deploy)
|
|
347
|
-
if (activityInfo.id === null || activityInfo.id === undefined) {
|
|
348
|
-
issues.push({
|
|
349
|
-
id: `unset-activity-id:${activityDir.name}`,
|
|
350
|
-
severity: 'warning',
|
|
351
|
-
message: `${relConfig} has no activity id (deploy will fail until set)`,
|
|
352
|
-
suggestion: 'Set activityInfo.id once the activity exists in Adobe Target, or run "atb sync"',
|
|
353
|
-
fixable: false,
|
|
354
|
-
file: relConfig
|
|
355
|
-
});
|
|
356
|
-
}
|
|
357
|
-
|
|
358
|
-
// Variations validation
|
|
359
|
-
const variations = activityInfo.variations as Record<string, unknown> | unknown[] | undefined;
|
|
360
|
-
if (!variations || (Array.isArray(variations) && variations.length === 0) || (typeof variations === 'object' && !Array.isArray(variations) && Object.keys(variations).length === 0)) {
|
|
361
|
-
issues.push({
|
|
362
|
-
id: `missing-variations:${activityDir.name}`,
|
|
363
|
-
severity: 'error',
|
|
364
|
-
message: `${relConfig} has no variations`,
|
|
365
|
-
suggestion: 'Add at least one variation to activityInfo.variations',
|
|
366
|
-
fixable: false,
|
|
367
|
-
file: relConfig
|
|
368
|
-
});
|
|
369
|
-
continue;
|
|
370
|
-
}
|
|
371
|
-
|
|
372
|
-
// Multi-page detection: any variation value is an object with a `pages` key
|
|
373
|
-
const variationEntries = Array.isArray(variations)
|
|
374
|
-
? []
|
|
375
|
-
: Object.entries(variations);
|
|
376
|
-
const isMultiPage = variationEntries.some(([, v]) =>
|
|
377
|
-
typeof v === 'object' && v !== null && 'pages' in (v as object)
|
|
378
|
-
);
|
|
379
|
-
|
|
380
|
-
if (isMultiPage) {
|
|
381
|
-
const pages = activityInfo.pages as Record<string, unknown> | undefined;
|
|
382
|
-
if (!pages || typeof pages !== 'object' || Object.keys(pages).length === 0) {
|
|
383
|
-
issues.push({
|
|
384
|
-
id: `multi-page-missing-pages:${activityDir.name}`,
|
|
385
|
-
severity: 'error',
|
|
386
|
-
message: `${relConfig} uses multi-page variations but has no activityInfo.pages map`,
|
|
387
|
-
suggestion: 'Add `"pages": { "<pageName>": <locationLocalId>, ... }` or run "atb sync" to populate it from Adobe Target',
|
|
388
|
-
fixable: false,
|
|
389
|
-
file: relConfig
|
|
390
|
-
});
|
|
391
|
-
}
|
|
392
|
-
|
|
393
|
-
// Each multi-page variation: pages.<page> should map to a folder that exists
|
|
394
|
-
for (const [variationKey, variationValue] of variationEntries) {
|
|
395
|
-
if (typeof variationValue !== 'object' || variationValue === null || !('pages' in (variationValue as object))) continue;
|
|
396
|
-
const pagesMap = (variationValue as { pages?: Record<string, unknown> }).pages || {};
|
|
397
|
-
for (const [pageName, subPath] of Object.entries(pagesMap)) {
|
|
398
|
-
if (typeof subPath !== 'string') continue;
|
|
399
|
-
const folderAbs = path.join(activityRoot, subPath);
|
|
400
|
-
if (!fs.existsSync(folderAbs)) {
|
|
401
|
-
issues.push({
|
|
402
|
-
id: `missing-page-folder:${activityDir.name}:${variationKey}:${pageName}`,
|
|
403
|
-
severity: 'warning',
|
|
404
|
-
message: `${relConfig}: variation "${variationKey}" → page "${pageName}" expects folder "${subPath}" but it doesn't exist`,
|
|
405
|
-
suggestion: 'Run "atb sync --scaffold" to auto-create missing variation folders, or create them manually',
|
|
406
|
-
fixable: false,
|
|
407
|
-
file: relConfig
|
|
408
|
-
});
|
|
409
|
-
}
|
|
410
|
-
}
|
|
411
|
-
}
|
|
412
|
-
} else {
|
|
413
|
-
// Single-page: each variation key should map to a folder under the activity root
|
|
414
|
-
for (const [variationKey] of variationEntries) {
|
|
415
|
-
const folderAbs = path.join(activityRoot, variationKey);
|
|
416
|
-
if (!fs.existsSync(folderAbs)) {
|
|
417
|
-
issues.push({
|
|
418
|
-
id: `missing-variation-folder:${activityDir.name}:${variationKey}`,
|
|
419
|
-
severity: 'warning',
|
|
420
|
-
message: `${relConfig}: variation "${variationKey}" has no folder at ${path.relative(projectPath, folderAbs)}`,
|
|
421
|
-
suggestion: 'Create the folder, run "atb new" to scaffold variations, or remove the entry from build.config.json',
|
|
422
|
-
fixable: false,
|
|
423
|
-
file: relConfig
|
|
424
|
-
});
|
|
425
|
-
}
|
|
426
|
-
}
|
|
427
|
-
}
|
|
428
|
-
}
|
|
429
|
-
};
|
|
430
|
-
|
|
431
|
-
/**
|
|
432
|
-
* Check environment variables content
|
|
433
|
-
*/
|
|
434
|
-
const checkEnvVariables = async (projectPath: string, issues: DiagnosticIssue[]): Promise<void> => {
|
|
435
|
-
const envPath = path.join(projectPath, '.env');
|
|
436
|
-
if (!fs.existsSync(envPath)) return;
|
|
437
|
-
|
|
438
|
-
const envContent = fs.readFileSync(envPath, 'utf8');
|
|
439
|
-
|
|
440
|
-
// Check if ACTIVITY_FOLDER_NAME is empty
|
|
441
|
-
if (envContent.includes('ACTIVITY_FOLDER_NAME=""') || envContent.includes('ACTIVITY_FOLDER_NAME=\'\'')) {
|
|
442
|
-
issues.push({
|
|
443
|
-
id: 'empty-activity-folder-name',
|
|
444
|
-
severity: 'warning',
|
|
445
|
-
message: 'ACTIVITY_FOLDER_NAME is empty',
|
|
446
|
-
suggestion: 'Set ACTIVITY_FOLDER_NAME to your activity folder name',
|
|
447
|
-
fixable: false
|
|
448
|
-
});
|
|
449
|
-
}
|
|
450
|
-
|
|
451
|
-
// Check if Adobe credentials are empty
|
|
452
|
-
if (envContent.includes('ADOBE_CLIENT_ID=""') || envContent.includes('ADOBE_CLIENT_ID=\'\'')) {
|
|
453
|
-
issues.push({
|
|
454
|
-
id: 'empty-adobe-client-id',
|
|
455
|
-
severity: 'warning',
|
|
456
|
-
message: 'ADOBE_CLIENT_ID is empty',
|
|
457
|
-
suggestion: 'Set your Adobe Target API client ID',
|
|
458
|
-
fixable: false
|
|
459
|
-
});
|
|
460
|
-
}
|
|
461
|
-
|
|
462
|
-
if (envContent.includes('ADOBE_CLIENT_SECRET=""') || envContent.includes('ADOBE_CLIENT_SECRET=\'\'')) {
|
|
463
|
-
issues.push({
|
|
464
|
-
id: 'empty-adobe-client-secret',
|
|
465
|
-
severity: 'warning',
|
|
466
|
-
message: 'ADOBE_CLIENT_SECRET is empty',
|
|
467
|
-
suggestion: 'Set your Adobe Target API client secret',
|
|
468
|
-
fixable: false
|
|
469
|
-
});
|
|
470
|
-
}
|
|
471
|
-
|
|
472
|
-
// ADOBE_TENANT — if missing or empty, BASE_URL in adobe.config.js will fall
|
|
473
|
-
// back to the literal "YOUR_TENANT" placeholder and every API call will 404.
|
|
474
|
-
const tenantMatch = envContent.match(/^ADOBE_TENANT=(.*)$/m);
|
|
475
|
-
const tenantValue = tenantMatch ? tenantMatch[1].trim().replace(/^["']|["']$/g, '') : '';
|
|
476
|
-
if (!tenantValue) {
|
|
477
|
-
issues.push({
|
|
478
|
-
id: 'empty-adobe-tenant',
|
|
479
|
-
severity: 'warning',
|
|
480
|
-
message: 'ADOBE_TENANT is missing or empty',
|
|
481
|
-
suggestion: 'Set ADOBE_TENANT to your AT tenant slug (the segment after mc.adobe.io/ in the AT URL)',
|
|
482
|
-
fixable: false
|
|
483
|
-
});
|
|
484
|
-
}
|
|
485
|
-
};
|
|
486
|
-
|
|
487
|
-
/**
|
|
488
|
-
* Check dependencies
|
|
489
|
-
*/
|
|
490
|
-
const checkDependencies = async (projectPath: string, issues: DiagnosticIssue[]): Promise<void> => {
|
|
491
|
-
const packagePath = path.join(projectPath, 'package.json');
|
|
492
|
-
if (!fs.existsSync(packagePath)) return;
|
|
493
|
-
|
|
494
|
-
const nodeModulesPath = path.join(projectPath, 'node_modules');
|
|
495
|
-
|
|
496
|
-
if (!fs.existsSync(nodeModulesPath)) {
|
|
497
|
-
issues.push({
|
|
498
|
-
id: 'missing-node-modules',
|
|
499
|
-
severity: 'warning',
|
|
500
|
-
message: 'node_modules folder is missing',
|
|
501
|
-
suggestion: 'Run "npm install" to install dependencies',
|
|
502
|
-
fixable: false
|
|
503
|
-
});
|
|
504
|
-
}
|
|
505
|
-
};
|
|
506
|
-
|
|
507
|
-
/**
|
|
508
|
-
* Validate that the at-builder global install is healthy:
|
|
509
|
-
* - global npm bin is on PATH
|
|
510
|
-
* - the `atb` shim exists in the global bin
|
|
511
|
-
* - on POSIX, the shim and the package's bin/index.js are executable
|
|
512
|
-
* (auto-chmods 755 if missing the bit)
|
|
513
|
-
*
|
|
514
|
-
* Same checks as lib/postinstall.js, just surfaced through `atb doctor` for
|
|
515
|
-
* users who installed with --ignore-scripts or dismissed the postinstall
|
|
516
|
-
* warning. Issues are flagged but not auto-fixable here — the suggested
|
|
517
|
-
* commands need to run in the user's shell, not in this Node process.
|
|
518
|
-
*/
|
|
519
|
-
const checkInstallEnv = async (issues: DiagnosticIssue[]): Promise<void> => {
|
|
520
|
-
let result;
|
|
521
|
-
try {
|
|
522
|
-
result = installChecks.checkInstallEnvironment();
|
|
523
|
-
} catch (err) {
|
|
524
|
-
// Don't let a bug in install-checks block the rest of doctor.
|
|
525
|
-
logger.error("checkInstallEnv", `Skipped: ${(err as Error).message}`);
|
|
526
|
-
return;
|
|
527
|
-
}
|
|
528
|
-
|
|
529
|
-
for (const problem of result.problems) {
|
|
530
|
-
issues.push({
|
|
531
|
-
id: 'install-env',
|
|
532
|
-
severity: 'warning',
|
|
533
|
-
message: problem,
|
|
534
|
-
suggestion: result.isWin
|
|
535
|
-
? `On Windows: ensure ${result.globalBin || 'your npm global bin (npm config get prefix)'} is on PATH, then reinstall: npm i -g at-builder`
|
|
536
|
-
: `On macOS/Linux: chmod +x "${result.shimPath || 'atb shim'}" and ensure ${result.globalBin || '$(npm config get prefix)/bin'} is on PATH; then run: hash -r`,
|
|
537
|
-
fixable: false
|
|
538
|
-
});
|
|
539
|
-
}
|
|
540
|
-
};
|
|
541
|
-
|
|
542
|
-
// Fix functions
|
|
543
|
-
|
|
544
|
-
const createEnvFile = async (projectPath: string): Promise<boolean> => {
|
|
545
|
-
const envPath = path.join(projectPath, '.env');
|
|
546
|
-
const envContent = `ACTIVITIES_BASE_FOLDER="Activities"
|
|
547
|
-
ACTIVITY_FOLDER_NAME=""
|
|
548
|
-
PUPPETEER_LANDING_PAGE=""
|
|
549
|
-
TARGET_URL=""
|
|
550
|
-
LOGIN_URL=""
|
|
551
|
-
|
|
552
|
-
# Dev-server selection (used by \`atb dev --browser\`).
|
|
553
|
-
# Edit and save while puppeteer is running to hot-swap the previewed bundle.
|
|
554
|
-
# PAGE is only meaningful for multi-page activities — leave empty otherwise.
|
|
555
|
-
VARIATION="Variation-1"
|
|
556
|
-
PAGE=""
|
|
557
|
-
|
|
558
|
-
NODE_ENV="development"
|
|
559
|
-
VERBOSE=false
|
|
560
|
-
|
|
561
|
-
# Build wrapper config.
|
|
562
|
-
# TARGET_BUILD_PREFIX customizes the window flag baked into each build —
|
|
563
|
-
# rendered as window.\${TARGET_BUILD_PREFIX}_\${contentHash}_\${hash}. Defaults
|
|
564
|
-
# to "TargetBuild" when empty. Useful when multiple at-builder activities
|
|
565
|
-
# end up loaded on the same page and you want each project's flag namespaced.
|
|
566
|
-
TARGET_BUILD_PREFIX=""
|
|
567
|
-
|
|
568
|
-
# Adobe Target Deployment Configuration
|
|
569
|
-
# ADOBE_TENANT is your AT tenant slug — find it in the AT URL after "mc.adobe.io/".
|
|
570
|
-
ADOBE_TENANT=""
|
|
571
|
-
ADOBE_CLIENT_ID=""
|
|
572
|
-
ADOBE_CLIENT_SECRET=""`;
|
|
573
|
-
|
|
574
|
-
try {
|
|
575
|
-
fs.writeFileSync(envPath, envContent, 'utf8');
|
|
576
|
-
return true;
|
|
577
|
-
} catch (error) {
|
|
578
|
-
return false;
|
|
579
|
-
}
|
|
580
|
-
};
|
|
581
|
-
|
|
582
|
-
const createAdobeConfig = async (projectPath: string): Promise<boolean> => {
|
|
583
|
-
const configPath = path.join(projectPath, 'adobe.config.js');
|
|
584
|
-
const configContent = `/**
|
|
585
|
-
* Adobe Target API Configuration
|
|
586
|
-
*
|
|
587
|
-
* Used by at-sync.js and at-deploy.js. BASE_URL is the activities root —
|
|
588
|
-
* callers append \`\${activityType}/\${activityId}\` (e.g. ab/12345, xt/67890).
|
|
589
|
-
*
|
|
590
|
-
* ADOBE_TENANT comes from the consumer .env. Both at-sync and at-deploy load
|
|
591
|
-
* dotenv before requiring this file, so process.env is populated by the time
|
|
592
|
-
* BASE_URL is built.
|
|
593
|
-
*/
|
|
594
|
-
|
|
595
|
-
const TENANT = process.env.ADOBE_TENANT || 'YOUR_TENANT';
|
|
596
|
-
|
|
597
|
-
module.exports = {
|
|
598
|
-
BASE_URL: \`https://mc.adobe.io/\${TENANT}/target/activities/\`,
|
|
599
|
-
IMS_TOKEN_URL: 'https://ims-na1.adobelogin.com/ims/token/v3',
|
|
600
|
-
IMS_SCOPE: 'openid,AdobeID,target_sdk,additional_info.projectedProductContext'
|
|
601
|
-
};`;
|
|
602
|
-
|
|
603
|
-
try {
|
|
604
|
-
fs.writeFileSync(configPath, configContent, 'utf8');
|
|
605
|
-
return true;
|
|
606
|
-
} catch (error) {
|
|
607
|
-
return false;
|
|
608
|
-
}
|
|
609
|
-
};
|
|
610
|
-
|
|
611
|
-
/**
|
|
612
|
-
* Migrate VARIATION/PAGE from a legacy watch-config.json into .env, then
|
|
613
|
-
* delete the watch-config.json. Idempotent: existing .env entries are
|
|
614
|
-
* overwritten so the migrated values win (matching puppeteer's runtime
|
|
615
|
-
* precedence — watch-config.json was the source of truth).
|
|
616
|
-
*/
|
|
617
|
-
const migrateWatchConfig = async (projectPath: string): Promise<boolean> => {
|
|
618
|
-
const configPath = path.join(projectPath, 'watch-config.json');
|
|
619
|
-
const envPath = path.join(projectPath, '.env');
|
|
620
|
-
if (!fs.existsSync(configPath)) return true; // nothing to migrate
|
|
621
|
-
|
|
622
|
-
let watchCfg: { VARIATION?: string; PAGE?: string };
|
|
623
|
-
try {
|
|
624
|
-
watchCfg = JSON.parse(fs.readFileSync(configPath, 'utf8'));
|
|
625
|
-
} catch (err) {
|
|
626
|
-
console.error(`Failed to parse watch-config.json: ${(err as Error).message}`);
|
|
627
|
-
return false;
|
|
628
|
-
}
|
|
629
|
-
|
|
630
|
-
const variation = (watchCfg.VARIATION || '').trim();
|
|
631
|
-
const page = (watchCfg.PAGE || '').trim();
|
|
632
|
-
|
|
633
|
-
if (!fs.existsSync(envPath)) {
|
|
634
|
-
// No .env yet — synthesize a minimal one with just the migrated keys.
|
|
635
|
-
// The user can add the rest via `atb init` or by editing manually.
|
|
636
|
-
fs.writeFileSync(envPath, `VARIATION="${variation}"\nPAGE="${page}"\n`, 'utf8');
|
|
637
|
-
} else {
|
|
638
|
-
let envContent = fs.readFileSync(envPath, 'utf8');
|
|
639
|
-
envContent = upsertEnvLine(envContent, 'VARIATION', variation);
|
|
640
|
-
envContent = upsertEnvLine(envContent, 'PAGE', page);
|
|
641
|
-
fs.writeFileSync(envPath, envContent, 'utf8');
|
|
642
|
-
}
|
|
643
|
-
|
|
644
|
-
try {
|
|
645
|
-
fs.unlinkSync(configPath);
|
|
646
|
-
} catch (err) {
|
|
647
|
-
console.error(`Migrated values into .env, but failed to delete watch-config.json: ${(err as Error).message}`);
|
|
648
|
-
return false;
|
|
649
|
-
}
|
|
650
|
-
|
|
651
|
-
console.log(`✓ Migrated VARIATION="${variation}" PAGE="${page}" from watch-config.json into .env`);
|
|
652
|
-
return true;
|
|
653
|
-
};
|
|
654
|
-
|
|
655
|
-
/**
|
|
656
|
-
* Insert or replace a `KEY="value"` line in a .env file body. Preserves
|
|
657
|
-
* surrounding lines; appends if the key wasn't present.
|
|
658
|
-
*/
|
|
659
|
-
const upsertEnvLine = (envContent: string, key: string, value: string): string => {
|
|
660
|
-
const line = `${key}="${value}"`;
|
|
661
|
-
const re = new RegExp(`^${key}=.*$`, 'm');
|
|
662
|
-
if (re.test(envContent)) {
|
|
663
|
-
return envContent.replace(re, line);
|
|
664
|
-
}
|
|
665
|
-
if (!envContent.endsWith('\n')) envContent += '\n';
|
|
666
|
-
return envContent + line + '\n';
|
|
667
|
-
};
|
|
668
|
-
|
|
669
|
-
const createGitignore = async (projectPath: string): Promise<boolean> => {
|
|
670
|
-
const gitignorePath = path.join(projectPath, '.gitignore');
|
|
671
|
-
|
|
672
|
-
try {
|
|
673
|
-
fs.writeFileSync(gitignorePath, GITIGNORE_TEMPLATE, 'utf8');
|
|
674
|
-
return true;
|
|
675
|
-
} catch (error) {
|
|
676
|
-
return false;
|
|
677
|
-
}
|
|
678
|
-
};
|
|
679
|
-
|
|
680
|
-
const createActivitiesFolder = async (projectPath: string): Promise<boolean> => {
|
|
681
|
-
// Try to read ACTIVITIES_BASE_FOLDER from .env, fallback to default
|
|
682
|
-
let activitiesFolder = 'Activities';
|
|
683
|
-
const envPath = path.join(projectPath, '.env');
|
|
684
|
-
|
|
685
|
-
if (fs.existsSync(envPath)) {
|
|
686
|
-
const envContent = fs.readFileSync(envPath, 'utf8');
|
|
687
|
-
const match = envContent.match(/ACTIVITIES_BASE_FOLDER=["']?([^"'\n\r]+)["']?/);
|
|
688
|
-
if (match) {
|
|
689
|
-
activitiesFolder = match[1];
|
|
690
|
-
}
|
|
691
|
-
}
|
|
692
|
-
|
|
693
|
-
const activitiesPath = path.join(projectPath, activitiesFolder);
|
|
694
|
-
|
|
695
|
-
try {
|
|
696
|
-
fs.mkdirSync(activitiesPath, { recursive: true });
|
|
697
|
-
return true;
|
|
698
|
-
} catch (error) {
|
|
699
|
-
return false;
|
|
700
|
-
}
|
|
701
|
-
};
|
|
702
|
-
|
|
703
|
-
const createPackageJson = async (projectPath: string): Promise<boolean> => {
|
|
704
|
-
const packagePath = path.join(projectPath, 'package.json');
|
|
705
|
-
const packageContent = {
|
|
706
|
-
"name": path.basename(projectPath),
|
|
707
|
-
"version": "1.0.0",
|
|
708
|
-
"description": "",
|
|
709
|
-
"main": "index.js",
|
|
710
|
-
"scripts": {
|
|
711
|
-
"test": "echo \"Error: no test specified\" && exit 1"
|
|
712
|
-
},
|
|
713
|
-
"keywords": [],
|
|
714
|
-
"author": "",
|
|
715
|
-
"license": "ISC"
|
|
716
|
-
};
|
|
717
|
-
|
|
718
|
-
try {
|
|
719
|
-
fs.writeFileSync(packagePath, JSON.stringify(packageContent, null, 2), 'utf8');
|
|
720
|
-
return true;
|
|
721
|
-
} catch (error) {
|
|
722
|
-
return false;
|
|
723
|
-
}
|
|
724
|
-
};
|