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.
@@ -0,0 +1,186 @@
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
+ }
@@ -0,0 +1,156 @@
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
+ }
@@ -0,0 +1,316 @@
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 };