andrud 1.0.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/CHANGELOG.md +70 -0
- package/CODE_REVIEW_ANALYSIS.md +177 -0
- package/CONTRIBUTING.md +132 -0
- package/FIXES_IMPLEMENTED.md +546 -0
- package/LICENSE +21 -0
- package/README.md +310 -0
- package/bin/andrud.js +24 -0
- package/package.json +80 -0
- package/src/__tests__/context.test.ts +133 -0
- package/src/__tests__/generator.test.ts +107 -0
- package/src/__tests__/validation.test.ts +105 -0
- package/src/cli/commands/create.ts +252 -0
- package/src/cli/commands/info.ts +178 -0
- package/src/cli/commands/init.ts +186 -0
- package/src/cli/commands/list.ts +156 -0
- package/src/cli/commands/new.ts +316 -0
- package/src/cli/index.ts +116 -0
- package/src/core/config.ts +172 -0
- package/src/core/context.ts +212 -0
- package/src/core/generator.ts +1350 -0
- package/src/core/types.ts +184 -0
- package/src/templates/index.ts +162 -0
- package/src/types/gradient-string.d.ts +25 -0
- package/src/ui/colors.ts +139 -0
- package/src/ui/output.ts +230 -0
- package/src/ui/prompts.ts +170 -0
- package/src/ui/spinners.ts +95 -0
- package/src/ui/types.ts +41 -0
- package/src/utils/filesystem.ts +222 -0
- package/src/utils/logger.ts +67 -0
- package/src/utils/object.ts +456 -0
- package/src/utils/validation.ts +345 -0
- package/tsconfig.json +25 -0
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Test suite for validation utilities
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { test } from 'node:test';
|
|
6
|
+
import assert from 'node:assert';
|
|
7
|
+
import {
|
|
8
|
+
validateAppName,
|
|
9
|
+
validatePackageNameInput,
|
|
10
|
+
validateDirectoryPath,
|
|
11
|
+
camelCase,
|
|
12
|
+
pascalCase,
|
|
13
|
+
kebabCase,
|
|
14
|
+
snakeCase
|
|
15
|
+
} from '../utils/validation.js';
|
|
16
|
+
|
|
17
|
+
test('Validation - App Name', async (t) => {
|
|
18
|
+
await t.test('should accept valid app name', () => {
|
|
19
|
+
const result = validateAppName('MyAwesomeApp');
|
|
20
|
+
assert.strictEqual(result.valid, true);
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
await t.test('should reject app name starting with number', () => {
|
|
24
|
+
const result = validateAppName('1App');
|
|
25
|
+
assert.strictEqual(result.valid, false);
|
|
26
|
+
assert.ok(result.errors.length > 0);
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
await t.test('should reject app name with spaces', () => {
|
|
30
|
+
const result = validateAppName('My App');
|
|
31
|
+
assert.strictEqual(result.valid, false);
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
await t.test('should reject empty app name', () => {
|
|
35
|
+
const result = validateAppName('');
|
|
36
|
+
assert.strictEqual(result.valid, false);
|
|
37
|
+
});
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
test('Validation - Package Name', async (t) => {
|
|
41
|
+
await t.test('should accept valid package name', () => {
|
|
42
|
+
const result = validatePackageNameInput('com.example.myapp');
|
|
43
|
+
assert.strictEqual(result.valid, true);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
await t.test('should reject reserved prefix android', () => {
|
|
47
|
+
const result = validatePackageNameInput('android.example.app');
|
|
48
|
+
assert.strictEqual(result.valid, false);
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
await t.test('should reject reserved prefix kotlin', () => {
|
|
52
|
+
const result = validatePackageNameInput('kotlin.example.app');
|
|
53
|
+
assert.strictEqual(result.valid, false);
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
await t.test('should reject package with single segment', () => {
|
|
57
|
+
const result = validatePackageNameInput('myapp');
|
|
58
|
+
assert.strictEqual(result.valid, false);
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
await t.test('should warn on generic com.example prefix', () => {
|
|
62
|
+
const result = validatePackageNameInput('com.example.app');
|
|
63
|
+
assert.strictEqual(result.valid, true);
|
|
64
|
+
assert.ok(result.warnings && result.warnings.length > 0);
|
|
65
|
+
});
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
test('Validation - Directory Path', async (t) => {
|
|
69
|
+
await t.test('should accept valid directory path', () => {
|
|
70
|
+
const result = validateDirectoryPath('./my-project');
|
|
71
|
+
assert.strictEqual(result.valid, true);
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
await t.test('should reject empty directory path', () => {
|
|
75
|
+
const result = validateDirectoryPath('');
|
|
76
|
+
assert.strictEqual(result.valid, false);
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
await t.test('should reject directory traversal', () => {
|
|
80
|
+
const result = validateDirectoryPath('../../../etc/passwd');
|
|
81
|
+
assert.strictEqual(result.valid, false);
|
|
82
|
+
});
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
test('String Case Transformations', async (t) => {
|
|
86
|
+
await t.test('camelCase should transform correctly', () => {
|
|
87
|
+
const result = camelCase('my awesome app');
|
|
88
|
+
assert.strictEqual(result, 'myAwesomeApp');
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
await t.test('pascalCase should transform correctly', () => {
|
|
92
|
+
const result = pascalCase('my awesome app');
|
|
93
|
+
assert.strictEqual(result, 'MyAwesomeApp');
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
await t.test('kebabCase should transform correctly', () => {
|
|
97
|
+
const result = kebabCase('my awesome app');
|
|
98
|
+
assert.strictEqual(result, 'my-awesome-app');
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
await t.test('snakeCase should transform correctly', () => {
|
|
102
|
+
const result = snakeCase('my awesome app');
|
|
103
|
+
assert.strictEqual(result, 'my_awesome_app');
|
|
104
|
+
});
|
|
105
|
+
});
|
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Clean, minimal Vite-style project creation command
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import {
|
|
6
|
+
text,
|
|
7
|
+
select,
|
|
8
|
+
confirm,
|
|
9
|
+
isCancel,
|
|
10
|
+
cancel,
|
|
11
|
+
spinner
|
|
12
|
+
} from '@clack/prompts';
|
|
13
|
+
import { generateProject } from '../../core/generator.js';
|
|
14
|
+
import { buildDefaultProjectContext, buildTemplateContext } from '../../core/context.js';
|
|
15
|
+
import { exists, getAbsolutePath } from '../../utils/filesystem.js';
|
|
16
|
+
import { getAllTemplates, getTemplateMetadata } from '../../templates/index.js';
|
|
17
|
+
import type { TemplateType } from '../../core/types.js';
|
|
18
|
+
import pc from 'picocolors';
|
|
19
|
+
import { resolve, isAbsolute } from 'path';
|
|
20
|
+
import { platform } from 'os';
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Normalize path for cross-platform compatibility
|
|
24
|
+
*/
|
|
25
|
+
function normalizePath(path: string): string {
|
|
26
|
+
if (!path || path.trim() === '') {
|
|
27
|
+
return path;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const trimmed = path.trim();
|
|
31
|
+
|
|
32
|
+
// On Windows, convert Unix-style paths like /d/... to D:\...
|
|
33
|
+
if (platform() === 'win32') {
|
|
34
|
+
const unixPathMatch = trimmed.match(/^\/([a-zA-Z])\/(.*)$/);
|
|
35
|
+
if (unixPathMatch && unixPathMatch[1] && unixPathMatch[2]) {
|
|
36
|
+
return `${unixPathMatch[1].toUpperCase()}:\\${unixPathMatch[2].replace(/\//g, '\\')}`;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Force relative paths to be relative to current directory
|
|
41
|
+
if (!isAbsolute(trimmed)) {
|
|
42
|
+
return resolve(trimmed);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
return trimmed;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Create a new Android project with clean minimal UI
|
|
50
|
+
*/
|
|
51
|
+
export async function createCommand(options: { force?: boolean } = {}): Promise<void> {
|
|
52
|
+
// ============================================
|
|
53
|
+
// Step 1: App Name
|
|
54
|
+
// ============================================
|
|
55
|
+
console.log('');
|
|
56
|
+
|
|
57
|
+
let appName: string;
|
|
58
|
+
const nameResult = await text({
|
|
59
|
+
message: ' ? Project name:',
|
|
60
|
+
placeholder: 'MyApp',
|
|
61
|
+
defaultValue: 'MyApp',
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
if (isCancel(nameResult)) {
|
|
65
|
+
cancel();
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
appName = nameResult.trim();
|
|
70
|
+
if (!appName || !/^[a-zA-Z]/.test(appName)) {
|
|
71
|
+
console.log(pc.red('\n ✘ Project name must start with a letter\n'));
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// ============================================
|
|
76
|
+
// Step 2: Template Selection
|
|
77
|
+
// ============================================
|
|
78
|
+
const templates = getAllTemplates();
|
|
79
|
+
|
|
80
|
+
const templateOptions = templates.map(t => ({
|
|
81
|
+
label: t.name,
|
|
82
|
+
value: t.id,
|
|
83
|
+
hint: t.language === 'kotlin' ? 'Kotlin' : 'Java'
|
|
84
|
+
}));
|
|
85
|
+
|
|
86
|
+
const templateResult = await select({
|
|
87
|
+
message: ' ? Select template:',
|
|
88
|
+
options: templateOptions
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
if (isCancel(templateResult)) {
|
|
92
|
+
cancel();
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const selectedTemplate = templateResult as TemplateType;
|
|
97
|
+
const selectedMeta = getTemplateMetadata(selectedTemplate);
|
|
98
|
+
|
|
99
|
+
// ============================================
|
|
100
|
+
// Step 3: Package Name
|
|
101
|
+
// ============================================
|
|
102
|
+
const defaultPackage = `com.example.${appName.toLowerCase().replace(/[^a-z0-9]/g, '')}`;
|
|
103
|
+
|
|
104
|
+
const packageResult = await text({
|
|
105
|
+
message: ' ? Package name:',
|
|
106
|
+
placeholder: defaultPackage,
|
|
107
|
+
defaultValue: defaultPackage,
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
if (isCancel(packageResult)) {
|
|
111
|
+
cancel();
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const packageName = packageResult.trim().toLowerCase();
|
|
116
|
+
if (!packageName || !/^[a-z][a-z0-9_]*(\.[a-z][a-z0-9_]*)+$/.test(packageName)) {
|
|
117
|
+
console.log(pc.red('\n ✘ Invalid package name format (e.g., com.example.myapp)\n'));
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// ============================================
|
|
122
|
+
// Step 4: Directory
|
|
123
|
+
// ============================================
|
|
124
|
+
const dirResult = await text({
|
|
125
|
+
message: ' ? Where to create the project:',
|
|
126
|
+
placeholder: 'D:\\Projects\\Android',
|
|
127
|
+
defaultValue: 'D:\\Projects\\Android',
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
if (isCancel(dirResult)) {
|
|
131
|
+
cancel();
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// Normalize and append app name as project folder
|
|
136
|
+
let baseDir = dirResult.trim();
|
|
137
|
+
|
|
138
|
+
if (!baseDir) {
|
|
139
|
+
console.log(pc.red('\n ✘ Directory path cannot be empty\n'));
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
baseDir = normalizePath(baseDir);
|
|
144
|
+
|
|
145
|
+
// Ensure the directory path ends properly (no trailing slash issues)
|
|
146
|
+
let projectDirectory: string;
|
|
147
|
+
|
|
148
|
+
// If the path is ./something, append app name
|
|
149
|
+
if (baseDir.startsWith('./') || baseDir === '.') {
|
|
150
|
+
const baseName = baseDir === '.' ? '' : baseDir.slice(1).replace(/^[\\\/]/, '');
|
|
151
|
+
projectDirectory = baseName
|
|
152
|
+
? `${baseName}/${appName.toLowerCase().replace(/[^a-z0-9]/g, '-')}`
|
|
153
|
+
: `./${appName.toLowerCase().replace(/[^a-z0-9]/g, '-')}`;
|
|
154
|
+
} else {
|
|
155
|
+
// For absolute paths, append app name + sanitize
|
|
156
|
+
projectDirectory = `${baseDir}\\${appName.toLowerCase().replace(/[^a-z0-9]/g, '-')}`;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// Check if directory exists
|
|
160
|
+
const dirExists = await exists(projectDirectory);
|
|
161
|
+
if (dirExists) {
|
|
162
|
+
if (!options.force) {
|
|
163
|
+
console.log(pc.red(`\n ✘ Directory already exists: ${projectDirectory}`));
|
|
164
|
+
console.log(pc.gray(' Use --force to overwrite\n'));
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// ============================================
|
|
170
|
+
// Step 5: Summary & Confirm
|
|
171
|
+
// ============================================
|
|
172
|
+
console.log('');
|
|
173
|
+
console.log(pc.gray('─').repeat(50));
|
|
174
|
+
console.log('');
|
|
175
|
+
console.log(` ${pc.cyan('Project:')} ${pc.bold(appName)}`);
|
|
176
|
+
console.log(` ${pc.cyan('Template:')} ${selectedMeta?.name}`);
|
|
177
|
+
console.log(` ${pc.cyan('Package:')} ${packageName}`);
|
|
178
|
+
console.log(` ${pc.cyan('Path:')} ${projectDirectory}`);
|
|
179
|
+
console.log('');
|
|
180
|
+
|
|
181
|
+
const shouldCreate = await confirm({
|
|
182
|
+
message: ' Create project?',
|
|
183
|
+
initialValue: true
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
if (isCancel(shouldCreate)) {
|
|
187
|
+
cancel();
|
|
188
|
+
return;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
if (!shouldCreate) {
|
|
192
|
+
console.log(pc.gray('\n Cancelled.\n'));
|
|
193
|
+
return;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// ============================================
|
|
197
|
+
// Generate Project
|
|
198
|
+
// ============================================
|
|
199
|
+
console.log('');
|
|
200
|
+
|
|
201
|
+
const s = spinner();
|
|
202
|
+
s.start(' Creating project...');
|
|
203
|
+
|
|
204
|
+
// Build context
|
|
205
|
+
const baseContext = buildDefaultProjectContext(
|
|
206
|
+
appName,
|
|
207
|
+
packageName,
|
|
208
|
+
projectDirectory,
|
|
209
|
+
selectedTemplate,
|
|
210
|
+
{ git: true, readme: true, androidX: true, kotlinDsl: true }
|
|
211
|
+
);
|
|
212
|
+
|
|
213
|
+
const context = buildTemplateContext({
|
|
214
|
+
appName: baseContext.appName,
|
|
215
|
+
packageName: baseContext.packageName,
|
|
216
|
+
projectDirectory: baseContext.projectDirectory,
|
|
217
|
+
template: baseContext.template,
|
|
218
|
+
uiFramework: baseContext.uiFramework,
|
|
219
|
+
language: baseContext.language,
|
|
220
|
+
android: baseContext.android,
|
|
221
|
+
gradle: baseContext.gradle,
|
|
222
|
+
features: baseContext as unknown as Record<string, boolean>,
|
|
223
|
+
nativeCpp: baseContext.nativeCpp
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
// Generate the project
|
|
227
|
+
const result = await generateProject(context, {
|
|
228
|
+
overwrite: options.force ?? false,
|
|
229
|
+
dryRun: false,
|
|
230
|
+
skipInstall: false,
|
|
231
|
+
verbose: false
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
s.stop(' Done!');
|
|
235
|
+
|
|
236
|
+
if (result.success) {
|
|
237
|
+
console.log('');
|
|
238
|
+
console.log(pc.green(` ✓ Project "${appName}" created`));
|
|
239
|
+
console.log(pc.gray('─'.repeat(50)));
|
|
240
|
+
console.log('');
|
|
241
|
+
console.log(pc.gray(` cd ${projectDirectory}`));
|
|
242
|
+
console.log(pc.gray(' ./gradlew assembleDebug'));
|
|
243
|
+
console.log(pc.gray(' studio .'));
|
|
244
|
+
console.log('');
|
|
245
|
+
} else {
|
|
246
|
+
console.log(pc.red('\n ✘ Failed to create project'));
|
|
247
|
+
result.errors.forEach(err => {
|
|
248
|
+
console.log(` ${pc.red('•')} ${err.file ? `${err.file}: ` : ''}${err.message}`);
|
|
249
|
+
});
|
|
250
|
+
console.log('');
|
|
251
|
+
}
|
|
252
|
+
}
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Template info command - shows detailed information about templates
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { printSection, printKeyValue } from '../../ui/output.js';
|
|
6
|
+
import { gradientTeen, bold, primary, muted } from '../../ui/colors.js';
|
|
7
|
+
import { getTemplateMetadata, getAllTemplates, getTemplatePreview } from '../../templates/index.js';
|
|
8
|
+
import { GRADLE_VERSIONS, TEMPLATE_CONFIGS } from '../../core/config.js';
|
|
9
|
+
import pc from 'picocolors';
|
|
10
|
+
import type { TemplateType, TemplateMetadata } from '../../core/types.js';
|
|
11
|
+
|
|
12
|
+
interface InfoCommandOptions {
|
|
13
|
+
template?: string;
|
|
14
|
+
json: boolean;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Show detailed info about a template
|
|
19
|
+
*/
|
|
20
|
+
export async function createInfoCommand(
|
|
21
|
+
template?: string,
|
|
22
|
+
options: InfoCommandOptions = { json: false }
|
|
23
|
+
): Promise<void> {
|
|
24
|
+
if (options.json) {
|
|
25
|
+
if (template) {
|
|
26
|
+
const meta = getTemplateMetadata(template as TemplateType);
|
|
27
|
+
if (!meta) {
|
|
28
|
+
console.log(JSON.stringify({ error: `Template "${template}" not found` }, null, 2));
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
printInfoJson(meta);
|
|
32
|
+
} else {
|
|
33
|
+
// Show info for all templates
|
|
34
|
+
const templates = getAllTemplates();
|
|
35
|
+
console.log(JSON.stringify(templates, null, 2));
|
|
36
|
+
}
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
if (template) {
|
|
41
|
+
await showTemplateInfo(template as TemplateType);
|
|
42
|
+
} else {
|
|
43
|
+
// Show brief info for all templates
|
|
44
|
+
console.log('');
|
|
45
|
+
console.log(gradientTeen('Available Templates'));
|
|
46
|
+
console.log('');
|
|
47
|
+
|
|
48
|
+
const templates = getAllTemplates();
|
|
49
|
+
const maxNameLength = Math.max(...templates.map(t => t.name.length));
|
|
50
|
+
const maxIdLength = Math.max(...templates.map(t => t.id.length));
|
|
51
|
+
|
|
52
|
+
templates.forEach((t, index) => {
|
|
53
|
+
const paddedName = t.name.padEnd(maxNameLength);
|
|
54
|
+
const paddedId = t.id.padEnd(maxIdLength);
|
|
55
|
+
|
|
56
|
+
console.log(` ${primary(`${index + 1}.`)} ${bold(paddedName)} ${muted(`(${paddedId})`)}`);
|
|
57
|
+
console.log(` ${t.description}`);
|
|
58
|
+
console.log('');
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
console.log('Usage: andrud info <template-name>');
|
|
62
|
+
console.log('Example: andrud info kotlin-compose');
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Show detailed info for a specific template
|
|
68
|
+
*/
|
|
69
|
+
async function showTemplateInfo(templateId: TemplateType): Promise<void> {
|
|
70
|
+
const meta = getTemplateMetadata(templateId);
|
|
71
|
+
if (!meta) {
|
|
72
|
+
console.log(pc.red(`Template "${templateId}" not found.`));
|
|
73
|
+
console.log('Available templates:');
|
|
74
|
+
getAllTemplates().forEach(t => console.log(` - ${t.id}`));
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Get template config for version info
|
|
79
|
+
const config = TEMPLATE_CONFIGS[templateId];
|
|
80
|
+
if (!config) {
|
|
81
|
+
console.log(pc.red(`Template configuration not found for "${templateId}".`));
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
const isCompose = templateId === 'kotlin-compose';
|
|
85
|
+
|
|
86
|
+
console.log('');
|
|
87
|
+
console.log(gradientTeen(`═══ ${meta.name} ═══`));
|
|
88
|
+
console.log('');
|
|
89
|
+
console.log(bold(meta.description));
|
|
90
|
+
console.log('');
|
|
91
|
+
|
|
92
|
+
// Key information
|
|
93
|
+
printSection('Configuration');
|
|
94
|
+
printKeyValue([
|
|
95
|
+
{ key: 'Template ID', value: meta.id },
|
|
96
|
+
{ key: 'Language', value: config.language === 'kotlin' ? pc.green('Kotlin') : pc.yellow('Java') },
|
|
97
|
+
{ key: 'UI Framework', value: isCompose ? pc.cyan('Jetpack Compose') : pc.gray('XML Layouts') },
|
|
98
|
+
{ key: 'Min SDK', value: `API ${config.minSdk}` },
|
|
99
|
+
{ key: 'Target SDK', value: `API ${config.targetSdk}` },
|
|
100
|
+
{ key: 'Compile SDK', value: `API ${config.compileSdk}` }
|
|
101
|
+
]);
|
|
102
|
+
|
|
103
|
+
// Version information
|
|
104
|
+
printSection('Versions');
|
|
105
|
+
printKeyValue([
|
|
106
|
+
{ key: 'Gradle', value: config.gradleVersion },
|
|
107
|
+
{ key: 'Android Gradle Plugin', value: config.agpVersion },
|
|
108
|
+
{ key: 'Kotlin', value: config.kotlinVersion || 'N/A' },
|
|
109
|
+
...(isCompose ? [
|
|
110
|
+
{ key: 'Compose BOM', value: GRADLE_VERSIONS.COMPOSE_BOM },
|
|
111
|
+
{ key: 'Compose Compiler', value: GRADLE_VERSIONS.COMPOSE_COMPILER }
|
|
112
|
+
] : []),
|
|
113
|
+
...(templateId === 'native-cpp' ? [
|
|
114
|
+
{ key: 'NDK', value: config.ndkVersion || GRADLE_VERSIONS.NDK }
|
|
115
|
+
] : [])
|
|
116
|
+
]);
|
|
117
|
+
|
|
118
|
+
// Features
|
|
119
|
+
printSection('Features');
|
|
120
|
+
meta.features.forEach((feature, i) => {
|
|
121
|
+
console.log(` ${pc.green('+')} ${feature}`);
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
// Keywords
|
|
125
|
+
printSection('Keywords');
|
|
126
|
+
console.log(` ${meta.keywords.map(k => pc.cyan(k)).join(', ')}`);
|
|
127
|
+
|
|
128
|
+
// Code preview
|
|
129
|
+
const preview = getTemplatePreview(templateId);
|
|
130
|
+
if (preview) {
|
|
131
|
+
printSection('Code Preview');
|
|
132
|
+
console.log(pc.gray('─'.repeat(60)));
|
|
133
|
+
console.log(pc.dim(preview.split('\n').slice(0, 15).join('\n')));
|
|
134
|
+
if (preview.split('\n').length > 15) {
|
|
135
|
+
console.log(pc.gray('...'));
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
console.log('');
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Print template info as JSON
|
|
144
|
+
*/
|
|
145
|
+
function printInfoJson(meta: TemplateMetadata): void {
|
|
146
|
+
const config = TEMPLATE_CONFIGS[meta.id];
|
|
147
|
+
if (!config) return;
|
|
148
|
+
|
|
149
|
+
const isCompose = meta.id === 'kotlin-compose';
|
|
150
|
+
|
|
151
|
+
const info = {
|
|
152
|
+
id: meta.id,
|
|
153
|
+
name: meta.name,
|
|
154
|
+
description: meta.description,
|
|
155
|
+
language: config.language,
|
|
156
|
+
uiFramework: config.uiFramework === 'compose' ? 'Jetpack Compose' : 'XML Layouts',
|
|
157
|
+
keywords: meta.keywords,
|
|
158
|
+
features: meta.features,
|
|
159
|
+
versions: {
|
|
160
|
+
gradle: config.gradleVersion,
|
|
161
|
+
agp: config.agpVersion,
|
|
162
|
+
kotlin: config.kotlinVersion || null,
|
|
163
|
+
compose: config.composeEnabled ? {
|
|
164
|
+
bom: GRADLE_VERSIONS.COMPOSE_BOM,
|
|
165
|
+
compiler: GRADLE_VERSIONS.COMPOSE_COMPILER
|
|
166
|
+
} : null,
|
|
167
|
+
ndk: config.ndkVersion || null
|
|
168
|
+
},
|
|
169
|
+
sdk: {
|
|
170
|
+
min: config.minSdk,
|
|
171
|
+
target: config.targetSdk,
|
|
172
|
+
compile: config.compileSdk
|
|
173
|
+
},
|
|
174
|
+
codePreview: getTemplatePreview(meta.id)
|
|
175
|
+
};
|
|
176
|
+
|
|
177
|
+
console.log(JSON.stringify(info, null, 2));
|
|
178
|
+
}
|