@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.
- package/LICENSE +21 -21
- package/README.md +168 -138
- package/bin/ae.js +2 -2
- package/package.json +43 -43
- package/skills/brainstorming/SKILL.md +133 -133
- package/skills/diagnose/SKILL.md +146 -146
- package/skills/diagnose/assets/issue-7-sections.md +35 -35
- package/skills/diagnose/scripts/hitl-loop.template.sh +41 -41
- package/skills/grill-me/SKILL.md +10 -10
- package/skills/handoff/SKILL.md +19 -19
- package/skills/improve-codebase-architecture/DEEPENING.md +37 -37
- package/skills/improve-codebase-architecture/HTML-REPORT.md +123 -123
- package/skills/improve-codebase-architecture/INTERFACE-DESIGN.md +44 -44
- package/skills/improve-codebase-architecture/LANGUAGE.md +53 -53
- package/skills/improve-codebase-architecture/SKILL.md +101 -101
- package/skills/karpathy-guidelines/SKILL.md +63 -63
- package/skills/powerautomate-email-to-sharepoint-excel/powerautomate-email-to-sharepoint-excel-skill.md +496 -0
- package/skills/review/SKILL.md +119 -119
- package/skills/tdd/SKILL.md +157 -157
- package/skills/tdd/deep-modules.md +33 -33
- package/skills/tdd/interface-design.md +31 -31
- package/skills/tdd/mocking.md +59 -59
- package/skills/tdd/refactoring.md +10 -10
- package/skills/tdd/tests.md +61 -61
- package/skills/to-issues/SKILL.md +79 -79
- package/skills/to-issues/todo-template.md +25 -25
- package/skills/to-prd/SKILL.md +108 -108
- package/skills/using-agentic-engineering/SKILL.md +62 -62
- package/skills/verification-before-completion/SKILL.md +153 -153
- package/skills/writing-plans/SKILL.md +115 -115
- package/skills/zoom-out/SKILL.md +7 -7
- package/src/cli.js +61 -61
- package/src/commands/init.js +137 -134
- package/src/commands/setup.js +162 -103
- package/src/platforms.js +132 -0
- package/src/skeleton.js +134 -99
- package/src/utils/copy.js +100 -100
- package/src/utils/paths.js +60 -60
- package/src/utils/report.js +30 -30
- package/templates/entries/AGENTS.md +2 -0
- package/templates/entries/CLAUDE.md +5 -5
- package/templates/entries/README.md +33 -31
- package/templates/entries/handoff.md +1 -1
- package/templates/entries/spec/ADR/AGENTS.md +30 -30
- package/templates/entries/spec/ADR/CLAUDE.md +5 -5
- package/templates/entries/spec/AGENTS.md +34 -34
- package/templates/entries/spec/CLAUDE.md +5 -5
- package/templates/entries/spec/INDEX.md +28 -28
- package/templates/entries/spec/README.md +23 -23
package/src/commands/setup.js
CHANGED
|
@@ -1,103 +1,162 @@
|
|
|
1
|
-
import { mkdir, stat } from 'node:fs/promises';
|
|
2
|
-
import path from 'node:path';
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
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
|
+
}
|
package/src/platforms.js
ADDED
|
@@ -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
|
|
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
|
-
'
|
|
10
|
+
'docs',
|
|
11
11
|
'data',
|
|
12
|
-
'scripts',
|
|
13
12
|
];
|
|
14
13
|
|
|
15
|
-
const
|
|
14
|
+
const CORE_GITKEEP_DIRS = [
|
|
16
15
|
path.join('spec', 'needs'),
|
|
17
16
|
path.join('spec', 'ADR'),
|
|
18
|
-
'
|
|
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
|
-
|
|
43
|
-
|
|
44
|
-
|
|
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
|
-
|
|
61
|
-
|
|
62
|
-
|
|
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
|
-
|
|
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
|
-
|
|
79
|
-
const
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
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
|
|
116
|
-
|
|
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
|
+
}
|