andrud 1.0.0 → 1.0.2
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 +69 -83
- 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 +99 -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 +8 -19
- package/CHANGELOG.md +0 -70
- package/CODE_REVIEW_ANALYSIS.md +0 -177
- package/CONTRIBUTING.md +0 -132
- package/FIXES_IMPLEMENTED.md +0 -546
- 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
|
@@ -1,133 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Test suite for context builder
|
|
3
|
-
*/
|
|
4
|
-
|
|
5
|
-
import { test } from 'node:test';
|
|
6
|
-
import assert from 'node:assert';
|
|
7
|
-
import {
|
|
8
|
-
buildDefaultProjectContext,
|
|
9
|
-
buildTemplateContext,
|
|
10
|
-
validateContext
|
|
11
|
-
} from '../core/context.js';
|
|
12
|
-
import type { TemplateType } from '../core/types.js';
|
|
13
|
-
|
|
14
|
-
test('Context - Build Default Context', async (t) => {
|
|
15
|
-
await t.test('should create valid default context', () => {
|
|
16
|
-
const context = buildDefaultProjectContext(
|
|
17
|
-
'MyApp',
|
|
18
|
-
'com.example.myapp',
|
|
19
|
-
'./my-project',
|
|
20
|
-
'kotlin-compose' as TemplateType,
|
|
21
|
-
{ git: true, readme: true }
|
|
22
|
-
);
|
|
23
|
-
|
|
24
|
-
assert.strictEqual(context.appName, 'MyApp');
|
|
25
|
-
assert.strictEqual(context.packageName, 'com.example.myapp');
|
|
26
|
-
assert.strictEqual(context.template, 'kotlin-compose');
|
|
27
|
-
assert.strictEqual(context.language, 'kotlin');
|
|
28
|
-
assert.strictEqual(context.uiFramework, 'compose');
|
|
29
|
-
});
|
|
30
|
-
|
|
31
|
-
await t.test('should set default features', () => {
|
|
32
|
-
const context = buildDefaultProjectContext(
|
|
33
|
-
'MyApp',
|
|
34
|
-
'com.example.myapp',
|
|
35
|
-
'./my-project',
|
|
36
|
-
'kotlin-xml' as TemplateType
|
|
37
|
-
);
|
|
38
|
-
|
|
39
|
-
assert.strictEqual(context.git, true);
|
|
40
|
-
assert.strictEqual(context.readme, true);
|
|
41
|
-
assert.strictEqual(context.androidX, true);
|
|
42
|
-
});
|
|
43
|
-
});
|
|
44
|
-
|
|
45
|
-
test('Context - Build Template Context', async (t) => {
|
|
46
|
-
await t.test('should build full template context', () => {
|
|
47
|
-
const baseContext = buildDefaultProjectContext(
|
|
48
|
-
'MyApp',
|
|
49
|
-
'com.example.myapp',
|
|
50
|
-
'./my-project',
|
|
51
|
-
'kotlin-compose' as TemplateType
|
|
52
|
-
);
|
|
53
|
-
|
|
54
|
-
const context = buildTemplateContext({
|
|
55
|
-
appName: baseContext.appName,
|
|
56
|
-
packageName: baseContext.packageName,
|
|
57
|
-
projectDirectory: baseContext.projectDirectory,
|
|
58
|
-
template: baseContext.template,
|
|
59
|
-
uiFramework: baseContext.uiFramework,
|
|
60
|
-
language: baseContext.language,
|
|
61
|
-
android: baseContext.android,
|
|
62
|
-
gradle: baseContext.gradle,
|
|
63
|
-
features: baseContext as unknown as Record<string, boolean>
|
|
64
|
-
});
|
|
65
|
-
|
|
66
|
-
assert.ok(context.appNameCamel);
|
|
67
|
-
assert.ok(context.appNamePascal);
|
|
68
|
-
assert.ok(context.appNameKebab);
|
|
69
|
-
assert.ok(context.appNameSnake);
|
|
70
|
-
assert.ok(context.packagePath);
|
|
71
|
-
assert.ok(context.year);
|
|
72
|
-
assert.ok(context.generatorVersion);
|
|
73
|
-
});
|
|
74
|
-
|
|
75
|
-
await t.test('should generate correct app name transformations', () => {
|
|
76
|
-
const baseContext = buildDefaultProjectContext(
|
|
77
|
-
'MyAwesomeApp',
|
|
78
|
-
'com.example.myapp',
|
|
79
|
-
'./my-project',
|
|
80
|
-
'kotlin-compose' as TemplateType
|
|
81
|
-
);
|
|
82
|
-
|
|
83
|
-
const context = buildTemplateContext({
|
|
84
|
-
appName: baseContext.appName,
|
|
85
|
-
packageName: baseContext.packageName,
|
|
86
|
-
projectDirectory: baseContext.projectDirectory,
|
|
87
|
-
template: baseContext.template,
|
|
88
|
-
uiFramework: baseContext.uiFramework,
|
|
89
|
-
language: baseContext.language,
|
|
90
|
-
android: baseContext.android,
|
|
91
|
-
gradle: baseContext.gradle,
|
|
92
|
-
features: baseContext as unknown as Record<string, boolean>
|
|
93
|
-
});
|
|
94
|
-
|
|
95
|
-
assert.ok(context.appNamePascal.includes('Awesome'));
|
|
96
|
-
assert.ok(context.packagePath.includes('/'));
|
|
97
|
-
});
|
|
98
|
-
});
|
|
99
|
-
|
|
100
|
-
test('Context - Validate Context', async (t) => {
|
|
101
|
-
await t.test('should accept valid context', () => {
|
|
102
|
-
const baseContext = buildDefaultProjectContext(
|
|
103
|
-
'MyApp',
|
|
104
|
-
'com.example.myapp',
|
|
105
|
-
'./my-project',
|
|
106
|
-
'kotlin-compose' as TemplateType
|
|
107
|
-
);
|
|
108
|
-
|
|
109
|
-
const result = validateContext(baseContext);
|
|
110
|
-
assert.strictEqual(result.valid, true);
|
|
111
|
-
});
|
|
112
|
-
|
|
113
|
-
await t.test('should reject context with missing appName', () => {
|
|
114
|
-
const result = validateContext({
|
|
115
|
-
appName: '',
|
|
116
|
-
packageName: 'com.example.myapp',
|
|
117
|
-
template: 'kotlin-compose' as TemplateType
|
|
118
|
-
});
|
|
119
|
-
|
|
120
|
-
assert.strictEqual(result.valid, false);
|
|
121
|
-
assert.ok(result.errors.length > 0);
|
|
122
|
-
});
|
|
123
|
-
|
|
124
|
-
await t.test('should reject context with missing packageName', () => {
|
|
125
|
-
const result = validateContext({
|
|
126
|
-
appName: 'MyApp',
|
|
127
|
-
packageName: '',
|
|
128
|
-
template: 'kotlin-compose' as TemplateType
|
|
129
|
-
});
|
|
130
|
-
|
|
131
|
-
assert.strictEqual(result.valid, false);
|
|
132
|
-
});
|
|
133
|
-
});
|
|
@@ -1,107 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Test suite for generator utilities
|
|
3
|
-
*/
|
|
4
|
-
|
|
5
|
-
import { test } from 'node:test';
|
|
6
|
-
import assert from 'node:assert';
|
|
7
|
-
import {
|
|
8
|
-
validateContext,
|
|
9
|
-
validateProjectDirectory
|
|
10
|
-
} from '../core/generator.js';
|
|
11
|
-
import type { TemplateContext } from '../core/types.js';
|
|
12
|
-
|
|
13
|
-
test('Generator - Validate Context', async (t) => {
|
|
14
|
-
await t.test('should accept valid context', () => {
|
|
15
|
-
const context: Partial<TemplateContext> = {
|
|
16
|
-
appName: 'MyApp',
|
|
17
|
-
packageName: 'com.example.myapp',
|
|
18
|
-
projectDirectory: './my-project',
|
|
19
|
-
template: 'kotlin-compose',
|
|
20
|
-
language: 'kotlin',
|
|
21
|
-
uiFramework: 'compose',
|
|
22
|
-
android: {
|
|
23
|
-
minSdk: 31,
|
|
24
|
-
targetSdk: 36,
|
|
25
|
-
compileSdk: 36
|
|
26
|
-
},
|
|
27
|
-
gradle: {
|
|
28
|
-
agpVersion: '8.7.3',
|
|
29
|
-
gradleVersion: '8.14',
|
|
30
|
-
kotlinVersion: '2.0.21'
|
|
31
|
-
}
|
|
32
|
-
};
|
|
33
|
-
|
|
34
|
-
const result = validateContext(context);
|
|
35
|
-
assert.strictEqual(result.valid, true);
|
|
36
|
-
});
|
|
37
|
-
|
|
38
|
-
await t.test('should reject context with missing appName', () => {
|
|
39
|
-
const context: Partial<TemplateContext> = {
|
|
40
|
-
appName: '',
|
|
41
|
-
packageName: 'com.example.myapp'
|
|
42
|
-
};
|
|
43
|
-
|
|
44
|
-
const result = validateContext(context);
|
|
45
|
-
assert.strictEqual(result.valid, false);
|
|
46
|
-
assert.ok(result.errors.some(e => e.includes('appName')));
|
|
47
|
-
});
|
|
48
|
-
|
|
49
|
-
await t.test('should reject context with missing packageName', () => {
|
|
50
|
-
const context: Partial<TemplateContext> = {
|
|
51
|
-
appName: 'MyApp',
|
|
52
|
-
packageName: ''
|
|
53
|
-
};
|
|
54
|
-
|
|
55
|
-
const result = validateContext(context);
|
|
56
|
-
assert.strictEqual(result.valid, false);
|
|
57
|
-
assert.ok(result.errors.some(e => e.includes('packageName')));
|
|
58
|
-
});
|
|
59
|
-
|
|
60
|
-
await t.test('should reject context with missing template', () => {
|
|
61
|
-
const context: Partial<TemplateContext> = {
|
|
62
|
-
appName: 'MyApp',
|
|
63
|
-
packageName: 'com.example.myapp',
|
|
64
|
-
template: undefined
|
|
65
|
-
};
|
|
66
|
-
|
|
67
|
-
const result = validateContext(context);
|
|
68
|
-
assert.strictEqual(result.valid, false);
|
|
69
|
-
assert.ok(result.errors.some(e => e.includes('template')));
|
|
70
|
-
});
|
|
71
|
-
|
|
72
|
-
await t.test('should reject context with missing language', () => {
|
|
73
|
-
const context: Partial<TemplateContext> = {
|
|
74
|
-
appName: 'MyApp',
|
|
75
|
-
packageName: 'com.example.myapp',
|
|
76
|
-
template: 'kotlin-compose',
|
|
77
|
-
language: undefined
|
|
78
|
-
};
|
|
79
|
-
|
|
80
|
-
const result = validateContext(context);
|
|
81
|
-
assert.strictEqual(result.valid, false);
|
|
82
|
-
assert.ok(result.errors.some(e => e.includes('language')));
|
|
83
|
-
});
|
|
84
|
-
});
|
|
85
|
-
|
|
86
|
-
test('Generator - Validate Project Directory', async (t) => {
|
|
87
|
-
await t.test('should validate non-existent directory', async () => {
|
|
88
|
-
const result = await validateProjectDirectory(
|
|
89
|
-
'/tmp/nonexistent-dir-' + Date.now(),
|
|
90
|
-
{ overwrite: false }
|
|
91
|
-
);
|
|
92
|
-
|
|
93
|
-
assert.strictEqual(result.valid, true);
|
|
94
|
-
});
|
|
95
|
-
|
|
96
|
-
await t.test('should allow non-empty directory with overwrite', async () => {
|
|
97
|
-
// Note: This would require actual file system operations
|
|
98
|
-
// In a real test, we'd mock the filesystem
|
|
99
|
-
const result = await validateProjectDirectory(
|
|
100
|
-
'/tmp/test-dir',
|
|
101
|
-
{ overwrite: true }
|
|
102
|
-
);
|
|
103
|
-
|
|
104
|
-
// Just assert the result has the expected shape
|
|
105
|
-
assert.ok(typeof result.valid === 'boolean');
|
|
106
|
-
});
|
|
107
|
-
});
|
|
@@ -1,105 +0,0 @@
|
|
|
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
|
-
});
|
|
@@ -1,252 +0,0 @@
|
|
|
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
|
-
}
|