andrud 1.0.1 → 1.0.3
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 +295 -306
- package/dist/__tests__/context.test.d.ts +5 -0
- package/dist/__tests__/context.test.d.ts.map +1 -0
- package/dist/__tests__/context.test.js +86 -0
- package/dist/__tests__/context.test.js.map +1 -0
- package/dist/__tests__/generator.test.d.ts +5 -0
- package/dist/__tests__/generator.test.d.ts.map +1 -0
- package/dist/__tests__/generator.test.js +83 -0
- package/dist/__tests__/generator.test.js.map +1 -0
- package/dist/__tests__/validation.test.d.ts +5 -0
- package/dist/__tests__/validation.test.d.ts.map +1 -0
- package/dist/__tests__/validation.test.js +81 -0
- package/dist/__tests__/validation.test.js.map +1 -0
- package/dist/cli/commands/create.d.ts +10 -0
- package/dist/cli/commands/create.d.ts.map +1 -0
- package/dist/cli/commands/create.js +203 -0
- package/dist/cli/commands/create.js.map +1 -0
- package/dist/cli/commands/info.d.ts +13 -0
- package/dist/cli/commands/info.d.ts.map +1 -0
- package/dist/cli/commands/info.js +153 -0
- package/dist/cli/commands/info.js.map +1 -0
- package/dist/cli/commands/init.d.ts +15 -0
- package/dist/cli/commands/init.d.ts.map +1 -0
- package/dist/cli/commands/init.js +141 -0
- package/dist/cli/commands/init.js.map +1 -0
- package/dist/cli/commands/list.d.ts +18 -0
- package/dist/cli/commands/list.d.ts.map +1 -0
- package/dist/cli/commands/list.js +122 -0
- package/dist/cli/commands/list.js.map +1 -0
- package/dist/cli/commands/new.d.ts +22 -0
- package/dist/cli/commands/new.d.ts.map +1 -0
- package/dist/cli/commands/new.js +245 -0
- package/dist/cli/commands/new.js.map +1 -0
- package/dist/cli/index.d.ts +5 -0
- package/dist/cli/index.d.ts.map +1 -0
- package/dist/cli/index.js +157 -0
- package/dist/cli/index.js.map +1 -0
- package/dist/core/config.d.ts +89 -0
- package/dist/core/config.d.ts.map +1 -0
- package/dist/core/config.js +151 -0
- package/dist/core/config.js.map +1 -0
- package/dist/core/context.d.ts +47 -0
- package/dist/core/context.d.ts.map +1 -0
- package/dist/core/context.js +175 -0
- package/dist/core/context.js.map +1 -0
- package/dist/core/generator.d.ts +44 -0
- package/dist/core/generator.d.ts.map +1 -0
- package/{src/core/generator.ts → dist/core/generator.js} +394 -484
- package/dist/core/generator.js.map +1 -0
- package/dist/core/types.d.ts +125 -0
- package/dist/core/types.d.ts.map +1 -0
- package/dist/core/types.js +22 -0
- package/dist/core/types.js.map +1 -0
- package/dist/templates/index.d.ts +36 -0
- package/dist/templates/index.d.ts.map +1 -0
- package/dist/templates/index.js +141 -0
- package/dist/templates/index.js.map +1 -0
- package/dist/ui/colors.d.ts +40 -0
- package/dist/ui/colors.d.ts.map +1 -0
- package/dist/ui/colors.js +117 -0
- package/dist/ui/colors.js.map +1 -0
- package/dist/ui/output.d.ts +69 -0
- package/dist/ui/output.d.ts.map +1 -0
- package/dist/ui/output.js +199 -0
- package/dist/ui/output.js.map +1 -0
- package/dist/ui/prompts.d.ts +20 -0
- package/dist/ui/prompts.d.ts.map +1 -0
- package/dist/ui/prompts.js +118 -0
- package/dist/ui/prompts.js.map +1 -0
- package/dist/ui/spinners.d.ts +30 -0
- package/dist/ui/spinners.d.ts.map +1 -0
- package/dist/ui/spinners.js +74 -0
- package/dist/ui/spinners.js.map +1 -0
- package/dist/ui/types.d.ts +35 -0
- package/dist/ui/types.d.ts.map +1 -0
- package/dist/ui/types.js +5 -0
- package/dist/ui/types.js.map +1 -0
- package/dist/utils/filesystem.d.ts +38 -0
- package/dist/utils/filesystem.d.ts.map +1 -0
- package/dist/utils/filesystem.js +181 -0
- package/dist/utils/filesystem.js.map +1 -0
- package/dist/utils/logger.d.ts +27 -0
- package/dist/utils/logger.d.ts.map +1 -0
- package/dist/utils/logger.js +52 -0
- package/dist/utils/logger.js.map +1 -0
- package/dist/utils/object.d.ts +140 -0
- package/dist/utils/object.d.ts.map +1 -0
- package/dist/utils/object.js +385 -0
- package/dist/utils/object.js.map +1 -0
- package/dist/utils/validation.d.ts +35 -0
- package/dist/utils/validation.d.ts.map +1 -0
- package/dist/utils/validation.js +270 -0
- package/dist/utils/validation.js.map +1 -0
- package/package.json +10 -21
- package/CHANGELOG.md +0 -70
- package/CONTRIBUTING.md +0 -132
- package/sc.png +0 -0
- package/src/__tests__/context.test.ts +0 -133
- package/src/__tests__/generator.test.ts +0 -107
- package/src/__tests__/validation.test.ts +0 -105
- package/src/cli/commands/create.ts +0 -252
- package/src/cli/commands/info.ts +0 -178
- package/src/cli/commands/init.ts +0 -186
- package/src/cli/commands/list.ts +0 -156
- package/src/cli/commands/new.ts +0 -316
- package/src/cli/index.ts +0 -116
- package/src/core/config.ts +0 -172
- package/src/core/context.ts +0 -212
- package/src/core/types.ts +0 -184
- package/src/templates/index.ts +0 -162
- package/src/types/gradient-string.d.ts +0 -25
- package/src/ui/colors.ts +0 -139
- package/src/ui/output.ts +0 -230
- package/src/ui/prompts.ts +0 -170
- package/src/ui/spinners.ts +0 -95
- package/src/ui/types.ts +0 -41
- package/src/utils/filesystem.ts +0 -222
- package/src/utils/logger.ts +0 -67
- package/src/utils/object.ts +0 -456
- package/src/utils/validation.ts +0 -345
- package/tsconfig.json +0 -25
package/src/cli/commands/init.ts
DELETED
|
@@ -1,186 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Init project command - initialize Android project in current directory
|
|
3
|
-
*/
|
|
4
|
-
|
|
5
|
-
import {
|
|
6
|
-
select,
|
|
7
|
-
text,
|
|
8
|
-
confirm,
|
|
9
|
-
isCancel,
|
|
10
|
-
cancel
|
|
11
|
-
} from '@clack/prompts';
|
|
12
|
-
import {
|
|
13
|
-
printWelcome,
|
|
14
|
-
printSuccess,
|
|
15
|
-
printError,
|
|
16
|
-
printSection,
|
|
17
|
-
printKeyValue,
|
|
18
|
-
printGoodbye
|
|
19
|
-
} from '../../ui/output.js';
|
|
20
|
-
import { validatePackageStructure } from '../../utils/validation.js';
|
|
21
|
-
import { generateProject } from '../../core/generator.js';
|
|
22
|
-
import { buildDefaultProjectContext, buildTemplateContext } from '../../core/context.js';
|
|
23
|
-
import { getCurrentWorkingDirectory } from '../../utils/filesystem.js';
|
|
24
|
-
import { getTemplateMetadata, getAllTemplates } from '../../templates/index.js';
|
|
25
|
-
import pc from 'picocolors';
|
|
26
|
-
import type { TemplateType } from '../../core/types.js';
|
|
27
|
-
|
|
28
|
-
interface InitCommandOptions {
|
|
29
|
-
force: boolean;
|
|
30
|
-
template?: string;
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
/**
|
|
34
|
-
* Initialize Android project in current directory
|
|
35
|
-
*/
|
|
36
|
-
export async function createInitCommand(
|
|
37
|
-
_options: InitCommandOptions,
|
|
38
|
-
args?: { template?: string }
|
|
39
|
-
): Promise<void> {
|
|
40
|
-
try {
|
|
41
|
-
printWelcome();
|
|
42
|
-
|
|
43
|
-
const cwd = getCurrentWorkingDirectory();
|
|
44
|
-
const projectName = cwd.split(/[\\/]/).pop() ?? 'MyApp';
|
|
45
|
-
|
|
46
|
-
printSection('Initialize Android Project');
|
|
47
|
-
console.log(`Current directory: ${pc.cyan(cwd)}`);
|
|
48
|
-
console.log('');
|
|
49
|
-
|
|
50
|
-
// Get templates
|
|
51
|
-
const templates = getAllTemplates();
|
|
52
|
-
|
|
53
|
-
// Step 1: Select template
|
|
54
|
-
let selectedTemplate: string;
|
|
55
|
-
if (args?.template) {
|
|
56
|
-
const meta = getTemplateMetadata(args.template as TemplateType);
|
|
57
|
-
if (!meta) {
|
|
58
|
-
printError(`Unknown template: ${args.template}`);
|
|
59
|
-
return;
|
|
60
|
-
}
|
|
61
|
-
selectedTemplate = args.template;
|
|
62
|
-
} else {
|
|
63
|
-
const templateOptions = templates.map(t => ({
|
|
64
|
-
label: t.name,
|
|
65
|
-
value: t.id,
|
|
66
|
-
hint: t.keywords.slice(0, 2).join(', ')
|
|
67
|
-
}));
|
|
68
|
-
|
|
69
|
-
const templateResult = await select({
|
|
70
|
-
message: '? Select a project template',
|
|
71
|
-
options: templateOptions
|
|
72
|
-
});
|
|
73
|
-
|
|
74
|
-
if (isCancel(templateResult)) {
|
|
75
|
-
cancel();
|
|
76
|
-
return;
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
selectedTemplate = templateResult as string;
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
// Step 2: Get package name
|
|
83
|
-
const defaultPackage = `com.example.${projectName.toLowerCase().replace(/[^a-z0-9]/g, '')}`;
|
|
84
|
-
const packageResult = await text({
|
|
85
|
-
message: '? What is the package name?',
|
|
86
|
-
placeholder: 'com.example.myapp',
|
|
87
|
-
defaultValue: defaultPackage,
|
|
88
|
-
validate: (value: string) => {
|
|
89
|
-
const result = validatePackageStructure(value);
|
|
90
|
-
if (!result.valid) {
|
|
91
|
-
return result.errors[0];
|
|
92
|
-
}
|
|
93
|
-
return undefined;
|
|
94
|
-
}
|
|
95
|
-
});
|
|
96
|
-
|
|
97
|
-
if (isCancel(packageResult)) {
|
|
98
|
-
cancel();
|
|
99
|
-
return;
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
const packageName = packageResult as string;
|
|
103
|
-
const validation = validatePackageStructure(packageName);
|
|
104
|
-
if (!validation.valid) {
|
|
105
|
-
printError('Invalid package name', validation.errors.join(', '));
|
|
106
|
-
return;
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
// Confirm initialization
|
|
110
|
-
const confirmInit = await confirm({
|
|
111
|
-
message: `? Initialize Android project in ${cwd}?`,
|
|
112
|
-
initialValue: true
|
|
113
|
-
});
|
|
114
|
-
|
|
115
|
-
if (isCancel(confirmInit)) {
|
|
116
|
-
cancel();
|
|
117
|
-
return;
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
if (!confirmInit) {
|
|
121
|
-
console.log('Initialization cancelled.');
|
|
122
|
-
return;
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
// Build context
|
|
126
|
-
const baseContext = buildDefaultProjectContext(
|
|
127
|
-
projectName,
|
|
128
|
-
packageName,
|
|
129
|
-
cwd,
|
|
130
|
-
selectedTemplate as TemplateType,
|
|
131
|
-
{
|
|
132
|
-
git: true,
|
|
133
|
-
readme: true,
|
|
134
|
-
androidX: true,
|
|
135
|
-
kotlinDsl: true
|
|
136
|
-
}
|
|
137
|
-
);
|
|
138
|
-
|
|
139
|
-
const context = buildTemplateContext({
|
|
140
|
-
appName: baseContext.appName,
|
|
141
|
-
packageName: baseContext.packageName,
|
|
142
|
-
projectDirectory: baseContext.projectDirectory,
|
|
143
|
-
template: baseContext.template,
|
|
144
|
-
uiFramework: baseContext.uiFramework,
|
|
145
|
-
language: baseContext.language,
|
|
146
|
-
android: baseContext.android,
|
|
147
|
-
gradle: baseContext.gradle,
|
|
148
|
-
features: baseContext as unknown as Record<string, boolean>,
|
|
149
|
-
nativeCpp: baseContext.nativeCpp
|
|
150
|
-
});
|
|
151
|
-
|
|
152
|
-
// Generate project
|
|
153
|
-
printSection('Generating Project');
|
|
154
|
-
|
|
155
|
-
const result = await generateProject(context, {
|
|
156
|
-
overwrite: true,
|
|
157
|
-
dryRun: false,
|
|
158
|
-
skipInstall: false,
|
|
159
|
-
verbose: false
|
|
160
|
-
});
|
|
161
|
-
|
|
162
|
-
if (result.success) {
|
|
163
|
-
printSuccess('Project initialized successfully!');
|
|
164
|
-
printKeyValue([
|
|
165
|
-
{ key: 'Package', value: packageName },
|
|
166
|
-
{ key: 'Template', value: selectedTemplate },
|
|
167
|
-
{ key: 'Location', value: cwd },
|
|
168
|
-
{ key: 'Files created', value: result.generatedFiles.length.toString() }
|
|
169
|
-
]);
|
|
170
|
-
|
|
171
|
-
printGoodbye(true);
|
|
172
|
-
} else {
|
|
173
|
-
printError('Project initialization failed');
|
|
174
|
-
if (result.errors.length > 0) {
|
|
175
|
-
result.errors.forEach(err => {
|
|
176
|
-
console.log(` ${pc.red('•')} ${err.file ? `${err.file}: ` : ''}${err.message}`);
|
|
177
|
-
});
|
|
178
|
-
}
|
|
179
|
-
printGoodbye(false);
|
|
180
|
-
}
|
|
181
|
-
} catch (error) {
|
|
182
|
-
printError('An unexpected error occurred', (error as Error).message);
|
|
183
|
-
printGoodbye(false);
|
|
184
|
-
process.exit(1);
|
|
185
|
-
}
|
|
186
|
-
}
|
package/src/cli/commands/list.ts
DELETED
|
@@ -1,156 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* List templates command - shows all available templates
|
|
3
|
-
*/
|
|
4
|
-
|
|
5
|
-
import { gradientTeen, bold, primary, muted } from '../../ui/colors.js';
|
|
6
|
-
import { printSection } from '../../ui/output.js';
|
|
7
|
-
import { getAllTemplates, getTemplatePreview, searchTemplates } from '../../templates/index.js';
|
|
8
|
-
import { TEMPLATE_CONFIGS } from '../../core/config.js';
|
|
9
|
-
import pc from 'picocolors';
|
|
10
|
-
import type { TemplateType, TemplateMetadata } from '../../core/types.js';
|
|
11
|
-
|
|
12
|
-
interface ListCommandOptions {
|
|
13
|
-
json: boolean;
|
|
14
|
-
search?: string;
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
/**
|
|
18
|
-
* List all available templates
|
|
19
|
-
*/
|
|
20
|
-
export async function createListCommand(
|
|
21
|
-
options: ListCommandOptions = { json: false }
|
|
22
|
-
): Promise<void> {
|
|
23
|
-
// Validate search input
|
|
24
|
-
if (options.search && typeof options.search !== 'string') {
|
|
25
|
-
console.log(pc.red('Error: Search query must be a string'));
|
|
26
|
-
return;
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
if (options.search && options.search.length > 100) {
|
|
30
|
-
console.log(pc.red('Error: Search query is too long (maximum 100 characters)'));
|
|
31
|
-
return;
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
// Filter templates based on search
|
|
35
|
-
const templates = options.search?.trim()
|
|
36
|
-
? searchTemplates(options.search.trim())
|
|
37
|
-
: getAllTemplates();
|
|
38
|
-
|
|
39
|
-
if (options.json) {
|
|
40
|
-
const templateList = templates.map(t => ({
|
|
41
|
-
id: t.id,
|
|
42
|
-
name: t.name,
|
|
43
|
-
description: t.description,
|
|
44
|
-
language: TEMPLATE_CONFIGS[t.id]?.language === 'kotlin' ? 'Kotlin' : 'Java',
|
|
45
|
-
uiFramework: TEMPLATE_CONFIGS[t.id]?.uiFramework === 'compose' ? 'Compose' : 'XML'
|
|
46
|
-
}));
|
|
47
|
-
console.log(JSON.stringify(templateList, null, 2));
|
|
48
|
-
return;
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
if (templates.length === 0) {
|
|
52
|
-
console.log(pc.yellow('No templates found.'));
|
|
53
|
-
return;
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
console.log('');
|
|
57
|
-
console.log(gradientTeen('╔══════════════════════════════════════════════════════════════╗'));
|
|
58
|
-
console.log(gradientTeen('║ Android Project Templates ║'));
|
|
59
|
-
console.log(gradientTeen('╚══════════════════════════════════════════════════════════════╝'));
|
|
60
|
-
console.log('');
|
|
61
|
-
|
|
62
|
-
templates.forEach((template, index) => {
|
|
63
|
-
printTemplateCard(template, index + 1);
|
|
64
|
-
});
|
|
65
|
-
|
|
66
|
-
// Summary
|
|
67
|
-
printSection('Summary');
|
|
68
|
-
console.log(` Total templates: ${bold(templates.length.toString())}`);
|
|
69
|
-
console.log(` Kotlin templates: ${bold(
|
|
70
|
-
templates.filter(t => TEMPLATE_CONFIGS[t.id]?.language === 'kotlin').length.toString()
|
|
71
|
-
)}`);
|
|
72
|
-
console.log(` Java templates: ${bold(
|
|
73
|
-
templates.filter(t => TEMPLATE_CONFIGS[t.id]?.language === 'java').length.toString()
|
|
74
|
-
)}`);
|
|
75
|
-
console.log(` Compose templates: ${bold(
|
|
76
|
-
templates.filter(t => TEMPLATE_CONFIGS[t.id]?.uiFramework === 'compose').length.toString()
|
|
77
|
-
)}`);
|
|
78
|
-
console.log('');
|
|
79
|
-
|
|
80
|
-
console.log(muted(' Use ') + primary('andrud info <template>') + muted(' for more details'));
|
|
81
|
-
console.log('');
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
/**
|
|
85
|
-
* Print a template as a card
|
|
86
|
-
*/
|
|
87
|
-
function printTemplateCard(template: TemplateMetadata, number: number): void {
|
|
88
|
-
const config = TEMPLATE_CONFIGS[template.id];
|
|
89
|
-
if (!config) return;
|
|
90
|
-
|
|
91
|
-
const isKotlin = config.language === 'kotlin';
|
|
92
|
-
const isCompose = config.uiFramework === 'compose';
|
|
93
|
-
|
|
94
|
-
// Template header
|
|
95
|
-
console.log(primary(` ${number}. `) + bold(template.name));
|
|
96
|
-
console.log(` ${template.description}`);
|
|
97
|
-
console.log('');
|
|
98
|
-
|
|
99
|
-
// Tags
|
|
100
|
-
const tags: string[] = [];
|
|
101
|
-
tags.push(isKotlin ? pc.green('Kotlin') : pc.yellow('Java'));
|
|
102
|
-
tags.push(isCompose ? pc.cyan('Compose') : pc.gray('XML'));
|
|
103
|
-
if (template.id === 'native-cpp') {
|
|
104
|
-
tags.push(pc.magenta('NDK'));
|
|
105
|
-
}
|
|
106
|
-
console.log(` ${tags.join(' ')}`);
|
|
107
|
-
|
|
108
|
-
// Key features
|
|
109
|
-
const features = template.features.slice(0, 3);
|
|
110
|
-
console.log(` ${muted('Features:')} ${features.join(pc.gray(', '))}`);
|
|
111
|
-
|
|
112
|
-
// Version info
|
|
113
|
-
console.log(` ${muted('Gradle:')} ${config.gradleVersion} ${muted('AGP:')} ${config.agpVersion}`);
|
|
114
|
-
|
|
115
|
-
// Command example
|
|
116
|
-
console.log('');
|
|
117
|
-
console.log(` ${muted('Run:')} ${primary(`andrud new MyApp -t ${template.id}`)}`);
|
|
118
|
-
|
|
119
|
-
// Code preview snippet
|
|
120
|
-
const preview = getTemplatePreview(template.id);
|
|
121
|
-
if (preview) {
|
|
122
|
-
const lines = preview.split('\n').slice(0, 4);
|
|
123
|
-
console.log('');
|
|
124
|
-
console.log(' ' + pc.gray('┌─ Code preview'));
|
|
125
|
-
lines.forEach((line, i) => {
|
|
126
|
-
const isLast = i === lines.length - 1;
|
|
127
|
-
const prefix = isLast ? ' ' : ' ';
|
|
128
|
-
const linePrefix = isLast ? '└─' : '│ ';
|
|
129
|
-
console.log(prefix + pc.gray(linePrefix) + pc.dim(line.substring(0, 50) + (line.length > 50 ? '...' : '')));
|
|
130
|
-
});
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
console.log('');
|
|
134
|
-
console.log(pc.gray(' ' + '─'.repeat(60)));
|
|
135
|
-
console.log('');
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
/**
|
|
139
|
-
* Print templates as a compact list
|
|
140
|
-
*/
|
|
141
|
-
export function printTemplateList(templates: TemplateMetadata[]): void {
|
|
142
|
-
const maxIdLength = Math.max(...templates.map(t => t.id.length));
|
|
143
|
-
const maxNameLength = Math.max(...templates.map(t => t.name.length));
|
|
144
|
-
|
|
145
|
-
templates.forEach((t, i) => {
|
|
146
|
-
const config = TEMPLATE_CONFIGS[t.id];
|
|
147
|
-
if (!config) return;
|
|
148
|
-
|
|
149
|
-
const id = t.id.padEnd(maxIdLength);
|
|
150
|
-
const name = t.name.padEnd(maxNameLength);
|
|
151
|
-
const lang = config.language === 'kotlin' ? pc.green('K') : pc.yellow('J');
|
|
152
|
-
const ui = config.uiFramework === 'compose' ? pc.cyan('C') : pc.gray('X');
|
|
153
|
-
|
|
154
|
-
console.log(` ${primary(String(i + 1).padStart(2))} ${bold(id)} ${name} ${lang}${ui} ${muted(t.description.substring(0, 40))}`);
|
|
155
|
-
});
|
|
156
|
-
}
|
package/src/cli/commands/new.ts
DELETED
|
@@ -1,316 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* New project creation command with interactive prompts
|
|
3
|
-
*/
|
|
4
|
-
|
|
5
|
-
import {
|
|
6
|
-
select,
|
|
7
|
-
text,
|
|
8
|
-
multiselect,
|
|
9
|
-
confirm,
|
|
10
|
-
isCancel,
|
|
11
|
-
cancel
|
|
12
|
-
} from '@clack/prompts';
|
|
13
|
-
import {
|
|
14
|
-
printWelcome,
|
|
15
|
-
printSuccess,
|
|
16
|
-
printError,
|
|
17
|
-
printSection,
|
|
18
|
-
printKeyValue,
|
|
19
|
-
printGoodbye,
|
|
20
|
-
printSteps
|
|
21
|
-
} from '../../ui/output.js';
|
|
22
|
-
import { gradientTeen } from '../../ui/colors.js';
|
|
23
|
-
import { validateAppName, validatePackageStructure, validateDirectoryPath } from '../../utils/validation.js';
|
|
24
|
-
import { generateProject } from '../../core/generator.js';
|
|
25
|
-
import { buildDefaultProjectContext, buildTemplateContext } from '../../core/context.js';
|
|
26
|
-
import { exists, getCurrentWorkingDirectory } from '../../utils/filesystem.js';
|
|
27
|
-
import { getTemplateMetadata, getAllTemplates } from '../../templates/index.js';
|
|
28
|
-
import pc from 'picocolors';
|
|
29
|
-
import type { TemplateType } from '../../core/types.js';
|
|
30
|
-
|
|
31
|
-
interface NewCommandOptions {
|
|
32
|
-
name?: string;
|
|
33
|
-
template?: string;
|
|
34
|
-
packageName?: string;
|
|
35
|
-
directory?: string;
|
|
36
|
-
minSdk?: number;
|
|
37
|
-
targetSdk?: number;
|
|
38
|
-
force?: boolean;
|
|
39
|
-
skipInstall?: boolean;
|
|
40
|
-
git?: boolean;
|
|
41
|
-
kotlin?: boolean;
|
|
42
|
-
verbose?: boolean;
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
/**
|
|
46
|
-
* Create a new Android project
|
|
47
|
-
*/
|
|
48
|
-
export async function createNewCommand(
|
|
49
|
-
name?: string,
|
|
50
|
-
options: NewCommandOptions = {}
|
|
51
|
-
): Promise<void> {
|
|
52
|
-
try {
|
|
53
|
-
// Print welcome banner
|
|
54
|
-
printWelcome();
|
|
55
|
-
|
|
56
|
-
// Get all available templates
|
|
57
|
-
const templates = getAllTemplates();
|
|
58
|
-
|
|
59
|
-
// Step 1: Select template
|
|
60
|
-
let selectedTemplate: string;
|
|
61
|
-
if (options.template) {
|
|
62
|
-
const meta = getTemplateMetadata(options.template as TemplateType);
|
|
63
|
-
if (!meta) {
|
|
64
|
-
printError(`Unknown template: ${options.template}`);
|
|
65
|
-
console.log('Available templates:');
|
|
66
|
-
templates.forEach(t => console.log(` - ${t.id}`));
|
|
67
|
-
return;
|
|
68
|
-
}
|
|
69
|
-
selectedTemplate = options.template;
|
|
70
|
-
} else {
|
|
71
|
-
const templateOptions = templates.map(t => ({
|
|
72
|
-
label: `${t.name}`,
|
|
73
|
-
value: t.id,
|
|
74
|
-
hint: t.keywords.slice(0, 2).join(', ')
|
|
75
|
-
}));
|
|
76
|
-
|
|
77
|
-
const templateResult = await select({
|
|
78
|
-
message: '? Select a project template',
|
|
79
|
-
options: templateOptions
|
|
80
|
-
});
|
|
81
|
-
|
|
82
|
-
if (isCancel(templateResult)) {
|
|
83
|
-
cancel();
|
|
84
|
-
return;
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
selectedTemplate = templateResult as string;
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
printSection(`Template: ${selectedTemplate}`);
|
|
91
|
-
|
|
92
|
-
// Step 2: Get app name
|
|
93
|
-
let appName: string;
|
|
94
|
-
if (name) {
|
|
95
|
-
const validation = validateAppName(name);
|
|
96
|
-
if (!validation.valid) {
|
|
97
|
-
printError('Invalid app name', validation.errors.join(', '));
|
|
98
|
-
return;
|
|
99
|
-
}
|
|
100
|
-
appName = validation.normalized ?? name;
|
|
101
|
-
} else {
|
|
102
|
-
const nameResult = await text({
|
|
103
|
-
message: '? What is the app name?',
|
|
104
|
-
placeholder: 'MyAwesomeApp',
|
|
105
|
-
defaultValue: 'MyAwesomeApp',
|
|
106
|
-
validate: (value: string) => {
|
|
107
|
-
const result = validateAppName(value);
|
|
108
|
-
if (!result.valid) {
|
|
109
|
-
return result.errors[0];
|
|
110
|
-
}
|
|
111
|
-
return undefined;
|
|
112
|
-
}
|
|
113
|
-
});
|
|
114
|
-
|
|
115
|
-
if (isCancel(nameResult)) {
|
|
116
|
-
cancel();
|
|
117
|
-
return;
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
const validation = validateAppName(nameResult as string);
|
|
121
|
-
if (!validation.valid) {
|
|
122
|
-
printError('Invalid app name', validation.errors.join(', '));
|
|
123
|
-
return;
|
|
124
|
-
}
|
|
125
|
-
appName = validation.normalized ?? (nameResult as string);
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
// Step 3: Get package name
|
|
129
|
-
let packageName: string;
|
|
130
|
-
if (options.packageName) {
|
|
131
|
-
const validation = validatePackageStructure(options.packageName);
|
|
132
|
-
if (!validation.valid) {
|
|
133
|
-
printError('Invalid package name', validation.errors.join(', '));
|
|
134
|
-
return;
|
|
135
|
-
}
|
|
136
|
-
packageName = options.packageName;
|
|
137
|
-
} else {
|
|
138
|
-
// Generate default package name from app name
|
|
139
|
-
const defaultPackage = `com.example.${appName.toLowerCase().replace(/[^a-z0-9]/g, '')}`;
|
|
140
|
-
|
|
141
|
-
const packageResult = await text({
|
|
142
|
-
message: '? What is the package name?',
|
|
143
|
-
placeholder: 'com.example.myapp',
|
|
144
|
-
defaultValue: defaultPackage,
|
|
145
|
-
validate: (value: string) => {
|
|
146
|
-
const result = validatePackageStructure(value);
|
|
147
|
-
if (!result.valid) {
|
|
148
|
-
return result.errors[0];
|
|
149
|
-
}
|
|
150
|
-
return undefined;
|
|
151
|
-
}
|
|
152
|
-
});
|
|
153
|
-
|
|
154
|
-
if (isCancel(packageResult)) {
|
|
155
|
-
cancel();
|
|
156
|
-
return;
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
const validation = validatePackageStructure(packageResult as string);
|
|
160
|
-
if (!validation.valid) {
|
|
161
|
-
printError('Invalid package name', validation.errors.join(', '));
|
|
162
|
-
return;
|
|
163
|
-
}
|
|
164
|
-
packageName = packageResult as string;
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
// Step 4: Get project directory
|
|
168
|
-
let projectDirectory: string;
|
|
169
|
-
if (options.directory) {
|
|
170
|
-
const validation = validateDirectoryPath(options.directory);
|
|
171
|
-
if (!validation.valid) {
|
|
172
|
-
printError('Invalid directory', validation.errors.join(', '));
|
|
173
|
-
return;
|
|
174
|
-
}
|
|
175
|
-
projectDirectory = validation.normalized ?? options.directory;
|
|
176
|
-
} else {
|
|
177
|
-
const defaultDir = `./${appName.toLowerCase().replace(/[^a-z0-9]/g, '-')}`;
|
|
178
|
-
const dirResult = await text({
|
|
179
|
-
message: '? In which directory should the project be created?',
|
|
180
|
-
placeholder: defaultDir,
|
|
181
|
-
defaultValue: defaultDir,
|
|
182
|
-
validate: (value: string) => {
|
|
183
|
-
const result = validateDirectoryPath(value);
|
|
184
|
-
if (!result.valid) {
|
|
185
|
-
return result.errors[0];
|
|
186
|
-
}
|
|
187
|
-
return undefined;
|
|
188
|
-
}
|
|
189
|
-
});
|
|
190
|
-
|
|
191
|
-
if (isCancel(dirResult)) {
|
|
192
|
-
cancel();
|
|
193
|
-
return;
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
const validation = validateDirectoryPath(dirResult as string);
|
|
197
|
-
if (!validation.valid) {
|
|
198
|
-
printError('Invalid directory', validation.errors.join(', '));
|
|
199
|
-
return;
|
|
200
|
-
}
|
|
201
|
-
projectDirectory = validation.normalized ?? (dirResult as string);
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
// Step 5: Optional features
|
|
205
|
-
const featureOptions = [
|
|
206
|
-
{ label: 'Git repository', value: 'git', hint: 'Initialize with git' },
|
|
207
|
-
{ label: 'README.md', value: 'readme', hint: 'Generate readme' },
|
|
208
|
-
{ label: 'AndroidX libraries', value: 'androidX', hint: 'Use AndroidX (recommended)', selected: true },
|
|
209
|
-
{ label: 'Kotlin DSL', value: 'kotlinDsl', hint: 'Use Kotlin DSL for Gradle', selected: true }
|
|
210
|
-
];
|
|
211
|
-
|
|
212
|
-
const featuresResult = await multiselect({
|
|
213
|
-
message: '? Select additional features (optional)',
|
|
214
|
-
options: featureOptions,
|
|
215
|
-
required: false
|
|
216
|
-
});
|
|
217
|
-
|
|
218
|
-
if (isCancel(featuresResult)) {
|
|
219
|
-
cancel();
|
|
220
|
-
return;
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
const selectedFeatures = featuresResult as string[];
|
|
224
|
-
const features = {
|
|
225
|
-
git: selectedFeatures.includes('git') || options.git === true,
|
|
226
|
-
readme: selectedFeatures.includes('readme'),
|
|
227
|
-
androidX: selectedFeatures.includes('androidX'),
|
|
228
|
-
kotlinDsl: selectedFeatures.includes('kotlinDsl')
|
|
229
|
-
};
|
|
230
|
-
|
|
231
|
-
// Build context
|
|
232
|
-
const baseContext = buildDefaultProjectContext(
|
|
233
|
-
appName,
|
|
234
|
-
packageName,
|
|
235
|
-
projectDirectory,
|
|
236
|
-
selectedTemplate as TemplateType,
|
|
237
|
-
features,
|
|
238
|
-
options.minSdk,
|
|
239
|
-
options.targetSdk
|
|
240
|
-
);
|
|
241
|
-
|
|
242
|
-
const context = buildTemplateContext({
|
|
243
|
-
appName: baseContext.appName,
|
|
244
|
-
packageName: baseContext.packageName,
|
|
245
|
-
projectDirectory: baseContext.projectDirectory,
|
|
246
|
-
template: baseContext.template,
|
|
247
|
-
uiFramework: baseContext.uiFramework,
|
|
248
|
-
language: baseContext.language,
|
|
249
|
-
android: baseContext.android,
|
|
250
|
-
gradle: baseContext.gradle,
|
|
251
|
-
features: baseContext as unknown as Record<string, boolean>,
|
|
252
|
-
nativeCpp: baseContext.nativeCpp
|
|
253
|
-
});
|
|
254
|
-
|
|
255
|
-
// Check if directory exists
|
|
256
|
-
const dirExists = await exists(projectDirectory);
|
|
257
|
-
if (dirExists && !options.force) {
|
|
258
|
-
printError(
|
|
259
|
-
`Directory "${projectDirectory}" already exists`,
|
|
260
|
-
'Use --force to overwrite existing files'
|
|
261
|
-
);
|
|
262
|
-
return;
|
|
263
|
-
}
|
|
264
|
-
|
|
265
|
-
// Generate project
|
|
266
|
-
console.log('');
|
|
267
|
-
printSection('Generating Project');
|
|
268
|
-
|
|
269
|
-
const result = await generateProject(context, {
|
|
270
|
-
overwrite: options.force,
|
|
271
|
-
dryRun: false,
|
|
272
|
-
skipInstall: options.skipInstall,
|
|
273
|
-
verbose: options.verbose ?? false
|
|
274
|
-
});
|
|
275
|
-
|
|
276
|
-
if (result.success) {
|
|
277
|
-
printSuccess('Project generated successfully!');
|
|
278
|
-
printKeyValue([
|
|
279
|
-
{ key: 'Project', value: appName },
|
|
280
|
-
{ key: 'Package', value: packageName },
|
|
281
|
-
{ key: 'Template', value: selectedTemplate },
|
|
282
|
-
{ key: 'Location', value: projectDirectory },
|
|
283
|
-
{ key: 'Files created', value: result.generatedFiles.length.toString() },
|
|
284
|
-
{ key: 'Duration', value: `${(result.duration / 1000).toFixed(1)}s` }
|
|
285
|
-
]);
|
|
286
|
-
|
|
287
|
-
console.log('');
|
|
288
|
-
printSteps([
|
|
289
|
-
`cd ${projectDirectory}`,
|
|
290
|
-
'Open in Android Studio: studio .',
|
|
291
|
-
'Or build from command line: ./gradlew assembleDebug'
|
|
292
|
-
]);
|
|
293
|
-
|
|
294
|
-
printGoodbye(true);
|
|
295
|
-
} else {
|
|
296
|
-
printError('Project generation failed');
|
|
297
|
-
if (result.errors.length > 0) {
|
|
298
|
-
console.log('');
|
|
299
|
-
result.errors.forEach(err => {
|
|
300
|
-
console.log(` ${pc.red('•')} ${err.file ? `${err.file}: ` : ''}${err.message}`);
|
|
301
|
-
});
|
|
302
|
-
}
|
|
303
|
-
printGoodbye(false);
|
|
304
|
-
}
|
|
305
|
-
} catch (error) {
|
|
306
|
-
printError('An unexpected error occurred', (error as Error).message);
|
|
307
|
-
if (process.env.DEBUG || process.env.VERBOSE) {
|
|
308
|
-
console.error(error);
|
|
309
|
-
}
|
|
310
|
-
printGoodbye(false);
|
|
311
|
-
process.exit(1);
|
|
312
|
-
}
|
|
313
|
-
}
|
|
314
|
-
|
|
315
|
-
// Export for use in CLI
|
|
316
|
-
export type { NewCommandOptions };
|