azcodr 1.5.2 → 2.1.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/.agents/hooks.json +42 -42
- package/.agents/hooks.json.example +42 -42
- package/.agents/mcp_config.json.example +29 -29
- package/.agents/scripts/safety_guard.sh +143 -34
- package/.agents/scripts/verify_completion.sh +90 -27
- package/.agents/skills/agentic-architect/SKILL.md +125 -125
- package/.agents/skills/agentic-architect/references/agents_md_template.md +62 -62
- package/.agents/skills/agentic-architect/references/refinement_workflow.md +32 -32
- package/.agents/skills/agentic-architect/references/skill_architecture_inquiry.md +63 -63
- package/.agents/skills/agentic-architect/references/skill_template.md +56 -56
- package/.agents/skills/agentic-architect/scripts/validate_agentic_configs.sh +402 -402
- package/.agents/skills/clean-code-refactor/SKILL.md +91 -91
- package/.agents/skills/clean-code-refactor/references/clean_code_smells.md +27 -27
- package/.agents/skills/clean-code-refactor/references/design_patterns_ts.md +65 -65
- package/.agents/skills/compliance-audit/SKILL.md +120 -120
- package/.agents/skills/compliance-audit/references/owasp_top10_controls.md +16 -16
- package/.agents/skills/compliance-audit/references/soc2_iso_controls.md +28 -28
- package/.agents/skills/lets-build/SKILL.md +173 -173
- package/.agents/skills/lets-build/references/architecture_interview_matrix.md +115 -115
- package/.agents/skills/lets-build/references/hexagonal_bootstrap_scaffolds.md +160 -160
- package/.agents/skills/lets-build/references/project_readme_template.md +79 -79
- package/.agents/skills/lets-build/scripts/bootstrap_workspace.sh +419 -255
- package/.agents/skills/product-analyst/SKILL.md +154 -154
- package/.agents/skills/product-analyst/references/backlog_ordering_techniques.md +107 -107
- package/.agents/skills/product-analyst/references/gherkin_patterns.md +46 -46
- package/.agents/skills/product-analyst/references/invest_checklist.md +38 -38
- package/.agents/skills/product-analyst/references/okr_alignment_guide.md +76 -76
- package/.agents/skills/product-analyst/references/smart_tasks.md +59 -59
- package/.agents/skills/relentless-questioner/SKILL.md +128 -128
- package/.agents/skills/relentless-questioner/references/adaptive_question_trees.md +102 -102
- package/.editorconfig +19 -19
- package/.github/workflows/ci.yml +167 -78
- package/.github/workflows/publish.yml +196 -0
- package/.gitignore +40 -25
- package/AGENTS.md +103 -102
- package/LICENSE +21 -21
- package/README.md +168 -165
- package/bin/azcodr.js +19 -228
- package/docs/knowledge/ubiquitous_language.md +31 -18
- package/docs/rules/agentic_configuration.md +259 -259
- package/docs/rules/api_architecture.md +179 -179
- package/docs/rules/authentication.md +76 -76
- package/docs/rules/authorization.md +75 -75
- package/docs/rules/caching.md +69 -69
- package/docs/rules/clean_code.md +62 -62
- package/docs/rules/cloud_native.md +41 -41
- package/docs/rules/cqrs.md +203 -203
- package/docs/rules/database_design.md +125 -125
- package/docs/rules/database_operations.md +69 -69
- package/docs/rules/design_patterns.md +98 -98
- package/docs/rules/devops_ci_cd.md +76 -76
- package/docs/rules/domain_driven_design.md +122 -122
- package/docs/rules/error_handling.md +54 -52
- package/docs/rules/feature_flags.md +59 -59
- package/docs/rules/frontend_architecture.md +157 -157
- package/docs/rules/multitenancy_architecture.md +98 -98
- package/docs/rules/product_ownership.md +127 -127
- package/docs/rules/project_management.md +49 -49
- package/docs/rules/relentless_questioning.md +52 -52
- package/docs/rules/requirements_engineering.md +98 -98
- package/docs/rules/security_compliance.md +53 -53
- package/docs/rules/server_driven_ui.md +88 -88
- package/docs/rules/test_driven_development.md +185 -185
- package/docs/rules/transactional_email.md +27 -27
- package/docs/rules/type_safety.md +65 -65
- package/docs/rules/ui_ux_architecture.md +150 -150
- package/docs/rules/workflow_state_machines.md +117 -117
- package/lib/cli-parse.d.ts +32 -0
- package/lib/cli-parse.d.ts.map +1 -0
- package/lib/cli-parse.js +55 -0
- package/lib/cli-parse.js.map +1 -0
- package/lib/cli-target.d.ts +66 -0
- package/lib/cli-target.d.ts.map +1 -0
- package/lib/cli-target.js +102 -0
- package/lib/cli-target.js.map +1 -0
- package/lib/cli.d.ts +40 -0
- package/lib/cli.d.ts.map +1 -0
- package/lib/cli.js +166 -0
- package/lib/cli.js.map +1 -0
- package/lib/errors.d.ts +39 -0
- package/lib/errors.d.ts.map +1 -0
- package/lib/errors.js +26 -0
- package/lib/errors.js.map +1 -0
- package/lib/git.d.ts +15 -0
- package/lib/git.d.ts.map +1 -0
- package/lib/git.js +32 -0
- package/lib/git.js.map +1 -0
- package/lib/guards.d.ts +35 -0
- package/lib/guards.d.ts.map +1 -0
- package/lib/guards.js +95 -0
- package/lib/guards.js.map +1 -0
- package/lib/index.d.ts +6 -134
- package/lib/index.d.ts.map +1 -0
- package/lib/index.js +4 -5
- package/lib/index.js.map +1 -0
- package/lib/links.d.ts +28 -0
- package/lib/links.d.ts.map +1 -0
- package/lib/links.js +129 -0
- package/lib/links.js.map +1 -0
- package/lib/permissions.d.ts +9 -0
- package/lib/permissions.d.ts.map +1 -0
- package/lib/permissions.js +44 -0
- package/lib/permissions.js.map +1 -0
- package/lib/repo.d.ts +20 -0
- package/lib/repo.d.ts.map +1 -0
- package/lib/repo.js +92 -0
- package/lib/repo.js.map +1 -0
- package/lib/scaffold.d.ts +80 -0
- package/lib/scaffold.d.ts.map +1 -0
- package/lib/scaffold.js +201 -448
- package/lib/scaffold.js.map +1 -0
- package/memory.md +135 -36
- package/package.json +75 -62
- package/scripts/test_coverage.js +66 -38
- package/scripts/validate/adr.js +155 -0
- package/scripts/validate/io.js +82 -0
- package/scripts/validate/links.js +166 -0
- package/scripts/validate/parity.js +122 -0
- package/scripts/validate/root.js +183 -0
- package/scripts/validate/rules.js +42 -0
- package/scripts/validate/skills.js +94 -0
- package/scripts/validate/text.js +27 -0
- package/scripts/validate-cli.js +12 -0
- package/scripts/validate.js +158 -258
- package/src/cli-parse.ts +77 -0
- package/src/cli-target.ts +167 -0
- package/src/cli.ts +240 -0
- package/src/errors.ts +35 -0
- package/src/git.ts +34 -0
- package/src/guards.ts +101 -0
- package/src/index.ts +39 -0
- package/src/links.ts +139 -0
- package/src/permissions.ts +42 -0
- package/src/repo.ts +94 -0
- package/src/scaffold.ts +273 -0
- package/.github/copilot-instructions.md +0 -1
package/src/links.ts
ADDED
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
|
|
4
|
+
export interface EnsureSymlinkOptions {
|
|
5
|
+
targetDir: string;
|
|
6
|
+
linkName: string;
|
|
7
|
+
targetFileName: string;
|
|
8
|
+
dryRun?: boolean;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
interface BothEntriesExistOptions {
|
|
12
|
+
targetDir: string;
|
|
13
|
+
linkName: string;
|
|
14
|
+
targetFileName: string;
|
|
15
|
+
entries: readonly string[];
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function namesDifferOnlyByCase(linkName: string, targetFileName: string): boolean {
|
|
19
|
+
if (linkName.toLowerCase() !== targetFileName.toLowerCase()) return false;
|
|
20
|
+
return linkName !== targetFileName;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function readDirEntries(targetDir: string): string[] | null {
|
|
24
|
+
try {
|
|
25
|
+
return fs.readdirSync(targetDir);
|
|
26
|
+
} catch {
|
|
27
|
+
return null;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function bothEntriesExist(options: BothEntriesExistOptions): boolean {
|
|
32
|
+
const { targetDir, linkName, targetFileName, entries } = options;
|
|
33
|
+
if (!entries.includes(targetFileName) || !entries.includes(linkName)) return false;
|
|
34
|
+
try {
|
|
35
|
+
fs.lstatSync(path.join(targetDir, targetFileName));
|
|
36
|
+
fs.lstatSync(path.join(targetDir, linkName));
|
|
37
|
+
return true;
|
|
38
|
+
} catch {
|
|
39
|
+
return false;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function accessiblePair(targetPath: string, linkPath: string): boolean {
|
|
44
|
+
if (!fs.existsSync(targetPath) || !fs.existsSync(linkPath)) return false;
|
|
45
|
+
try {
|
|
46
|
+
fs.lstatSync(targetPath);
|
|
47
|
+
fs.lstatSync(linkPath);
|
|
48
|
+
return true;
|
|
49
|
+
} catch {
|
|
50
|
+
return false;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function removeIfExists(linkPath: string): void {
|
|
55
|
+
try {
|
|
56
|
+
fs.lstatSync(linkPath);
|
|
57
|
+
fs.rmSync(linkPath, { force: true });
|
|
58
|
+
} catch {
|
|
59
|
+
// Path does not exist, proceed
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function copyFallback(targetDir: string, linkName: string, targetFileName: string): void {
|
|
64
|
+
const sourceFile = path.resolve(targetDir, targetFileName);
|
|
65
|
+
if (fs.existsSync(sourceFile)) {
|
|
66
|
+
fs.copyFileSync(sourceFile, path.join(targetDir, linkName));
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Detects whether linkName and targetFileName refer to the same entry on a case-insensitive filesystem.
|
|
72
|
+
* Uses exact directory listing (not existsSync, which lies on case-insensitive systems).
|
|
73
|
+
*/
|
|
74
|
+
export function isSameCaseInsensitiveFile(
|
|
75
|
+
targetDir: string,
|
|
76
|
+
linkName: string,
|
|
77
|
+
targetFileName: string
|
|
78
|
+
): boolean {
|
|
79
|
+
if (linkName === targetFileName) return true;
|
|
80
|
+
if (!namesDifferOnlyByCase(linkName, targetFileName)) return false;
|
|
81
|
+
const entries = readDirEntries(targetDir);
|
|
82
|
+
if (entries === null) return false;
|
|
83
|
+
if (bothEntriesExist({ targetDir, linkName, targetFileName, entries })) {
|
|
84
|
+
return !fs.lstatSync(path.join(targetDir, linkName)).isSymbolicLink();
|
|
85
|
+
}
|
|
86
|
+
const targetPath = path.join(targetDir, targetFileName);
|
|
87
|
+
const linkPath = path.join(targetDir, linkName);
|
|
88
|
+
return accessiblePair(targetPath, linkPath);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Safely creates or updates a symbolic link, falling back to a file copy if symlinks are unsupported.
|
|
93
|
+
*/
|
|
94
|
+
export function ensureSymlink(options: EnsureSymlinkOptions): boolean {
|
|
95
|
+
const { targetDir, linkName, targetFileName, dryRun = false } = options;
|
|
96
|
+
if (dryRun) return true;
|
|
97
|
+
if (isSameCaseInsensitiveFile(targetDir, linkName, targetFileName)) return true;
|
|
98
|
+
const linkPath = path.join(targetDir, linkName);
|
|
99
|
+
removeIfExists(linkPath);
|
|
100
|
+
try {
|
|
101
|
+
fs.symlinkSync(targetFileName, linkPath, 'file');
|
|
102
|
+
return true;
|
|
103
|
+
} catch {
|
|
104
|
+
copyFallback(targetDir, linkName, targetFileName);
|
|
105
|
+
return true;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Creates a symlink, falling back to a text pointer (not a full copy).
|
|
111
|
+
* Used for .github/copilot-instructions.md where a full AGENTS.md copy would
|
|
112
|
+
* break relative markdown links (they resolve from .github/, not root).
|
|
113
|
+
*/
|
|
114
|
+
export function ensureSymlinkOrPointer(options: EnsureSymlinkOptions): boolean {
|
|
115
|
+
const { targetDir, linkName, targetFileName, dryRun = false } = options;
|
|
116
|
+
if (dryRun) return true;
|
|
117
|
+
const linkPath = path.join(targetDir, linkName);
|
|
118
|
+
try {
|
|
119
|
+
try {
|
|
120
|
+
fs.lstatSync(linkPath);
|
|
121
|
+
const existing = fs.existsSync(linkPath) ? fs.readFileSync(linkPath, 'utf-8') : '';
|
|
122
|
+
if (existing.trim() === targetFileName) return true;
|
|
123
|
+
fs.rmSync(linkPath, { force: true });
|
|
124
|
+
} catch {
|
|
125
|
+
// Path does not exist, proceed
|
|
126
|
+
}
|
|
127
|
+
fs.symlinkSync(targetFileName, linkPath, 'file');
|
|
128
|
+
return true;
|
|
129
|
+
} catch {
|
|
130
|
+
fs.writeFileSync(linkPath, `${targetFileName}\n`, 'utf-8');
|
|
131
|
+
return true;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
export default {
|
|
136
|
+
isSameCaseInsensitiveFile,
|
|
137
|
+
ensureSymlink,
|
|
138
|
+
ensureSymlinkOrPointer
|
|
139
|
+
};
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
|
|
4
|
+
function chmodShFile(filePath: string, dryRun: boolean): void {
|
|
5
|
+
if (dryRun) return;
|
|
6
|
+
try {
|
|
7
|
+
fs.chmodSync(filePath, 0o755);
|
|
8
|
+
} catch {
|
|
9
|
+
// Non-critical if filesystem does not support POSIX permissions
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function chmodShFilesIn(scriptsDir: string, dryRun: boolean): string[] {
|
|
14
|
+
const modified: string[] = [];
|
|
15
|
+
if (!fs.existsSync(scriptsDir) || !fs.statSync(scriptsDir).isDirectory()) return modified;
|
|
16
|
+
const files = fs.readdirSync(scriptsDir);
|
|
17
|
+
for (const file of files) {
|
|
18
|
+
if (!file.endsWith('.sh')) continue;
|
|
19
|
+
const filePath = path.join(scriptsDir, file);
|
|
20
|
+
modified.push(filePath);
|
|
21
|
+
chmodShFile(filePath, dryRun);
|
|
22
|
+
}
|
|
23
|
+
return modified;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Ensures all bash scripts in agent and skill directories have executable permissions (0o755).
|
|
28
|
+
*/
|
|
29
|
+
export function makeScriptsExecutable(targetDir: string, dryRun = false): string[] {
|
|
30
|
+
const agentScriptsDir = path.join(targetDir, '.agents', 'scripts');
|
|
31
|
+
const modified = chmodShFilesIn(agentScriptsDir, dryRun);
|
|
32
|
+
const skillsDir = path.join(targetDir, '.agents', 'skills');
|
|
33
|
+
if (!fs.existsSync(skillsDir)) return modified;
|
|
34
|
+
const skills = fs.readdirSync(skillsDir);
|
|
35
|
+
for (const skill of skills) {
|
|
36
|
+
const scriptsDir = path.join(skillsDir, skill, 'scripts');
|
|
37
|
+
modified.push(...chmodShFilesIn(scriptsDir, dryRun));
|
|
38
|
+
}
|
|
39
|
+
return modified;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export default { makeScriptsExecutable };
|
package/src/repo.ts
ADDED
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import process from 'node:process';
|
|
4
|
+
import { runGit } from './git.js';
|
|
5
|
+
|
|
6
|
+
const INITIAL_COMMIT_MESSAGE = 'chore: initial scaffold from azcodr template';
|
|
7
|
+
|
|
8
|
+
export interface InitGitOptions {
|
|
9
|
+
noGit?: boolean;
|
|
10
|
+
dryRun?: boolean;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Detects whether targetDir is already inside an existing Git worktree.
|
|
15
|
+
* Socket note: shell access is limited to `git rev-parse` via runGit (no shell).
|
|
16
|
+
*/
|
|
17
|
+
export function isInsideGitWorkTree(targetDir: string): boolean {
|
|
18
|
+
try {
|
|
19
|
+
const checkDir = fs.existsSync(targetDir) ? targetDir : path.dirname(targetDir);
|
|
20
|
+
const out = runGit(['rev-parse', '--is-inside-work-tree'], {
|
|
21
|
+
cwd: checkDir,
|
|
22
|
+
stdio: ['ignore', 'pipe', 'ignore']
|
|
23
|
+
});
|
|
24
|
+
return String(out).trim() === 'true';
|
|
25
|
+
} catch {
|
|
26
|
+
return false;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function renameBranchMain(targetDir: string): void {
|
|
31
|
+
try {
|
|
32
|
+
runGit(['branch', '-m', 'main'], { cwd: targetDir, stdio: 'ignore' });
|
|
33
|
+
} catch {
|
|
34
|
+
// Non-critical if branch rename fails
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function initRepository(targetDir: string): void {
|
|
39
|
+
try {
|
|
40
|
+
runGit(['init', '-b', 'main', '-q'], { cwd: targetDir, stdio: 'ignore' });
|
|
41
|
+
} catch {
|
|
42
|
+
runGit(['init', '-q'], { cwd: targetDir, stdio: 'ignore' });
|
|
43
|
+
renameBranchMain(targetDir);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function commitWithFallbackIdentity(targetDir: string): void {
|
|
48
|
+
const options = { cwd: targetDir, stdio: 'ignore' as const };
|
|
49
|
+
try {
|
|
50
|
+
runGit(['commit', '-q', '-m', INITIAL_COMMIT_MESSAGE], options);
|
|
51
|
+
} catch {
|
|
52
|
+
runGit(['commit', '-q', '-m', INITIAL_COMMIT_MESSAGE], {
|
|
53
|
+
...options,
|
|
54
|
+
env: {
|
|
55
|
+
...process.env,
|
|
56
|
+
GIT_AUTHOR_NAME: 'Subodh Khanal',
|
|
57
|
+
GIT_AUTHOR_EMAIL: 'prosubodh@gmail.com',
|
|
58
|
+
GIT_COMMITTER_NAME: 'Subodh Khanal',
|
|
59
|
+
GIT_COMMITTER_EMAIL: 'prosubodh@gmail.com'
|
|
60
|
+
}
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function initialCommit(targetDir: string): void {
|
|
66
|
+
try {
|
|
67
|
+
runGit(['add', '-A'], { cwd: targetDir, stdio: 'ignore' });
|
|
68
|
+
commitWithFallbackIdentity(targetDir);
|
|
69
|
+
} catch {
|
|
70
|
+
// Non-critical if initial commit fails
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Initializes a git repository in the target directory if not already inside one.
|
|
76
|
+
* Socket note: only allowlisted `git init/branch/add/commit` via execFileSync, cwd scoped.
|
|
77
|
+
*/
|
|
78
|
+
export function initGit(targetDir: string, options: InitGitOptions = {}): boolean {
|
|
79
|
+
const { noGit = false, dryRun = false } = options;
|
|
80
|
+
if (noGit) return false;
|
|
81
|
+
const gitDir = path.join(targetDir, '.git');
|
|
82
|
+
if (fs.existsSync(gitDir)) return false;
|
|
83
|
+
if (isInsideGitWorkTree(targetDir)) return false;
|
|
84
|
+
if (dryRun) return true;
|
|
85
|
+
try {
|
|
86
|
+
initRepository(targetDir);
|
|
87
|
+
initialCommit(targetDir);
|
|
88
|
+
return true;
|
|
89
|
+
} catch {
|
|
90
|
+
return false;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export default { isInsideGitWorkTree, initGit };
|
package/src/scaffold.ts
ADDED
|
@@ -0,0 +1,273 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import process from 'node:process';
|
|
4
|
+
import { fileURLToPath } from 'node:url';
|
|
5
|
+
import { ScaffoldError, ERROR_CODES } from './errors.js';
|
|
6
|
+
import type { ScaffoldErrorCode, ScaffoldErrorShape } from './errors.js';
|
|
7
|
+
import { runGit } from './git.js';
|
|
8
|
+
import { assertInside, isProtectedTarget } from './guards.js';
|
|
9
|
+
import { ensureSymlink, ensureSymlinkOrPointer, isSameCaseInsensitiveFile } from './links.js';
|
|
10
|
+
import type { EnsureSymlinkOptions } from './links.js';
|
|
11
|
+
import { makeScriptsExecutable } from './permissions.js';
|
|
12
|
+
import { isInsideGitWorkTree, initGit } from './repo.js';
|
|
13
|
+
import type { InitGitOptions } from './repo.js';
|
|
14
|
+
|
|
15
|
+
export const TEMPLATE_ITEMS: readonly string[] = [
|
|
16
|
+
'AGENTS.md',
|
|
17
|
+
'memory.md',
|
|
18
|
+
'README.md',
|
|
19
|
+
'docs',
|
|
20
|
+
'.agents',
|
|
21
|
+
'.github',
|
|
22
|
+
'scripts',
|
|
23
|
+
'.gitignore',
|
|
24
|
+
'.editorconfig',
|
|
25
|
+
'LICENSE'
|
|
26
|
+
];
|
|
27
|
+
|
|
28
|
+
export interface ScaffoldOptions {
|
|
29
|
+
targetDir?: string;
|
|
30
|
+
force?: boolean;
|
|
31
|
+
noGit?: boolean;
|
|
32
|
+
templateDir?: string;
|
|
33
|
+
dryRun?: boolean;
|
|
34
|
+
silent?: boolean;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export interface ScaffoldResult {
|
|
38
|
+
success: boolean;
|
|
39
|
+
targetDir: string;
|
|
40
|
+
gitInitialized: boolean;
|
|
41
|
+
dryRun: boolean;
|
|
42
|
+
actions: string[];
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export interface ValidateTargetOptions {
|
|
46
|
+
templateDir?: string;
|
|
47
|
+
force?: boolean;
|
|
48
|
+
dryRun?: boolean;
|
|
49
|
+
allowProtected?: boolean;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export interface CopyTemplateOptions {
|
|
53
|
+
dryRun?: boolean;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Returns the root path to the azcodr template files.
|
|
58
|
+
*/
|
|
59
|
+
export function getTemplateDir(): string {
|
|
60
|
+
return path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function rejectTemplateSelf(resolvedTarget: string, resolvedTemplate: string): void {
|
|
64
|
+
if (resolvedTarget !== resolvedTemplate) return;
|
|
65
|
+
throw new ScaffoldError(
|
|
66
|
+
'E_TARGET_IS_TEMPLATE',
|
|
67
|
+
`${ERROR_CODES.E_TARGET_IS_TEMPLATE}: ${resolvedTarget}`
|
|
68
|
+
);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function rejectProtectedTarget(
|
|
72
|
+
resolvedTarget: string,
|
|
73
|
+
templateDir: string,
|
|
74
|
+
allowProtected: boolean
|
|
75
|
+
): void {
|
|
76
|
+
if (allowProtected) return;
|
|
77
|
+
if (!isProtectedTarget(resolvedTarget, { templateDir })) return;
|
|
78
|
+
throw new ScaffoldError(
|
|
79
|
+
'E_TARGET_IS_PROTECTED',
|
|
80
|
+
`${ERROR_CODES.E_TARGET_IS_PROTECTED}: ${resolvedTarget}. ` +
|
|
81
|
+
'Scaffolding there would overwrite personal or system files. Choose a project subdirectory instead.'
|
|
82
|
+
);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function rejectNonEmptyTarget(resolvedTarget: string, force: boolean): void {
|
|
86
|
+
const entries = fs.readdirSync(resolvedTarget);
|
|
87
|
+
if (entries.length === 0 || force) return;
|
|
88
|
+
throw new ScaffoldError(
|
|
89
|
+
'E_TARGET_NOT_EMPTY',
|
|
90
|
+
`${ERROR_CODES.E_TARGET_NOT_EMPTY} (${entries.length} items found: ${resolvedTarget}). ` +
|
|
91
|
+
'Use --force to overwrite existing files.'
|
|
92
|
+
);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Validates the target directory to ensure it is suitable for scaffolding.
|
|
97
|
+
*/
|
|
98
|
+
export function validateTarget(targetDir: string, options: ValidateTargetOptions = {}): void {
|
|
99
|
+
const { templateDir = getTemplateDir(), force = false, dryRun = false, allowProtected = false } = options;
|
|
100
|
+
const resolvedTarget = path.resolve(targetDir);
|
|
101
|
+
const resolvedTemplate = path.resolve(templateDir);
|
|
102
|
+
rejectTemplateSelf(resolvedTarget, resolvedTemplate);
|
|
103
|
+
rejectProtectedTarget(resolvedTarget, templateDir, allowProtected);
|
|
104
|
+
if (!fs.existsSync(resolvedTarget)) {
|
|
105
|
+
if (!dryRun) fs.mkdirSync(resolvedTarget, { recursive: true });
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
rejectNonEmptyTarget(resolvedTarget, force);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function resolveTemplateItem(resolvedTemplate: string, item: string): string {
|
|
112
|
+
let srcPath = path.join(resolvedTemplate, item);
|
|
113
|
+
if (item === '.gitignore' && !fs.existsSync(srcPath)) {
|
|
114
|
+
const npmIgnorePath = path.join(resolvedTemplate, '.npmignore');
|
|
115
|
+
if (fs.existsSync(npmIgnorePath)) srcPath = npmIgnorePath;
|
|
116
|
+
}
|
|
117
|
+
return srcPath;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function copyTemplateItems(resolvedTarget: string, resolvedTemplate: string, dryRun: boolean): string[] {
|
|
121
|
+
const actions: string[] = [];
|
|
122
|
+
for (const item of TEMPLATE_ITEMS) {
|
|
123
|
+
const srcPath = resolveTemplateItem(resolvedTemplate, item);
|
|
124
|
+
if (!fs.existsSync(srcPath)) continue;
|
|
125
|
+
const destPath = path.join(resolvedTarget, item);
|
|
126
|
+
assertInside(resolvedTemplate, srcPath);
|
|
127
|
+
assertInside(resolvedTarget, destPath);
|
|
128
|
+
actions.push(`copy: ${item} -> ${destPath}`);
|
|
129
|
+
if (!dryRun) fs.cpSync(srcPath, destPath, { recursive: true, force: true, dereference: false });
|
|
130
|
+
}
|
|
131
|
+
return actions;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function ensureHarnessParity(resolvedTarget: string, dryRun: boolean): string[] {
|
|
135
|
+
const links: readonly [string, string][] = [
|
|
136
|
+
['CLAUDE.md', 'AGENTS.md'],
|
|
137
|
+
['agents.md', 'AGENTS.md'],
|
|
138
|
+
['GEMINI.md', 'AGENTS.md'],
|
|
139
|
+
['.cursorrules', 'AGENTS.md'],
|
|
140
|
+
['.windsurfrules', 'AGENTS.md']
|
|
141
|
+
];
|
|
142
|
+
const actions: string[] = [];
|
|
143
|
+
for (const [linkName, targetFileName] of links) {
|
|
144
|
+
actions.push(`symlink: ${linkName} -> ${targetFileName}`);
|
|
145
|
+
ensureSymlink({ targetDir: resolvedTarget, linkName, targetFileName, dryRun });
|
|
146
|
+
}
|
|
147
|
+
return actions;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function ensureCopilotParity(resolvedTarget: string, dryRun: boolean): string[] {
|
|
151
|
+
const githubDir = path.join(resolvedTarget, '.github');
|
|
152
|
+
if (!dryRun && !fs.existsSync(githubDir)) fs.mkdirSync(githubDir, { recursive: true });
|
|
153
|
+
ensureSymlinkOrPointer({
|
|
154
|
+
targetDir: githubDir,
|
|
155
|
+
linkName: 'copilot-instructions.md',
|
|
156
|
+
targetFileName: '../AGENTS.md',
|
|
157
|
+
dryRun
|
|
158
|
+
});
|
|
159
|
+
return ['symlink: .github/copilot-instructions.md -> ../AGENTS.md'];
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function starterPackageJson(resolvedTarget: string): Record<string, unknown> {
|
|
163
|
+
const projectName = path.basename(resolvedTarget) || 'my-project';
|
|
164
|
+
return {
|
|
165
|
+
name: projectName,
|
|
166
|
+
version: '0.1.0',
|
|
167
|
+
type: 'module',
|
|
168
|
+
private: true,
|
|
169
|
+
description: 'Scaffolded with azcodr enterprise architecture template',
|
|
170
|
+
scripts: {
|
|
171
|
+
test: 'node --test',
|
|
172
|
+
'test:coverage': 'node scripts/test_coverage.js',
|
|
173
|
+
lint: 'echo "No linter configured yet. Run /lets-build to configure toolchain."',
|
|
174
|
+
validate: 'node scripts/validate-cli.js'
|
|
175
|
+
}
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
function ensureStarterPackageJson(resolvedTarget: string, dryRun: boolean): string[] {
|
|
180
|
+
const pkgJsonPath = path.join(resolvedTarget, 'package.json');
|
|
181
|
+
if (fs.existsSync(pkgJsonPath)) return [];
|
|
182
|
+
if (dryRun) return ['create: package.json'];
|
|
183
|
+
const starterPkg = starterPackageJson(resolvedTarget);
|
|
184
|
+
fs.writeFileSync(pkgJsonPath, JSON.stringify(starterPkg, null, 2) + '\n', 'utf-8');
|
|
185
|
+
return ['create: package.json'];
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
function chmodTemplateScripts(inspectDir: string, dryRun: boolean): string[] {
|
|
189
|
+
const actions: string[] = [];
|
|
190
|
+
const scripts = makeScriptsExecutable(inspectDir, dryRun);
|
|
191
|
+
for (const script of scripts) {
|
|
192
|
+
actions.push(`chmod: +x ${path.relative(inspectDir, script)}`);
|
|
193
|
+
}
|
|
194
|
+
return actions;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* Recursively copies template files into the target directory and sets up symlinks and permissions.
|
|
199
|
+
* Socket note: filesystem access is scoped to templateDir -> targetDir only (assertInside).
|
|
200
|
+
*/
|
|
201
|
+
export function copyTemplate(
|
|
202
|
+
targetDir: string,
|
|
203
|
+
templateDir: string = getTemplateDir(),
|
|
204
|
+
options: CopyTemplateOptions = {}
|
|
205
|
+
): string[] {
|
|
206
|
+
const { dryRun = false } = options;
|
|
207
|
+
const resolvedTarget = path.resolve(targetDir);
|
|
208
|
+
const resolvedTemplate = path.resolve(templateDir);
|
|
209
|
+
if (!dryRun && !fs.existsSync(resolvedTarget)) {
|
|
210
|
+
fs.mkdirSync(resolvedTarget, { recursive: true });
|
|
211
|
+
}
|
|
212
|
+
const actions = copyTemplateItems(resolvedTarget, resolvedTemplate, dryRun);
|
|
213
|
+
actions.push(...ensureHarnessParity(resolvedTarget, dryRun));
|
|
214
|
+
actions.push(...ensureCopilotParity(resolvedTarget, dryRun));
|
|
215
|
+
actions.push(...ensureStarterPackageJson(resolvedTarget, dryRun));
|
|
216
|
+
const inspectDir = dryRun ? resolvedTemplate : resolvedTarget;
|
|
217
|
+
actions.push(...chmodTemplateScripts(inspectDir, dryRun));
|
|
218
|
+
return actions;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* High-level orchestration function to scaffold the azcodr workspace into targetDir.
|
|
223
|
+
*/
|
|
224
|
+
export function scaffold(options: ScaffoldOptions = {}): ScaffoldResult {
|
|
225
|
+
const {
|
|
226
|
+
targetDir = process.cwd(),
|
|
227
|
+
force = false,
|
|
228
|
+
noGit = false,
|
|
229
|
+
templateDir = getTemplateDir(),
|
|
230
|
+
dryRun = false
|
|
231
|
+
} = options;
|
|
232
|
+
const resolvedTarget = path.resolve(targetDir);
|
|
233
|
+
validateTarget(resolvedTarget, { templateDir, force, dryRun });
|
|
234
|
+
const actions = copyTemplate(resolvedTarget, templateDir, { dryRun });
|
|
235
|
+
const gitInitialized = initGit(resolvedTarget, { noGit, dryRun });
|
|
236
|
+
if (gitInitialized) actions.push('git: initialize repository');
|
|
237
|
+
return { success: true, targetDir: resolvedTarget, gitInitialized, dryRun, actions };
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
export {
|
|
241
|
+
ensureSymlink,
|
|
242
|
+
ensureSymlinkOrPointer,
|
|
243
|
+
isSameCaseInsensitiveFile,
|
|
244
|
+
makeScriptsExecutable,
|
|
245
|
+
isInsideGitWorkTree,
|
|
246
|
+
initGit,
|
|
247
|
+
runGit,
|
|
248
|
+
assertInside,
|
|
249
|
+
ScaffoldError,
|
|
250
|
+
ERROR_CODES,
|
|
251
|
+
isProtectedTarget
|
|
252
|
+
};
|
|
253
|
+
|
|
254
|
+
export type { EnsureSymlinkOptions, InitGitOptions, ScaffoldErrorCode, ScaffoldErrorShape };
|
|
255
|
+
|
|
256
|
+
export default {
|
|
257
|
+
scaffold,
|
|
258
|
+
validateTarget,
|
|
259
|
+
copyTemplate,
|
|
260
|
+
ensureSymlink,
|
|
261
|
+
ensureSymlinkOrPointer,
|
|
262
|
+
isSameCaseInsensitiveFile,
|
|
263
|
+
makeScriptsExecutable,
|
|
264
|
+
isInsideGitWorkTree,
|
|
265
|
+
initGit,
|
|
266
|
+
getTemplateDir,
|
|
267
|
+
runGit,
|
|
268
|
+
assertInside,
|
|
269
|
+
TEMPLATE_ITEMS,
|
|
270
|
+
ScaffoldError,
|
|
271
|
+
ERROR_CODES,
|
|
272
|
+
isProtectedTarget
|
|
273
|
+
};
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
../AGENTS.md
|