@ecc-hgy/ae 0.4.0 → 0.6.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.
Files changed (49) hide show
  1. package/LICENSE +21 -21
  2. package/README.md +168 -138
  3. package/bin/ae.js +2 -2
  4. package/package.json +43 -43
  5. package/skills/brainstorming/SKILL.md +133 -133
  6. package/skills/diagnose/SKILL.md +146 -146
  7. package/skills/diagnose/assets/issue-7-sections.md +35 -35
  8. package/skills/diagnose/scripts/hitl-loop.template.sh +41 -41
  9. package/skills/grill-me/SKILL.md +10 -10
  10. package/skills/handoff/SKILL.md +19 -19
  11. package/skills/improve-codebase-architecture/DEEPENING.md +37 -37
  12. package/skills/improve-codebase-architecture/HTML-REPORT.md +123 -123
  13. package/skills/improve-codebase-architecture/INTERFACE-DESIGN.md +44 -44
  14. package/skills/improve-codebase-architecture/LANGUAGE.md +53 -53
  15. package/skills/improve-codebase-architecture/SKILL.md +101 -101
  16. package/skills/karpathy-guidelines/SKILL.md +63 -63
  17. package/skills/powerautomate-email-to-sharepoint-excel/powerautomate-email-to-sharepoint-excel-skill.md +496 -0
  18. package/skills/review/SKILL.md +119 -119
  19. package/skills/tdd/SKILL.md +157 -157
  20. package/skills/tdd/deep-modules.md +33 -33
  21. package/skills/tdd/interface-design.md +31 -31
  22. package/skills/tdd/mocking.md +59 -59
  23. package/skills/tdd/refactoring.md +10 -10
  24. package/skills/tdd/tests.md +61 -61
  25. package/skills/to-issues/SKILL.md +79 -79
  26. package/skills/to-issues/todo-template.md +25 -25
  27. package/skills/to-prd/SKILL.md +108 -108
  28. package/skills/using-agentic-engineering/SKILL.md +62 -62
  29. package/skills/verification-before-completion/SKILL.md +153 -153
  30. package/skills/writing-plans/SKILL.md +115 -115
  31. package/skills/zoom-out/SKILL.md +7 -7
  32. package/src/cli.js +61 -61
  33. package/src/commands/init.js +137 -134
  34. package/src/commands/setup.js +162 -103
  35. package/src/platforms.js +132 -0
  36. package/src/skeleton.js +134 -99
  37. package/src/utils/copy.js +100 -100
  38. package/src/utils/paths.js +60 -60
  39. package/src/utils/report.js +30 -30
  40. package/templates/entries/AGENTS.md +2 -0
  41. package/templates/entries/CLAUDE.md +5 -5
  42. package/templates/entries/README.md +33 -31
  43. package/templates/entries/handoff.md +1 -1
  44. package/templates/entries/spec/ADR/AGENTS.md +30 -30
  45. package/templates/entries/spec/ADR/CLAUDE.md +5 -5
  46. package/templates/entries/spec/AGENTS.md +34 -34
  47. package/templates/entries/spec/CLAUDE.md +5 -5
  48. package/templates/entries/spec/INDEX.md +28 -28
  49. package/templates/entries/spec/README.md +23 -23
@@ -1,103 +1,162 @@
1
- import { mkdir, stat } from 'node:fs/promises';
2
- import path from 'node:path';
3
- import { copyTreeIdempotent } from '../utils/copy.js';
4
- import { skillsPath } from '../utils/paths.js';
5
- import { formatSetupReport } from '../utils/report.js';
6
-
7
- const HELP = `Usage: ae setup [options]
8
-
9
- Options:
10
- --target <path> Install AE assets into this project directory. Defaults to cwd.
11
- --dry-run Show what would be installed without writing files.
12
- -h, --help Show help`;
13
-
14
- export async function run(argv = []) {
15
- const options = parseArgs(argv);
16
-
17
- if (options.help) {
18
- console.log(HELP);
19
- return 0;
20
- }
21
-
22
- const target = path.resolve(options.target ?? process.cwd());
23
-
24
- try {
25
- await ensureTargetDirectory(target, options.dryRun);
26
-
27
- const created = [];
28
- const skipped = [];
29
- const skillGroups = [
30
- {
31
- dest: path.join(target, '.claude', 'skills'),
32
- prefix: path.join('.claude', 'skills'),
33
- },
34
- {
35
- dest: path.join(target, '.codex', 'skills'),
36
- prefix: path.join('.codex', 'skills'),
37
- },
38
- ];
39
-
40
- for (const group of skillGroups) {
41
- const result = await copyTreeIdempotent(skillsPath(), group.dest, {
42
- dryRun: options.dryRun,
43
- });
44
- created.push(...result.created.map((item) => toDisplayPath(path.join(group.prefix, item))));
45
- skipped.push(...result.skipped.map((item) => toDisplayPath(path.join(group.prefix, item))));
46
- }
47
-
48
- console.log(formatSetupReport({ created, skipped, target, dryRun: options.dryRun }));
49
- return 0;
50
- } catch (error) {
51
- console.error(`AE setup failed: ${error instanceof Error ? error.message : String(error)}`);
52
- process.exitCode = 1;
53
- return 1;
54
- }
55
- }
56
-
57
- function parseArgs(argv) {
58
- const options = {
59
- target: undefined,
60
- dryRun: false,
61
- help: false,
62
- };
63
-
64
- for (let index = 0; index < argv.length; index += 1) {
65
- const arg = argv[index];
66
- if (arg === '--help' || arg === '-h') {
67
- options.help = true;
68
- } else if (arg === '--dry-run') {
69
- options.dryRun = true;
70
- } else if (arg === '--target') {
71
- const value = argv[index + 1];
72
- if (!value) {
73
- throw new Error('--target requires a path');
74
- }
75
- options.target = value;
76
- index += 1;
77
- } else {
78
- throw new Error(`Unknown option for setup: ${arg}`);
79
- }
80
- }
81
-
82
- return options;
83
- }
84
-
85
- async function ensureTargetDirectory(target, dryRun) {
86
- try {
87
- const current = await stat(target);
88
- if (!current.isDirectory()) {
89
- throw new Error(`target exists but is not a directory: ${target}`);
90
- }
91
- } catch (error) {
92
- if (error?.code !== 'ENOENT') {
93
- throw error;
94
- }
95
- if (!dryRun) {
96
- await mkdir(target, { recursive: true });
97
- }
98
- }
99
- }
100
-
101
- function toDisplayPath(value) {
102
- return value.split(path.sep).join('/');
103
- }
1
+ import { mkdir, stat } from 'node:fs/promises';
2
+ import path from 'node:path';
3
+ import { createInterface } from 'node:readline/promises';
4
+ import { copyTreeIdempotent } from '../utils/copy.js';
5
+ import { skillsPath } from '../utils/paths.js';
6
+ import { formatSetupReport } from '../utils/report.js';
7
+ import {
8
+ PLATFORMS,
9
+ detectPlatforms,
10
+ getPlatformSkillsDir,
11
+ listPlatformIds,
12
+ parsePlatformList,
13
+ resolvePlatforms,
14
+ } from '../platforms.js';
15
+
16
+ const HELP = `Usage: ae setup [options]
17
+
18
+ Options:
19
+ --target <path> Install AE assets into this project directory. Defaults to cwd.
20
+ --platform <ids> Install for comma-separated platforms, e.g. claude,codex,cursor.
21
+ --dry-run Show what would be installed without writing files.
22
+ -h, --help Show help
23
+
24
+ Platforms:
25
+ ${listPlatformIds().join(', ')}`;
26
+
27
+ export async function run(argv = []) {
28
+ const options = parseArgs(argv);
29
+
30
+ if (options.help) {
31
+ console.log(HELP);
32
+ return 0;
33
+ }
34
+
35
+ const target = path.resolve(options.target ?? process.cwd());
36
+
37
+ try {
38
+ const explicitPlatforms =
39
+ options.platformIds.length > 0 ? resolvePlatforms(options.platformIds) : undefined;
40
+ await ensureTargetDirectory(target, options.dryRun);
41
+ const selectedPlatforms = explicitPlatforms ?? (await selectPlatforms(target));
42
+
43
+ const created = [];
44
+ const skipped = [];
45
+ const skillGroups = selectedPlatforms.map((platform) => {
46
+ const skillsDir = getPlatformSkillsDir(platform);
47
+ return {
48
+ dest: path.join(target, skillsDir),
49
+ prefix: skillsDir,
50
+ };
51
+ });
52
+
53
+ for (const group of skillGroups) {
54
+ const result = await copyTreeIdempotent(skillsPath(), group.dest, {
55
+ dryRun: options.dryRun,
56
+ });
57
+ created.push(...result.created.map((item) => toDisplayPath(path.join(group.prefix, item))));
58
+ skipped.push(...result.skipped.map((item) => toDisplayPath(path.join(group.prefix, item))));
59
+ }
60
+
61
+ console.log(formatSetupReport({ created, skipped, target, dryRun: options.dryRun }));
62
+ return 0;
63
+ } catch (error) {
64
+ console.error(`AE setup failed: ${error instanceof Error ? error.message : String(error)}`);
65
+ process.exitCode = 1;
66
+ return 1;
67
+ }
68
+ }
69
+
70
+ function parseArgs(argv) {
71
+ const options = {
72
+ target: undefined,
73
+ platformIds: [],
74
+ dryRun: false,
75
+ help: false,
76
+ };
77
+
78
+ for (let index = 0; index < argv.length; index += 1) {
79
+ const arg = argv[index];
80
+ if (arg === '--help' || arg === '-h') {
81
+ options.help = true;
82
+ } else if (arg === '--dry-run') {
83
+ options.dryRun = true;
84
+ } else if (arg === '--platform') {
85
+ const value = argv[index + 1];
86
+ if (!value) {
87
+ throw new Error('--platform requires a comma-separated platform list');
88
+ }
89
+ options.platformIds.push(...parsePlatformList(value));
90
+ index += 1;
91
+ } else if (arg === '--target') {
92
+ const value = argv[index + 1];
93
+ if (!value) {
94
+ throw new Error('--target requires a path');
95
+ }
96
+ options.target = value;
97
+ index += 1;
98
+ } else {
99
+ throw new Error(`Unknown option for setup: ${arg}`);
100
+ }
101
+ }
102
+
103
+ return options;
104
+ }
105
+
106
+ async function selectPlatforms(target) {
107
+ if (!process.stdin.isTTY || !process.stdout.isTTY) {
108
+ throw new Error(
109
+ `No platforms selected. In non-interactive mode, pass --platform <ids>. Available: ${listPlatformIds().join(', ')}`,
110
+ );
111
+ }
112
+
113
+ const detected = await detectPlatforms(target);
114
+ const detectedIds = detected.map((platform) => platform.id);
115
+ const defaultIds = detectedIds;
116
+
117
+ console.log('Select target platforms for AE skills:');
118
+ for (const [index, platform] of PLATFORMS.entries()) {
119
+ const marker = defaultIds.includes(platform.id) ? '*' : ' ';
120
+ console.log(` ${index + 1}. [${marker}] ${platform.name} (${platform.id}) -> ${getPlatformSkillsDir(platform)}/`);
121
+ }
122
+
123
+ const defaultPrompt = defaultIds.length > 0 ? ` [${defaultIds.join(',')}]` : '';
124
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
125
+ try {
126
+ const answer = await rl.question(`Platform ids or numbers, comma-separated${defaultPrompt}: `);
127
+ const selected = answer.trim() ? parseInteractivePlatformAnswer(answer) : defaultIds;
128
+ return resolvePlatforms(selected);
129
+ } finally {
130
+ rl.close();
131
+ }
132
+ }
133
+
134
+ function parseInteractivePlatformAnswer(answer) {
135
+ return parsePlatformList(answer).map((item) => {
136
+ const number = Number(item);
137
+ if (Number.isInteger(number) && number >= 1 && number <= PLATFORMS.length) {
138
+ return PLATFORMS[number - 1].id;
139
+ }
140
+ return item;
141
+ });
142
+ }
143
+
144
+ async function ensureTargetDirectory(target, dryRun) {
145
+ try {
146
+ const current = await stat(target);
147
+ if (!current.isDirectory()) {
148
+ throw new Error(`target exists but is not a directory: ${target}`);
149
+ }
150
+ } catch (error) {
151
+ if (error?.code !== 'ENOENT') {
152
+ throw error;
153
+ }
154
+ if (!dryRun) {
155
+ await mkdir(target, { recursive: true });
156
+ }
157
+ }
158
+ }
159
+
160
+ function toDisplayPath(value) {
161
+ return value.split(path.sep).join('/');
162
+ }
@@ -0,0 +1,132 @@
1
+ import { stat } from 'node:fs/promises';
2
+ import path from 'node:path';
3
+
4
+ export const PLATFORMS = [
5
+ { id: 'claude', name: 'Claude Code', skillsDir: '.claude', aliases: ['claude-code'] },
6
+ { id: 'codex', name: 'Codex', skillsDir: '.codex' },
7
+ { id: 'windsurf', name: 'Windsurf', skillsDir: '.windsurf' },
8
+ {
9
+ id: 'github-copilot',
10
+ name: 'GitHub Copilot',
11
+ skillsDir: '.github',
12
+ aliases: ['copilot', 'github'],
13
+ detectionPaths: [
14
+ '.github/copilot-instructions.md',
15
+ '.github/instructions',
16
+ '.github/prompts',
17
+ '.github/skills',
18
+ ],
19
+ },
20
+ { id: 'kilocode', name: 'Kilo Code', skillsDir: '.kilocode', aliases: ['kilo-code'] },
21
+ { id: 'kimicode', name: 'Kimi Code', skillsDir: '.kimi-code', aliases: ['kimi', 'kimi-code'] },
22
+ { id: 'codebuddy', name: 'CodeBuddy', skillsDir: '.codebuddy', aliases: ['codebuddy-code'] },
23
+ { id: 'qoder', name: 'Qoder', skillsDir: '.qoder' },
24
+ { id: 'trae', name: 'Trae', skillsDir: '.trae' },
25
+ { id: 'cursor', name: 'Cursor', skillsDir: '.cursor' },
26
+ { id: 'opencode', name: 'OpenCode', skillsDir: '.opencode', aliases: ['open-code'] },
27
+ { id: 'antigravity', name: 'Antigravity', skillsDir: '.agents' },
28
+ { id: 'gemini', name: 'Gemini CLI', skillsDir: '.gemini', aliases: ['gemini-cli'] },
29
+ { id: 'qwen', name: 'Qwen Code', skillsDir: '.qwen', aliases: ['qwen-code'] },
30
+ { id: 'kiro', name: 'Kiro', skillsDir: '.kiro' },
31
+ ];
32
+
33
+ const PLATFORM_BY_ID_OR_ALIAS = new Map(
34
+ PLATFORMS.flatMap((platform) => [
35
+ [platform.id, platform],
36
+ ...(platform.aliases ?? []).map((alias) => [alias, platform]),
37
+ ]),
38
+ );
39
+
40
+ export function listPlatformIds() {
41
+ return PLATFORMS.map((platform) => platform.id);
42
+ }
43
+
44
+ export function resolvePlatforms(ids) {
45
+ const resolved = [];
46
+ const seen = new Set();
47
+ const unknown = [];
48
+
49
+ for (const rawId of ids) {
50
+ const id = rawId.trim().toLowerCase();
51
+ if (!id) {
52
+ continue;
53
+ }
54
+
55
+ const platform = PLATFORM_BY_ID_OR_ALIAS.get(id);
56
+ if (!platform) {
57
+ unknown.push(rawId);
58
+ continue;
59
+ }
60
+
61
+ if (!seen.has(platform.id)) {
62
+ resolved.push(platform);
63
+ seen.add(platform.id);
64
+ }
65
+ }
66
+
67
+ if (unknown.length > 0) {
68
+ throw new Error(`Unknown platform(s): ${unknown.join(', ')}. Available: ${listPlatformIds().join(', ')}`);
69
+ }
70
+
71
+ if (resolved.length === 0) {
72
+ throw new Error(`No platforms selected. Available: ${listPlatformIds().join(', ')}`);
73
+ }
74
+
75
+ return resolved;
76
+ }
77
+
78
+ export function parsePlatformList(value) {
79
+ return value
80
+ .split(',')
81
+ .map((item) => item.trim())
82
+ .filter(Boolean);
83
+ }
84
+
85
+ export function getPlatformSkillsDir(platform) {
86
+ return path.join(platform.skillsDir, 'skills');
87
+ }
88
+
89
+ export async function detectPlatforms(target) {
90
+ const detected = [];
91
+
92
+ for (const platform of PLATFORMS) {
93
+ if (platform.detectionPaths?.length > 0) {
94
+ for (const detectionPath of platform.detectionPaths) {
95
+ if (await pathExists(path.join(target, detectionPath))) {
96
+ detected.push(platform);
97
+ break;
98
+ }
99
+ }
100
+ continue;
101
+ }
102
+
103
+ if (await isDirectory(path.join(target, platform.skillsDir))) {
104
+ detected.push(platform);
105
+ }
106
+ }
107
+
108
+ return detected;
109
+ }
110
+
111
+ async function pathExists(value) {
112
+ try {
113
+ await stat(value);
114
+ return true;
115
+ } catch (error) {
116
+ if (error?.code === 'ENOENT') {
117
+ return false;
118
+ }
119
+ throw error;
120
+ }
121
+ }
122
+
123
+ async function isDirectory(value) {
124
+ try {
125
+ return (await stat(value)).isDirectory();
126
+ } catch (error) {
127
+ if (error?.code === 'ENOENT') {
128
+ return false;
129
+ }
130
+ throw error;
131
+ }
132
+ }
package/src/skeleton.js CHANGED
@@ -1,129 +1,164 @@
1
- import { mkdir, readFile, stat, writeFile } from 'node:fs/promises';
2
- import path from 'node:path';
3
-
4
- const DEFAULT_DRY_RUN = false;
5
-
6
- const SKELETON_DIRS = [
1
+ import { mkdir, readFile, stat, writeFile } from 'node:fs/promises';
2
+ import path from 'node:path';
3
+
4
+ const DEFAULT_DRY_RUN = false;
5
+
6
+ const CORE_DIRS = [
7
7
  'spec',
8
8
  path.join('spec', 'needs'),
9
9
  path.join('spec', 'ADR'),
10
- 'reference',
10
+ 'docs',
11
11
  'data',
12
- 'scripts',
13
12
  ];
14
13
 
15
- const GITKEEP_DIRS = [
14
+ const CORE_GITKEEP_DIRS = [
16
15
  path.join('spec', 'needs'),
17
16
  path.join('spec', 'ADR'),
18
- 'reference',
17
+ 'docs',
19
18
  'data',
20
- 'scripts',
21
- ];
22
-
23
- const ENTRY_FILES = [
24
- 'README.md',
25
- 'AGENTS.md',
26
- 'CLAUDE.md',
27
- 'handoff.md',
28
- path.join('spec', 'README.md'),
29
- path.join('spec', 'INDEX.md'),
30
- path.join('spec', 'AGENTS.md'),
31
- path.join('spec', 'CLAUDE.md'),
32
- path.join('spec', 'ADR', 'AGENTS.md'),
33
- path.join('spec', 'ADR', 'CLAUDE.md'),
34
19
  ];
35
20
 
21
+ const ALTERNATE_SOURCE_ROOTS = ['app', 'apps', 'lib', 'packages'];
22
+ const ALTERNATE_TEST_ROOTS = ['test', '__tests__'];
23
+
24
+ const ENTRY_FILES = [
25
+ 'README.md',
26
+ 'AGENTS.md',
27
+ 'CLAUDE.md',
28
+ 'handoff.md',
29
+ path.join('spec', 'README.md'),
30
+ path.join('spec', 'INDEX.md'),
31
+ path.join('spec', 'AGENTS.md'),
32
+ path.join('spec', 'CLAUDE.md'),
33
+ path.join('spec', 'ADR', 'AGENTS.md'),
34
+ path.join('spec', 'ADR', 'CLAUDE.md'),
35
+ ];
36
+
36
37
  export async function createDirectories(target, options = {}) {
37
38
  const dryRun = Boolean(options.dryRun ?? DEFAULT_DRY_RUN);
38
39
  const created = [];
39
40
  const skipped = [];
40
41
  const updated = [];
42
+ const skeletonDirs = [...CORE_DIRS];
43
+ const gitkeepDirs = [...CORE_GITKEEP_DIRS];
41
44
 
42
- for (const dir of SKELETON_DIRS) {
43
- const absolute = path.join(target, dir);
44
- const display = `${normalizeRelative(dir)}/`;
45
-
46
- if (await exists(absolute)) {
47
- const current = await stat(absolute);
48
- if (!current.isDirectory()) {
49
- throw new Error(`expected directory but found file: ${absolute}`);
50
- }
51
- skipped.push(display);
52
- } else {
53
- created.push(display);
54
- if (!dryRun) {
55
- await mkdir(absolute, { recursive: true });
56
- }
57
- }
45
+ if (await shouldIncludeDefaultRoot(target, 'src', ALTERNATE_SOURCE_ROOTS)) {
46
+ skeletonDirs.push('src');
47
+ gitkeepDirs.push('src');
58
48
  }
59
49
 
60
- for (const dir of GITKEEP_DIRS) {
61
- const file = path.join(target, dir, '.gitkeep');
62
- const display = normalizeRelative(path.join(dir, '.gitkeep'));
63
-
64
- if (await exists(file)) {
65
- skipped.push(display);
66
- } else {
67
- created.push(display);
68
- if (!dryRun) {
69
- await mkdir(path.dirname(file), { recursive: true });
70
- await writeFile(file, '');
71
- }
72
- }
50
+ if (await shouldIncludeDefaultRoot(target, 'tests', ALTERNATE_TEST_ROOTS)) {
51
+ skeletonDirs.push('tests');
52
+ gitkeepDirs.push('tests');
73
53
  }
74
54
 
75
- return { created, skipped, updated };
55
+ for (const dir of skeletonDirs) {
56
+ const absolute = path.join(target, dir);
57
+ const display = `${normalizeRelative(dir)}/`;
58
+
59
+ if (await exists(absolute)) {
60
+ const current = await stat(absolute);
61
+ if (!current.isDirectory()) {
62
+ throw new Error(`expected directory but found file: ${absolute}`);
63
+ }
64
+ skipped.push(display);
65
+ } else {
66
+ created.push(display);
67
+ if (!dryRun) {
68
+ await mkdir(absolute, { recursive: true });
69
+ }
70
+ }
71
+ }
72
+
73
+ for (const dir of gitkeepDirs) {
74
+ const file = path.join(target, dir, '.gitkeep');
75
+ const display = normalizeRelative(path.join(dir, '.gitkeep'));
76
+
77
+ if (await exists(file)) {
78
+ skipped.push(display);
79
+ } else {
80
+ created.push(display);
81
+ if (!dryRun) {
82
+ await mkdir(path.dirname(file), { recursive: true });
83
+ await writeFile(file, '');
84
+ }
85
+ }
86
+ }
87
+
88
+ return { created, skipped, updated };
89
+ }
90
+
91
+ export async function renderEntryFiles(target, templateRoot, options = {}) {
92
+ const dryRun = Boolean(options.dryRun ?? DEFAULT_DRY_RUN);
93
+ const projectName = options.projectName ?? path.basename(target);
94
+ const projectGoal = options.projectGoal ?? 'TODO: 用一句话说明这个项目要解决什么问题。';
95
+ const created = [];
96
+ const skipped = [];
97
+ const updated = [];
98
+
99
+ for (const file of ENTRY_FILES) {
100
+ const source = path.join(templateRoot, file);
101
+ const dest = path.join(target, file);
102
+ const display = normalizeRelative(file);
103
+
104
+ if (await exists(dest)) {
105
+ skipped.push(display);
106
+ continue;
107
+ }
108
+
109
+ const template = await readFile(source, 'utf8');
110
+ const rendered = renderTemplate(template, { projectName, projectGoal });
111
+ created.push(display);
112
+
113
+ if (!dryRun) {
114
+ await mkdir(path.dirname(dest), { recursive: true });
115
+ await writeFile(dest, rendered);
116
+ }
117
+ }
118
+
119
+ return { created, skipped, updated };
120
+ }
121
+
122
+ function renderTemplate(template, replacements) {
123
+ return template
124
+ .replaceAll('{{PROJECT_NAME}}', replacements.projectName)
125
+ .replaceAll('{{PROJECT_GOAL}}', replacements.projectGoal);
126
+ }
127
+
128
+ async function exists(value) {
129
+ try {
130
+ await stat(value);
131
+ return true;
132
+ } catch (error) {
133
+ if (error?.code === 'ENOENT') {
134
+ return false;
135
+ }
136
+ throw error;
137
+ }
76
138
  }
77
139
 
78
- export async function renderEntryFiles(target, templateRoot, options = {}) {
79
- const dryRun = Boolean(options.dryRun ?? DEFAULT_DRY_RUN);
80
- const projectName = options.projectName ?? path.basename(target);
81
- const projectGoal = options.projectGoal ?? 'TODO: 用一句话说明这个项目要解决什么问题。';
82
- const created = [];
83
- const skipped = [];
84
- const updated = [];
85
-
86
- for (const file of ENTRY_FILES) {
87
- const source = path.join(templateRoot, file);
88
- const dest = path.join(target, file);
89
- const display = normalizeRelative(file);
90
-
91
- if (await exists(dest)) {
92
- skipped.push(display);
93
- continue;
94
- }
95
-
96
- const template = await readFile(source, 'utf8');
97
- const rendered = renderTemplate(template, { projectName, projectGoal });
98
- created.push(display);
99
-
100
- if (!dryRun) {
101
- await mkdir(path.dirname(dest), { recursive: true });
102
- await writeFile(dest, rendered);
140
+ async function hasDirectory(target, candidates) {
141
+ for (const candidate of candidates) {
142
+ try {
143
+ if ((await stat(path.join(target, candidate))).isDirectory()) {
144
+ return true;
145
+ }
146
+ } catch (error) {
147
+ if (error?.code !== 'ENOENT') {
148
+ throw error;
149
+ }
103
150
  }
104
151
  }
105
-
106
- return { created, skipped, updated };
107
- }
108
-
109
- function renderTemplate(template, replacements) {
110
- return template
111
- .replaceAll('{{PROJECT_NAME}}', replacements.projectName)
112
- .replaceAll('{{PROJECT_GOAL}}', replacements.projectGoal);
152
+ return false;
113
153
  }
114
154
 
115
- async function exists(value) {
116
- try {
117
- await stat(value);
155
+ async function shouldIncludeDefaultRoot(target, defaultRoot, alternatives) {
156
+ if (await exists(path.join(target, defaultRoot))) {
118
157
  return true;
119
- } catch (error) {
120
- if (error?.code === 'ENOENT') {
121
- return false;
122
- }
123
- throw error;
124
158
  }
159
+ return !(await hasDirectory(target, alternatives));
125
160
  }
126
-
127
- function normalizeRelative(value) {
128
- return value.split(path.sep).join('/');
129
- }
161
+
162
+ function normalizeRelative(value) {
163
+ return value.split(path.sep).join('/');
164
+ }