@ecc-hgy/ae 0.5.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 -155
- 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 -496
- 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 -137
- package/src/commands/setup.js +162 -162
- package/src/platforms.js +132 -132
- 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/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
|
+
}
|
package/src/utils/copy.js
CHANGED
|
@@ -1,100 +1,100 @@
|
|
|
1
|
-
import { copyFile, chmod, mkdir, readdir, stat, writeFile } from 'node:fs/promises';
|
|
2
|
-
import path from 'node:path';
|
|
3
|
-
|
|
4
|
-
export async function copyTreeIdempotent(srcDir, destDir, options = {}) {
|
|
5
|
-
const dryRun = Boolean(options.dryRun);
|
|
6
|
-
const created = [];
|
|
7
|
-
const skipped = [];
|
|
8
|
-
|
|
9
|
-
const source = await stat(srcDir);
|
|
10
|
-
if (!source.isDirectory()) {
|
|
11
|
-
throw new Error(`source is not a directory: ${srcDir}`);
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
await walk(srcDir, '');
|
|
15
|
-
|
|
16
|
-
return { created, skipped };
|
|
17
|
-
|
|
18
|
-
async function walk(currentSourceDir, relativeDir) {
|
|
19
|
-
const entries = await readdir(currentSourceDir, { withFileTypes: true });
|
|
20
|
-
const currentDestDir = path.join(destDir, relativeDir);
|
|
21
|
-
|
|
22
|
-
if (!dryRun) {
|
|
23
|
-
await mkdir(currentDestDir, { recursive: true });
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
if (entries.length === 0) {
|
|
27
|
-
const marker = normalizeRelative(path.join(relativeDir, '.gitkeep'));
|
|
28
|
-
const markerDest = path.join(currentDestDir, '.gitkeep');
|
|
29
|
-
if (await exists(markerDest)) {
|
|
30
|
-
skipped.push(marker);
|
|
31
|
-
} else {
|
|
32
|
-
created.push(marker);
|
|
33
|
-
if (!dryRun) {
|
|
34
|
-
await writeFile(markerDest, '');
|
|
35
|
-
}
|
|
36
|
-
}
|
|
37
|
-
return;
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
for (const entry of entries) {
|
|
41
|
-
const relativePath = path.join(relativeDir, entry.name);
|
|
42
|
-
const sourcePath = path.join(currentSourceDir, entry.name);
|
|
43
|
-
const destPath = path.join(destDir, relativePath);
|
|
44
|
-
|
|
45
|
-
if (entry.isDirectory()) {
|
|
46
|
-
await walk(sourcePath, relativePath);
|
|
47
|
-
} else if (entry.isFile()) {
|
|
48
|
-
const normalized = normalizeRelative(relativePath);
|
|
49
|
-
if (await exists(destPath)) {
|
|
50
|
-
skipped.push(normalized);
|
|
51
|
-
} else {
|
|
52
|
-
created.push(normalized);
|
|
53
|
-
if (!dryRun) {
|
|
54
|
-
await mkdir(path.dirname(destPath), { recursive: true });
|
|
55
|
-
await copyFile(sourcePath, destPath);
|
|
56
|
-
const mode = (await stat(sourcePath)).mode & 0o777;
|
|
57
|
-
await chmod(destPath, mode);
|
|
58
|
-
}
|
|
59
|
-
}
|
|
60
|
-
}
|
|
61
|
-
}
|
|
62
|
-
}
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
export async function copyFileIdempotent(srcFile, destFile, options = {}) {
|
|
66
|
-
const dryRun = Boolean(options.dryRun);
|
|
67
|
-
const source = await stat(srcFile);
|
|
68
|
-
if (!source.isFile()) {
|
|
69
|
-
throw new Error(`source is not a file: ${srcFile}`);
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
const relative = path.basename(destFile);
|
|
73
|
-
if (await exists(destFile)) {
|
|
74
|
-
return { created: [], skipped: [relative] };
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
if (!dryRun) {
|
|
78
|
-
await mkdir(path.dirname(destFile), { recursive: true });
|
|
79
|
-
await copyFile(srcFile, destFile);
|
|
80
|
-
await chmod(destFile, source.mode & 0o777);
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
return { created: [relative], skipped: [] };
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
async function exists(value) {
|
|
87
|
-
try {
|
|
88
|
-
await stat(value);
|
|
89
|
-
return true;
|
|
90
|
-
} catch (error) {
|
|
91
|
-
if (error?.code === 'ENOENT') {
|
|
92
|
-
return false;
|
|
93
|
-
}
|
|
94
|
-
throw error;
|
|
95
|
-
}
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
function normalizeRelative(value) {
|
|
99
|
-
return value.split(path.sep).join('/');
|
|
100
|
-
}
|
|
1
|
+
import { copyFile, chmod, mkdir, readdir, stat, writeFile } from 'node:fs/promises';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
|
|
4
|
+
export async function copyTreeIdempotent(srcDir, destDir, options = {}) {
|
|
5
|
+
const dryRun = Boolean(options.dryRun);
|
|
6
|
+
const created = [];
|
|
7
|
+
const skipped = [];
|
|
8
|
+
|
|
9
|
+
const source = await stat(srcDir);
|
|
10
|
+
if (!source.isDirectory()) {
|
|
11
|
+
throw new Error(`source is not a directory: ${srcDir}`);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
await walk(srcDir, '');
|
|
15
|
+
|
|
16
|
+
return { created, skipped };
|
|
17
|
+
|
|
18
|
+
async function walk(currentSourceDir, relativeDir) {
|
|
19
|
+
const entries = await readdir(currentSourceDir, { withFileTypes: true });
|
|
20
|
+
const currentDestDir = path.join(destDir, relativeDir);
|
|
21
|
+
|
|
22
|
+
if (!dryRun) {
|
|
23
|
+
await mkdir(currentDestDir, { recursive: true });
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
if (entries.length === 0) {
|
|
27
|
+
const marker = normalizeRelative(path.join(relativeDir, '.gitkeep'));
|
|
28
|
+
const markerDest = path.join(currentDestDir, '.gitkeep');
|
|
29
|
+
if (await exists(markerDest)) {
|
|
30
|
+
skipped.push(marker);
|
|
31
|
+
} else {
|
|
32
|
+
created.push(marker);
|
|
33
|
+
if (!dryRun) {
|
|
34
|
+
await writeFile(markerDest, '');
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
for (const entry of entries) {
|
|
41
|
+
const relativePath = path.join(relativeDir, entry.name);
|
|
42
|
+
const sourcePath = path.join(currentSourceDir, entry.name);
|
|
43
|
+
const destPath = path.join(destDir, relativePath);
|
|
44
|
+
|
|
45
|
+
if (entry.isDirectory()) {
|
|
46
|
+
await walk(sourcePath, relativePath);
|
|
47
|
+
} else if (entry.isFile()) {
|
|
48
|
+
const normalized = normalizeRelative(relativePath);
|
|
49
|
+
if (await exists(destPath)) {
|
|
50
|
+
skipped.push(normalized);
|
|
51
|
+
} else {
|
|
52
|
+
created.push(normalized);
|
|
53
|
+
if (!dryRun) {
|
|
54
|
+
await mkdir(path.dirname(destPath), { recursive: true });
|
|
55
|
+
await copyFile(sourcePath, destPath);
|
|
56
|
+
const mode = (await stat(sourcePath)).mode & 0o777;
|
|
57
|
+
await chmod(destPath, mode);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export async function copyFileIdempotent(srcFile, destFile, options = {}) {
|
|
66
|
+
const dryRun = Boolean(options.dryRun);
|
|
67
|
+
const source = await stat(srcFile);
|
|
68
|
+
if (!source.isFile()) {
|
|
69
|
+
throw new Error(`source is not a file: ${srcFile}`);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const relative = path.basename(destFile);
|
|
73
|
+
if (await exists(destFile)) {
|
|
74
|
+
return { created: [], skipped: [relative] };
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
if (!dryRun) {
|
|
78
|
+
await mkdir(path.dirname(destFile), { recursive: true });
|
|
79
|
+
await copyFile(srcFile, destFile);
|
|
80
|
+
await chmod(destFile, source.mode & 0o777);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
return { created: [relative], skipped: [] };
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
async function exists(value) {
|
|
87
|
+
try {
|
|
88
|
+
await stat(value);
|
|
89
|
+
return true;
|
|
90
|
+
} catch (error) {
|
|
91
|
+
if (error?.code === 'ENOENT') {
|
|
92
|
+
return false;
|
|
93
|
+
}
|
|
94
|
+
throw error;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function normalizeRelative(value) {
|
|
99
|
+
return value.split(path.sep).join('/');
|
|
100
|
+
}
|
package/src/utils/paths.js
CHANGED
|
@@ -1,60 +1,60 @@
|
|
|
1
|
-
import { existsSync } from 'node:fs';
|
|
2
|
-
import { access } from 'node:fs/promises';
|
|
3
|
-
import path from 'node:path';
|
|
4
|
-
import { fileURLToPath } from 'node:url';
|
|
5
|
-
|
|
6
|
-
let cachedPackageRoot;
|
|
7
|
-
|
|
8
|
-
export async function packageRootAsync() {
|
|
9
|
-
if (cachedPackageRoot) {
|
|
10
|
-
return cachedPackageRoot;
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
let current = path.dirname(fileURLToPath(import.meta.url));
|
|
14
|
-
while (true) {
|
|
15
|
-
const candidate = path.join(current, 'package.json');
|
|
16
|
-
try {
|
|
17
|
-
await access(candidate);
|
|
18
|
-
cachedPackageRoot = current;
|
|
19
|
-
return cachedPackageRoot;
|
|
20
|
-
} catch {
|
|
21
|
-
const parent = path.dirname(current);
|
|
22
|
-
if (parent === current) {
|
|
23
|
-
throw new Error('Could not find package.json from current module path.');
|
|
24
|
-
}
|
|
25
|
-
current = parent;
|
|
26
|
-
}
|
|
27
|
-
}
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
export function packageRoot() {
|
|
31
|
-
if (cachedPackageRoot) {
|
|
32
|
-
return cachedPackageRoot;
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
let current = path.dirname(fileURLToPath(import.meta.url));
|
|
36
|
-
while (true) {
|
|
37
|
-
if (existsSync(path.join(current, 'package.json'))) {
|
|
38
|
-
cachedPackageRoot = current;
|
|
39
|
-
return cachedPackageRoot;
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
const parent = path.dirname(current);
|
|
43
|
-
if (parent === current) {
|
|
44
|
-
throw new Error('Could not find package.json from current module path.');
|
|
45
|
-
}
|
|
46
|
-
current = parent;
|
|
47
|
-
}
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
export function assetPath(name) {
|
|
51
|
-
return path.join(packageRoot(), name);
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
export function skillsPath() {
|
|
55
|
-
return path.join(packageRoot(), 'skills');
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
export function templatesPath(subdir = '') {
|
|
59
|
-
return path.join(packageRoot(), 'templates', subdir);
|
|
60
|
-
}
|
|
1
|
+
import { existsSync } from 'node:fs';
|
|
2
|
+
import { access } from 'node:fs/promises';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { fileURLToPath } from 'node:url';
|
|
5
|
+
|
|
6
|
+
let cachedPackageRoot;
|
|
7
|
+
|
|
8
|
+
export async function packageRootAsync() {
|
|
9
|
+
if (cachedPackageRoot) {
|
|
10
|
+
return cachedPackageRoot;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
let current = path.dirname(fileURLToPath(import.meta.url));
|
|
14
|
+
while (true) {
|
|
15
|
+
const candidate = path.join(current, 'package.json');
|
|
16
|
+
try {
|
|
17
|
+
await access(candidate);
|
|
18
|
+
cachedPackageRoot = current;
|
|
19
|
+
return cachedPackageRoot;
|
|
20
|
+
} catch {
|
|
21
|
+
const parent = path.dirname(current);
|
|
22
|
+
if (parent === current) {
|
|
23
|
+
throw new Error('Could not find package.json from current module path.');
|
|
24
|
+
}
|
|
25
|
+
current = parent;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function packageRoot() {
|
|
31
|
+
if (cachedPackageRoot) {
|
|
32
|
+
return cachedPackageRoot;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
let current = path.dirname(fileURLToPath(import.meta.url));
|
|
36
|
+
while (true) {
|
|
37
|
+
if (existsSync(path.join(current, 'package.json'))) {
|
|
38
|
+
cachedPackageRoot = current;
|
|
39
|
+
return cachedPackageRoot;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const parent = path.dirname(current);
|
|
43
|
+
if (parent === current) {
|
|
44
|
+
throw new Error('Could not find package.json from current module path.');
|
|
45
|
+
}
|
|
46
|
+
current = parent;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function assetPath(name) {
|
|
51
|
+
return path.join(packageRoot(), name);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function skillsPath() {
|
|
55
|
+
return path.join(packageRoot(), 'skills');
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export function templatesPath(subdir = '') {
|
|
59
|
+
return path.join(packageRoot(), 'templates', subdir);
|
|
60
|
+
}
|
package/src/utils/report.js
CHANGED
|
@@ -1,30 +1,30 @@
|
|
|
1
|
-
export function formatSetupReport({ created, skipped, target, dryRun }) {
|
|
2
|
-
const installedLabel = dryRun ? 'Would install' : 'Installed';
|
|
3
|
-
return [
|
|
4
|
-
dryRun ? 'AE setup dry run complete' : 'AE setup complete',
|
|
5
|
-
`- ${installedLabel}: ${created.length} (${summarize(created)})`,
|
|
6
|
-
`- Skipped: ${skipped.length} (${summarize(skipped)})`,
|
|
7
|
-
`- Target: ${target}`,
|
|
8
|
-
`- Next: run \`ae init "<goal>"\`.`,
|
|
9
|
-
].join('\n');
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
export function formatInitReport({ created, skipped, target, dryRun }) {
|
|
13
|
-
const createdLabel = dryRun ? 'Would create' : 'Created';
|
|
14
|
-
return [
|
|
15
|
-
dryRun ? 'AE init dry run complete' : 'AE init complete',
|
|
16
|
-
`- ${createdLabel}: ${created.length} (${summarize(created)})`,
|
|
17
|
-
`- Skipped: ${skipped.length} (${summarize(skipped)})`,
|
|
18
|
-
`- Target: ${target}`,
|
|
19
|
-
].join('\n');
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
function summarize(paths) {
|
|
23
|
-
if (paths.length === 0) {
|
|
24
|
-
return 'none';
|
|
25
|
-
}
|
|
26
|
-
if (paths.length <= 8) {
|
|
27
|
-
return paths.join(', ');
|
|
28
|
-
}
|
|
29
|
-
return `${paths.slice(0, 8).join(', ')}, ...`;
|
|
30
|
-
}
|
|
1
|
+
export function formatSetupReport({ created, skipped, target, dryRun }) {
|
|
2
|
+
const installedLabel = dryRun ? 'Would install' : 'Installed';
|
|
3
|
+
return [
|
|
4
|
+
dryRun ? 'AE setup dry run complete' : 'AE setup complete',
|
|
5
|
+
`- ${installedLabel}: ${created.length} (${summarize(created)})`,
|
|
6
|
+
`- Skipped: ${skipped.length} (${summarize(skipped)})`,
|
|
7
|
+
`- Target: ${target}`,
|
|
8
|
+
`- Next: run \`ae init "<goal>"\`.`,
|
|
9
|
+
].join('\n');
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function formatInitReport({ created, skipped, target, dryRun }) {
|
|
13
|
+
const createdLabel = dryRun ? 'Would create' : 'Created';
|
|
14
|
+
return [
|
|
15
|
+
dryRun ? 'AE init dry run complete' : 'AE init complete',
|
|
16
|
+
`- ${createdLabel}: ${created.length} (${summarize(created)})`,
|
|
17
|
+
`- Skipped: ${skipped.length} (${summarize(skipped)})`,
|
|
18
|
+
`- Target: ${target}`,
|
|
19
|
+
].join('\n');
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function summarize(paths) {
|
|
23
|
+
if (paths.length === 0) {
|
|
24
|
+
return 'none';
|
|
25
|
+
}
|
|
26
|
+
if (paths.length <= 8) {
|
|
27
|
+
return paths.join(', ');
|
|
28
|
+
}
|
|
29
|
+
return `${paths.slice(0, 8).join(', ')}, ...`;
|
|
30
|
+
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
# {{PROJECT_NAME}}
|
|
2
|
-
|
|
3
|
-
**Always read [AGENTS.md](./AGENTS.md) at the start of every session before doing any work.**
|
|
4
|
-
|
|
5
|
-
This project's instructions, rules, structure, and operating guidelines are all defined in `AGENTS.md`.
|
|
1
|
+
# {{PROJECT_NAME}}
|
|
2
|
+
|
|
3
|
+
**Always read [AGENTS.md](./AGENTS.md) at the start of every session before doing any work.**
|
|
4
|
+
|
|
5
|
+
This project's instructions, rules, structure, and operating guidelines are all defined in `AGENTS.md`.
|