create-aztec-privacy-template 0.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/dist/cli.js +9 -0
- package/dist/constants.js +35 -0
- package/dist/create-app.js +50 -0
- package/dist/helpers/cli-options.js +108 -0
- package/dist/helpers/examples.js +264 -0
- package/dist/helpers/git.js +73 -0
- package/dist/helpers/install.js +36 -0
- package/dist/helpers/post-init.js +33 -0
- package/dist/helpers/template-scaffold.js +18 -0
- package/dist/index.js +50 -0
- package/dist/placeholders.js +47 -0
- package/dist/prompts.js +170 -0
- package/dist/templates/index.js +56 -0
- package/dist/templates/types.js +1 -0
- package/dist/validate.js +45 -0
- package/overlays/examples/aave/examples/aave/README.md +19 -0
- package/overlays/examples/lido/examples/lido/README.md +19 -0
- package/overlays/examples/uniswap/examples/uniswap/README.md +19 -0
- package/package.json +35 -0
- package/scaffold/.solhint.json +13 -0
- package/scaffold/Makefile +160 -0
- package/scaffold/README.md +76 -0
- package/scaffold/contracts/README.md +23 -0
- package/scaffold/contracts/aztec/Nargo.toml +8 -0
- package/scaffold/contracts/aztec/README.md +22 -0
- package/scaffold/contracts/aztec/src/main.nr +104 -0
- package/scaffold/contracts/l1/BasePortal.sol +121 -0
- package/scaffold/contracts/l1/EscapeHatch.sol +157 -0
- package/scaffold/contracts/l1/GenericPortal.sol +181 -0
- package/scaffold/contracts/l1/README.md +24 -0
- package/scaffold/contracts/l1/foundry.toml +6 -0
- package/scaffold/contracts/l1/test/BasePortal.t.sol +121 -0
- package/scaffold/contracts/l1/test/EscapeHatch.t.sol +171 -0
- package/scaffold/contracts/l1/test/GenericPortal.t.sol +188 -0
- package/scaffold/gitignore +22 -0
- package/scaffold/scripts/compile-aztec-contract.sh +87 -0
package/dist/cli.js
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { printUsage } from './helpers/cli-options.js';
|
|
3
|
+
import { run } from './index.js';
|
|
4
|
+
run(process.argv.slice(2), import.meta.url).catch((error) => {
|
|
5
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
6
|
+
console.error(`Error: ${message}`);
|
|
7
|
+
printUsage();
|
|
8
|
+
process.exit(1);
|
|
9
|
+
});
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
export const SUPPORTED_PACKAGE_MANAGERS = ['bun', 'npm', 'pnpm', 'yarn'];
|
|
2
|
+
export const SCAFFOLD_DIR = 'scaffold';
|
|
3
|
+
export const OVERLAY_EXAMPLES_DIR = 'overlays/examples';
|
|
4
|
+
export const EXAMPLE_OVERLAY_ORDER = ['aave', 'lido', 'uniswap'];
|
|
5
|
+
export const SUPPORTED_EXAMPLE_SELECTIONS = ['none', ...EXAMPLE_OVERLAY_ORDER, 'all'];
|
|
6
|
+
export const TEMPLATE_COPY_ENTRIES = [
|
|
7
|
+
'.solhint.json',
|
|
8
|
+
'gitignore',
|
|
9
|
+
'Makefile',
|
|
10
|
+
'README.md',
|
|
11
|
+
'contracts',
|
|
12
|
+
'scripts',
|
|
13
|
+
];
|
|
14
|
+
export const PLACEHOLDER_TEXT_FILES = ['README.md'];
|
|
15
|
+
export const STARTER_PACKAGE_JSON_BASE = {
|
|
16
|
+
private: true,
|
|
17
|
+
version: '0.1.0',
|
|
18
|
+
description: 'Protocol-agnostic starter for Aztec privacy integrations',
|
|
19
|
+
license: 'MIT',
|
|
20
|
+
scripts: {
|
|
21
|
+
fmt: 'make fmt',
|
|
22
|
+
'fmt:check': 'make fmt-check',
|
|
23
|
+
lint: 'make lint',
|
|
24
|
+
test: 'make test',
|
|
25
|
+
},
|
|
26
|
+
devDependencies: {
|
|
27
|
+
solhint: '^6.0.3',
|
|
28
|
+
},
|
|
29
|
+
};
|
|
30
|
+
export const INSTALL_COMMANDS = {
|
|
31
|
+
bun: 'bun install',
|
|
32
|
+
npm: 'npm install',
|
|
33
|
+
pnpm: 'pnpm install',
|
|
34
|
+
yarn: 'yarn install',
|
|
35
|
+
};
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { relative } from 'node:path';
|
|
2
|
+
import { installExampleSource } from './helpers/examples.js';
|
|
3
|
+
import { tryGitInit } from './helpers/git.js';
|
|
4
|
+
import { installDependencies } from './helpers/install.js';
|
|
5
|
+
import { runPostInitHooks } from './helpers/post-init.js';
|
|
6
|
+
import { scaffoldTemplate } from './helpers/template-scaffold.js';
|
|
7
|
+
import { assertTargetPathSafe, resolveProjectTarget } from './validate.js';
|
|
8
|
+
export async function createApp(options, dependencies = {}) {
|
|
9
|
+
const { generatorRoot, projectArg, packageManager, exampleSelection, exampleSource, skipInstall = false, disableGit = false, } = options;
|
|
10
|
+
const install = dependencies.install ?? installDependencies;
|
|
11
|
+
const postInit = dependencies.postInit ?? runPostInitHooks;
|
|
12
|
+
const gitInit = dependencies.gitInit ?? tryGitInit;
|
|
13
|
+
const installRemoteExample = dependencies.installExampleSource ?? installExampleSource;
|
|
14
|
+
const { absoluteTargetPath, projectName } = resolveProjectTarget(projectArg);
|
|
15
|
+
assertTargetPathSafe(absoluteTargetPath);
|
|
16
|
+
await scaffoldTemplate({
|
|
17
|
+
generatorRoot,
|
|
18
|
+
absoluteTargetPath,
|
|
19
|
+
projectName,
|
|
20
|
+
packageManager,
|
|
21
|
+
exampleSelection,
|
|
22
|
+
});
|
|
23
|
+
const remoteExample = exampleSource
|
|
24
|
+
? await installRemoteExample({
|
|
25
|
+
absoluteTargetPath,
|
|
26
|
+
exampleSource,
|
|
27
|
+
})
|
|
28
|
+
: undefined;
|
|
29
|
+
if (!skipInstall) {
|
|
30
|
+
await install({
|
|
31
|
+
packageManager,
|
|
32
|
+
cwd: absoluteTargetPath,
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
await postInit({
|
|
36
|
+
absoluteTargetPath,
|
|
37
|
+
exampleSelection,
|
|
38
|
+
installedDependencies: !skipInstall,
|
|
39
|
+
});
|
|
40
|
+
const gitInitialized = disableGit ? false : gitInit(absoluteTargetPath);
|
|
41
|
+
return {
|
|
42
|
+
absoluteTargetPath,
|
|
43
|
+
displayPath: relative(process.cwd(), absoluteTargetPath) || absoluteTargetPath,
|
|
44
|
+
packageManager,
|
|
45
|
+
projectName,
|
|
46
|
+
installedDependencies: !skipInstall,
|
|
47
|
+
gitInitialized,
|
|
48
|
+
remoteExample,
|
|
49
|
+
};
|
|
50
|
+
}
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import { assertExampleSelection, assertExampleSource, assertPackageManager } from '../validate.js';
|
|
2
|
+
export function printUsage() {
|
|
3
|
+
console.log('Usage: create-aztec-privacy-template <project-name-or-path> [--pm <bun|npm|pnpm|yarn>] [--example <none|aave|lido|uniswap|all>] [--example-source <github-url|owner/repo[/path][#ref]>] [--yes] [--skip-install] [--disable-git]');
|
|
4
|
+
}
|
|
5
|
+
export function parseArgs(argv) {
|
|
6
|
+
let projectArg = '';
|
|
7
|
+
let packageManager = 'npm';
|
|
8
|
+
let exampleSelection = 'none';
|
|
9
|
+
let exampleSource;
|
|
10
|
+
let yes = false;
|
|
11
|
+
let skipInstall = false;
|
|
12
|
+
let disableGit = false;
|
|
13
|
+
let packageManagerProvided = false;
|
|
14
|
+
let exampleSelectionProvided = false;
|
|
15
|
+
let exampleSourceProvided = false;
|
|
16
|
+
for (let i = 0; i < argv.length; i += 1) {
|
|
17
|
+
const arg = argv[i];
|
|
18
|
+
if (arg === '--help' || arg === '-h') {
|
|
19
|
+
printUsage();
|
|
20
|
+
process.exit(0);
|
|
21
|
+
}
|
|
22
|
+
if (arg === '--yes') {
|
|
23
|
+
yes = true;
|
|
24
|
+
continue;
|
|
25
|
+
}
|
|
26
|
+
if (arg === '--skip-install') {
|
|
27
|
+
skipInstall = true;
|
|
28
|
+
continue;
|
|
29
|
+
}
|
|
30
|
+
if (arg === '--disable-git') {
|
|
31
|
+
disableGit = true;
|
|
32
|
+
continue;
|
|
33
|
+
}
|
|
34
|
+
if (arg === '--pm') {
|
|
35
|
+
const value = argv[i + 1];
|
|
36
|
+
if (!value) {
|
|
37
|
+
throw new Error('--pm requires a value');
|
|
38
|
+
}
|
|
39
|
+
assertPackageManager(value);
|
|
40
|
+
packageManager = value;
|
|
41
|
+
packageManagerProvided = true;
|
|
42
|
+
i += 1;
|
|
43
|
+
continue;
|
|
44
|
+
}
|
|
45
|
+
if (arg.startsWith('--pm=')) {
|
|
46
|
+
const value = arg.slice('--pm='.length);
|
|
47
|
+
assertPackageManager(value);
|
|
48
|
+
packageManager = value;
|
|
49
|
+
packageManagerProvided = true;
|
|
50
|
+
continue;
|
|
51
|
+
}
|
|
52
|
+
if (arg === '--example') {
|
|
53
|
+
const value = argv[i + 1];
|
|
54
|
+
if (!value) {
|
|
55
|
+
throw new Error('--example requires a value');
|
|
56
|
+
}
|
|
57
|
+
assertExampleSelection(value);
|
|
58
|
+
exampleSelection = value;
|
|
59
|
+
exampleSelectionProvided = true;
|
|
60
|
+
i += 1;
|
|
61
|
+
continue;
|
|
62
|
+
}
|
|
63
|
+
if (arg.startsWith('--example=')) {
|
|
64
|
+
const value = arg.slice('--example='.length);
|
|
65
|
+
assertExampleSelection(value);
|
|
66
|
+
exampleSelection = value;
|
|
67
|
+
exampleSelectionProvided = true;
|
|
68
|
+
continue;
|
|
69
|
+
}
|
|
70
|
+
if (arg === '--example-source') {
|
|
71
|
+
const value = argv[i + 1];
|
|
72
|
+
if (!value) {
|
|
73
|
+
throw new Error('--example-source requires a value');
|
|
74
|
+
}
|
|
75
|
+
assertExampleSource(value);
|
|
76
|
+
exampleSource = value;
|
|
77
|
+
exampleSourceProvided = true;
|
|
78
|
+
i += 1;
|
|
79
|
+
continue;
|
|
80
|
+
}
|
|
81
|
+
if (arg.startsWith('--example-source=')) {
|
|
82
|
+
const value = arg.slice('--example-source='.length);
|
|
83
|
+
assertExampleSource(value);
|
|
84
|
+
exampleSource = value;
|
|
85
|
+
exampleSourceProvided = true;
|
|
86
|
+
continue;
|
|
87
|
+
}
|
|
88
|
+
if (arg.startsWith('-')) {
|
|
89
|
+
throw new Error(`Unknown option: ${arg}`);
|
|
90
|
+
}
|
|
91
|
+
if (projectArg) {
|
|
92
|
+
throw new Error(`Unexpected extra argument: ${arg}`);
|
|
93
|
+
}
|
|
94
|
+
projectArg = arg;
|
|
95
|
+
}
|
|
96
|
+
return {
|
|
97
|
+
projectArg: projectArg || undefined,
|
|
98
|
+
packageManager,
|
|
99
|
+
exampleSelection,
|
|
100
|
+
exampleSource,
|
|
101
|
+
yes,
|
|
102
|
+
skipInstall,
|
|
103
|
+
disableGit,
|
|
104
|
+
packageManagerProvided,
|
|
105
|
+
exampleSelectionProvided,
|
|
106
|
+
exampleSourceProvided,
|
|
107
|
+
};
|
|
108
|
+
}
|
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
import { spawn } from 'node:child_process';
|
|
2
|
+
import { cp, mkdtemp, mkdir, readdir, rm, stat, writeFile } from 'node:fs/promises';
|
|
3
|
+
import { tmpdir } from 'node:os';
|
|
4
|
+
import { join, resolve, sep } from 'node:path';
|
|
5
|
+
const DEFAULT_RETRY_ATTEMPTS = 3;
|
|
6
|
+
const DEFAULT_RETRY_DELAY_MS = 400;
|
|
7
|
+
const GITHUB_HOSTS = new Set(['github.com', 'www.github.com']);
|
|
8
|
+
const GITHUB_TOKEN_PATTERN = /^[A-Za-z0-9._-]+$/;
|
|
9
|
+
const defaultDependencies = {
|
|
10
|
+
fetchImpl: fetch,
|
|
11
|
+
extractArchive: extractTarArchive,
|
|
12
|
+
sleep: wait,
|
|
13
|
+
};
|
|
14
|
+
export async function installExampleSource(options, dependencies = defaultDependencies) {
|
|
15
|
+
const parsedSource = parseGithubExampleSource(options.exampleSource);
|
|
16
|
+
const maxAttempts = normalizeRetryAttempts(options.maxAttempts);
|
|
17
|
+
const retryDelayMs = normalizeRetryDelay(options.retryDelayMs);
|
|
18
|
+
let lastError;
|
|
19
|
+
for (let attempt = 1; attempt <= maxAttempts; attempt += 1) {
|
|
20
|
+
try {
|
|
21
|
+
await downloadAndApplyExampleSource({
|
|
22
|
+
absoluteTargetPath: options.absoluteTargetPath,
|
|
23
|
+
parsedSource,
|
|
24
|
+
dependencies,
|
|
25
|
+
});
|
|
26
|
+
return {
|
|
27
|
+
source: parsedSource.normalizedSource,
|
|
28
|
+
applied: true,
|
|
29
|
+
attempts: attempt,
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
catch (error) {
|
|
33
|
+
lastError = error;
|
|
34
|
+
if (attempt < maxAttempts) {
|
|
35
|
+
await dependencies.sleep(retryDelayMs * attempt);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
return {
|
|
40
|
+
source: parsedSource.normalizedSource,
|
|
41
|
+
applied: false,
|
|
42
|
+
attempts: maxAttempts,
|
|
43
|
+
fallbackReason: formatErrorMessage(lastError),
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
export function parseGithubExampleSource(source) {
|
|
47
|
+
const trimmed = source.trim();
|
|
48
|
+
if (!trimmed) {
|
|
49
|
+
throw new Error('Example source cannot be empty.');
|
|
50
|
+
}
|
|
51
|
+
if (trimmed.startsWith('http://') || trimmed.startsWith('https://')) {
|
|
52
|
+
return parseGithubUrlSource(trimmed);
|
|
53
|
+
}
|
|
54
|
+
return parseGithubRepoSource(trimmed);
|
|
55
|
+
}
|
|
56
|
+
async function downloadAndApplyExampleSource({ absoluteTargetPath, parsedSource, dependencies, }) {
|
|
57
|
+
const tempRoot = await mkdtemp(join(tmpdir(), 'capt-example-source-'));
|
|
58
|
+
const archivePath = join(tempRoot, 'source.tar.gz');
|
|
59
|
+
const extractDir = join(tempRoot, 'extract');
|
|
60
|
+
try {
|
|
61
|
+
await mkdir(extractDir, { recursive: true });
|
|
62
|
+
await downloadArchive(parsedSource, archivePath, dependencies.fetchImpl);
|
|
63
|
+
await dependencies.extractArchive(archivePath, extractDir);
|
|
64
|
+
const extractedRoot = await findExtractedRoot(extractDir);
|
|
65
|
+
const sourcePath = resolveSourcePath(extractedRoot, parsedSource.subPath);
|
|
66
|
+
const sourceStats = await stat(sourcePath);
|
|
67
|
+
if (!sourceStats.isDirectory()) {
|
|
68
|
+
throw new Error(`Remote source path is not a directory: ${parsedSource.subPath || '.'}`);
|
|
69
|
+
}
|
|
70
|
+
await cp(sourcePath, absoluteTargetPath, {
|
|
71
|
+
recursive: true,
|
|
72
|
+
errorOnExist: false,
|
|
73
|
+
force: true,
|
|
74
|
+
preserveTimestamps: true,
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
finally {
|
|
78
|
+
await rm(tempRoot, { recursive: true, force: true });
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
async function downloadArchive(parsedSource, archivePath, fetchImpl) {
|
|
82
|
+
const archiveUrl = buildArchiveUrl(parsedSource);
|
|
83
|
+
const response = await fetchImpl(archiveUrl, {
|
|
84
|
+
redirect: 'follow',
|
|
85
|
+
headers: {
|
|
86
|
+
'user-agent': 'create-aztec-privacy-template',
|
|
87
|
+
},
|
|
88
|
+
});
|
|
89
|
+
if (!response.ok) {
|
|
90
|
+
throw new Error(`GitHub archive request failed (${response.status} ${response.statusText}).`);
|
|
91
|
+
}
|
|
92
|
+
const archiveBuffer = Buffer.from(await response.arrayBuffer());
|
|
93
|
+
await writeFile(archivePath, archiveBuffer);
|
|
94
|
+
}
|
|
95
|
+
function buildArchiveUrl(parsedSource) {
|
|
96
|
+
const { owner, repo, ref } = parsedSource;
|
|
97
|
+
return `https://codeload.github.com/${owner}/${repo}/tar.gz/${encodeURIComponent(ref)}`;
|
|
98
|
+
}
|
|
99
|
+
async function extractTarArchive(archivePath, extractDir) {
|
|
100
|
+
await new Promise((resolvePromise, reject) => {
|
|
101
|
+
const child = spawn('tar', ['-xzf', archivePath, '-C', extractDir], {
|
|
102
|
+
stdio: 'ignore',
|
|
103
|
+
});
|
|
104
|
+
child.on('error', (error) => {
|
|
105
|
+
reject(new Error(`Failed to extract remote archive: ${error.message}`));
|
|
106
|
+
});
|
|
107
|
+
child.on('close', (code) => {
|
|
108
|
+
if (code === 0) {
|
|
109
|
+
resolvePromise();
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
reject(new Error(`Failed to extract remote archive (tar exited with code ${code ?? 'unknown'}).`));
|
|
113
|
+
});
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
async function findExtractedRoot(extractDir) {
|
|
117
|
+
const entries = await readdir(extractDir, { withFileTypes: true });
|
|
118
|
+
const rootEntry = entries.find((entry) => entry.isDirectory());
|
|
119
|
+
if (!rootEntry) {
|
|
120
|
+
throw new Error('Remote archive extraction produced no files.');
|
|
121
|
+
}
|
|
122
|
+
return join(extractDir, rootEntry.name);
|
|
123
|
+
}
|
|
124
|
+
function resolveSourcePath(extractedRoot, subPath) {
|
|
125
|
+
if (!subPath) {
|
|
126
|
+
return extractedRoot;
|
|
127
|
+
}
|
|
128
|
+
const candidate = resolve(extractedRoot, subPath);
|
|
129
|
+
if (candidate === extractedRoot || candidate.startsWith(`${extractedRoot}${sep}`)) {
|
|
130
|
+
return candidate;
|
|
131
|
+
}
|
|
132
|
+
throw new Error('Remote source path escapes extracted archive.');
|
|
133
|
+
}
|
|
134
|
+
function parseGithubUrlSource(rawSource) {
|
|
135
|
+
if (rawSource.trim().endsWith('#')) {
|
|
136
|
+
throw new Error('GitHub source ref cannot be empty after "#".');
|
|
137
|
+
}
|
|
138
|
+
const url = new URL(rawSource);
|
|
139
|
+
if (!GITHUB_HOSTS.has(url.hostname)) {
|
|
140
|
+
throw new Error('Example source URL must point to github.com.');
|
|
141
|
+
}
|
|
142
|
+
const segments = url.pathname
|
|
143
|
+
.split('/')
|
|
144
|
+
.filter(Boolean)
|
|
145
|
+
.map((segment) => decodeURIComponent(segment));
|
|
146
|
+
if (segments.length < 2) {
|
|
147
|
+
throw new Error('GitHub example URL must include owner and repository.');
|
|
148
|
+
}
|
|
149
|
+
const owner = validateGitHubToken(segments[0], 'owner');
|
|
150
|
+
const repo = validateGitHubToken(stripGitSuffix(segments[1]), 'repository');
|
|
151
|
+
const refFromHash = parseUrlHashRef(url);
|
|
152
|
+
let ref = refFromHash ?? 'HEAD';
|
|
153
|
+
let subPath = '';
|
|
154
|
+
if (segments[2] === 'tree') {
|
|
155
|
+
if (!segments[3]) {
|
|
156
|
+
throw new Error('GitHub tree URL must include a branch or tag.');
|
|
157
|
+
}
|
|
158
|
+
ref = refFromHash ?? segments[3];
|
|
159
|
+
subPath = segments.slice(4).join('/');
|
|
160
|
+
}
|
|
161
|
+
else if (segments.length > 2) {
|
|
162
|
+
subPath = segments.slice(2).join('/');
|
|
163
|
+
}
|
|
164
|
+
return toParsedGithubSource(owner, repo, ref, subPath);
|
|
165
|
+
}
|
|
166
|
+
function parseGithubRepoSource(rawSource) {
|
|
167
|
+
const hashIndex = rawSource.indexOf('#');
|
|
168
|
+
let mainPart = rawSource;
|
|
169
|
+
let refFromHash;
|
|
170
|
+
if (hashIndex !== -1) {
|
|
171
|
+
mainPart = rawSource.slice(0, hashIndex);
|
|
172
|
+
refFromHash = rawSource.slice(hashIndex + 1).trim();
|
|
173
|
+
if (!refFromHash) {
|
|
174
|
+
throw new Error('GitHub source ref cannot be empty after "#".');
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
const segments = mainPart
|
|
178
|
+
.split('/')
|
|
179
|
+
.filter(Boolean)
|
|
180
|
+
.map((segment) => decodeURIComponent(segment));
|
|
181
|
+
if (segments.length < 2) {
|
|
182
|
+
throw new Error('GitHub repo source must use "<owner>/<repo>" format.');
|
|
183
|
+
}
|
|
184
|
+
const owner = validateGitHubToken(segments[0], 'owner');
|
|
185
|
+
const repo = validateGitHubToken(stripGitSuffix(segments[1]), 'repository');
|
|
186
|
+
let ref = refFromHash ?? 'HEAD';
|
|
187
|
+
let subPath = '';
|
|
188
|
+
if (segments[2] === 'tree') {
|
|
189
|
+
if (!segments[3]) {
|
|
190
|
+
throw new Error('GitHub tree source must include a branch or tag.');
|
|
191
|
+
}
|
|
192
|
+
ref = refFromHash ?? segments[3];
|
|
193
|
+
subPath = segments.slice(4).join('/');
|
|
194
|
+
}
|
|
195
|
+
else if (segments.length > 2) {
|
|
196
|
+
subPath = segments.slice(2).join('/');
|
|
197
|
+
}
|
|
198
|
+
return toParsedGithubSource(owner, repo, ref, subPath);
|
|
199
|
+
}
|
|
200
|
+
function validateGitHubToken(value, label) {
|
|
201
|
+
if (GITHUB_TOKEN_PATTERN.test(value)) {
|
|
202
|
+
return value;
|
|
203
|
+
}
|
|
204
|
+
throw new Error(`Invalid GitHub ${label} in example source: "${value}".`);
|
|
205
|
+
}
|
|
206
|
+
function stripGitSuffix(repo) {
|
|
207
|
+
return repo.endsWith('.git') ? repo.slice(0, -4) : repo;
|
|
208
|
+
}
|
|
209
|
+
function parseUrlHashRef(url) {
|
|
210
|
+
if (!url.hash) {
|
|
211
|
+
return undefined;
|
|
212
|
+
}
|
|
213
|
+
const rawRef = url.hash.slice(1).trim();
|
|
214
|
+
if (!rawRef) {
|
|
215
|
+
throw new Error('GitHub source ref cannot be empty after "#".');
|
|
216
|
+
}
|
|
217
|
+
try {
|
|
218
|
+
return decodeURIComponent(rawRef);
|
|
219
|
+
}
|
|
220
|
+
catch {
|
|
221
|
+
throw new Error('GitHub source ref is not valid URL encoding.');
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
function toParsedGithubSource(owner, repo, ref, subPath) {
|
|
225
|
+
const normalizedRef = ref.trim() || 'HEAD';
|
|
226
|
+
const normalizedSubPath = subPath.replace(/^\/+|\/+$/g, '');
|
|
227
|
+
const suffix = normalizedSubPath ? `/${normalizedSubPath}` : '';
|
|
228
|
+
return {
|
|
229
|
+
owner,
|
|
230
|
+
repo,
|
|
231
|
+
ref: normalizedRef,
|
|
232
|
+
subPath: normalizedSubPath,
|
|
233
|
+
normalizedSource: `https://github.com/${owner}/${repo}/tree/${normalizedRef}${suffix}`,
|
|
234
|
+
};
|
|
235
|
+
}
|
|
236
|
+
function normalizeRetryAttempts(value) {
|
|
237
|
+
if (value === undefined) {
|
|
238
|
+
return DEFAULT_RETRY_ATTEMPTS;
|
|
239
|
+
}
|
|
240
|
+
if (!Number.isInteger(value) || value < 1) {
|
|
241
|
+
throw new Error('maxAttempts must be a positive integer.');
|
|
242
|
+
}
|
|
243
|
+
return value;
|
|
244
|
+
}
|
|
245
|
+
function normalizeRetryDelay(value) {
|
|
246
|
+
if (value === undefined) {
|
|
247
|
+
return DEFAULT_RETRY_DELAY_MS;
|
|
248
|
+
}
|
|
249
|
+
if (!Number.isFinite(value) || value < 0) {
|
|
250
|
+
throw new Error('retryDelayMs must be a non-negative number.');
|
|
251
|
+
}
|
|
252
|
+
return value;
|
|
253
|
+
}
|
|
254
|
+
function formatErrorMessage(error) {
|
|
255
|
+
if (error instanceof Error) {
|
|
256
|
+
return error.message;
|
|
257
|
+
}
|
|
258
|
+
return String(error);
|
|
259
|
+
}
|
|
260
|
+
async function wait(delayMs) {
|
|
261
|
+
await new Promise((resolvePromise) => {
|
|
262
|
+
setTimeout(resolvePromise, delayMs);
|
|
263
|
+
});
|
|
264
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { execSync } from 'node:child_process';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import { rmSync } from 'node:fs';
|
|
4
|
+
function isInsideGitRepository(cwd) {
|
|
5
|
+
try {
|
|
6
|
+
execSync('git rev-parse --is-inside-work-tree', { cwd, stdio: 'ignore' });
|
|
7
|
+
return true;
|
|
8
|
+
}
|
|
9
|
+
catch {
|
|
10
|
+
return false;
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
function isInsideMercurialRepository(cwd) {
|
|
14
|
+
try {
|
|
15
|
+
execSync('hg --cwd . root', { cwd, stdio: 'ignore' });
|
|
16
|
+
return true;
|
|
17
|
+
}
|
|
18
|
+
catch {
|
|
19
|
+
return false;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
function hasGitBinary() {
|
|
23
|
+
try {
|
|
24
|
+
execSync('git --version', { stdio: 'ignore' });
|
|
25
|
+
return true;
|
|
26
|
+
}
|
|
27
|
+
catch {
|
|
28
|
+
return false;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
function hasDefaultBranchConfig(cwd) {
|
|
32
|
+
try {
|
|
33
|
+
execSync('git config init.defaultBranch', { cwd, stdio: 'ignore' });
|
|
34
|
+
return true;
|
|
35
|
+
}
|
|
36
|
+
catch {
|
|
37
|
+
return false;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
export function tryGitInit(root) {
|
|
41
|
+
if (!hasGitBinary()) {
|
|
42
|
+
return false;
|
|
43
|
+
}
|
|
44
|
+
if (isInsideGitRepository(root) || isInsideMercurialRepository(root)) {
|
|
45
|
+
return false;
|
|
46
|
+
}
|
|
47
|
+
let initialized = false;
|
|
48
|
+
try {
|
|
49
|
+
execSync('git init', { cwd: root, stdio: 'ignore' });
|
|
50
|
+
initialized = true;
|
|
51
|
+
if (!hasDefaultBranchConfig(root)) {
|
|
52
|
+
// Use branch -M so this remains safe if HEAD is already "main".
|
|
53
|
+
execSync('git branch -M main', { cwd: root, stdio: 'ignore' });
|
|
54
|
+
}
|
|
55
|
+
execSync('git add -A', { cwd: root, stdio: 'ignore' });
|
|
56
|
+
execSync('git commit -m "Initial commit from create-aztec-privacy-template"', {
|
|
57
|
+
cwd: root,
|
|
58
|
+
stdio: 'ignore',
|
|
59
|
+
});
|
|
60
|
+
return true;
|
|
61
|
+
}
|
|
62
|
+
catch {
|
|
63
|
+
if (initialized) {
|
|
64
|
+
try {
|
|
65
|
+
rmSync(join(root, '.git'), { recursive: true, force: true });
|
|
66
|
+
}
|
|
67
|
+
catch {
|
|
68
|
+
// Ignore cleanup failures after a failed git init attempt.
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
return false;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { spawn } from 'node:child_process';
|
|
2
|
+
export async function installDependencies(options) {
|
|
3
|
+
const { packageManager, cwd } = options;
|
|
4
|
+
const args = ['install'];
|
|
5
|
+
const env = {
|
|
6
|
+
...process.env,
|
|
7
|
+
NODE_ENV: 'development',
|
|
8
|
+
};
|
|
9
|
+
if (packageManager === 'yarn') {
|
|
10
|
+
// Yarn Berry enables immutable installs on CI by default, which blocks
|
|
11
|
+
// first-time lockfile generation in freshly scaffolded projects.
|
|
12
|
+
env.YARN_ENABLE_IMMUTABLE_INSTALLS = 'false';
|
|
13
|
+
}
|
|
14
|
+
await new Promise((resolve, reject) => {
|
|
15
|
+
const child = spawn(packageManager, args, {
|
|
16
|
+
cwd,
|
|
17
|
+
stdio: 'inherit',
|
|
18
|
+
env,
|
|
19
|
+
});
|
|
20
|
+
child.on('close', (code) => {
|
|
21
|
+
if (code === 0) {
|
|
22
|
+
resolve();
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
reject(new Error(`Dependency install failed: ${packageManager} ${args.join(' ')} (exit code ${code ?? 'unknown'})`));
|
|
26
|
+
});
|
|
27
|
+
child.on('error', (error) => {
|
|
28
|
+
const spawnError = error;
|
|
29
|
+
if (spawnError.code === 'ENOENT') {
|
|
30
|
+
reject(new Error(`Package manager "${packageManager}" is not installed or not available on PATH. Install it or rerun with --pm <bun|npm|pnpm|yarn>.`));
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
reject(new Error(`Failed to spawn installer ${packageManager}: ${error.message}`));
|
|
34
|
+
});
|
|
35
|
+
});
|
|
36
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { access, chmod } from 'node:fs/promises';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
const POST_INIT_HOOKS = [
|
|
4
|
+
verifyRequiredLayoutHook,
|
|
5
|
+
ensureCompileScriptExecutableHook,
|
|
6
|
+
];
|
|
7
|
+
export async function runPostInitHooks(context) {
|
|
8
|
+
for (const hook of POST_INIT_HOOKS) {
|
|
9
|
+
await hook(context);
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
async function verifyRequiredLayoutHook(context) {
|
|
13
|
+
const requiredPaths = [
|
|
14
|
+
'README.md',
|
|
15
|
+
'package.json',
|
|
16
|
+
join('contracts', 'l1'),
|
|
17
|
+
join('contracts', 'aztec'),
|
|
18
|
+
join('scripts', 'compile-aztec-contract.sh'),
|
|
19
|
+
];
|
|
20
|
+
if (context.exampleSelection === 'all') {
|
|
21
|
+
requiredPaths.push(join('examples', 'aave'), join('examples', 'lido'), join('examples', 'uniswap'));
|
|
22
|
+
}
|
|
23
|
+
else if (context.exampleSelection !== 'none') {
|
|
24
|
+
requiredPaths.push(join('examples', context.exampleSelection));
|
|
25
|
+
}
|
|
26
|
+
for (const relativePath of requiredPaths) {
|
|
27
|
+
await access(join(context.absoluteTargetPath, relativePath));
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
async function ensureCompileScriptExecutableHook(context) {
|
|
31
|
+
const compileScript = join(context.absoluteTargetPath, 'scripts', 'compile-aztec-contract.sh');
|
|
32
|
+
await chmod(compileScript, 0o755);
|
|
33
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { writeFile } from 'node:fs/promises';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import { STARTER_PACKAGE_JSON_BASE, } from '../constants.js';
|
|
4
|
+
import { applyPlaceholdersInSelectedFiles, assertNoUnresolvedPlaceholders, getPlaceholderMap, } from '../placeholders.js';
|
|
5
|
+
import { installTemplatePlan, resolveTemplateInstallPlan } from '../templates/index.js';
|
|
6
|
+
export async function scaffoldTemplate(options) {
|
|
7
|
+
const { generatorRoot, absoluteTargetPath, projectName, packageManager, exampleSelection = 'none', } = options;
|
|
8
|
+
const templatePlan = resolveTemplateInstallPlan(generatorRoot, exampleSelection);
|
|
9
|
+
await installTemplatePlan(absoluteTargetPath, templatePlan);
|
|
10
|
+
const packageJson = {
|
|
11
|
+
...STARTER_PACKAGE_JSON_BASE,
|
|
12
|
+
name: projectName,
|
|
13
|
+
};
|
|
14
|
+
await writeFile(join(absoluteTargetPath, 'package.json'), `${JSON.stringify(packageJson, null, 2)}\n`, 'utf8');
|
|
15
|
+
const placeholderMap = getPlaceholderMap(projectName, packageManager);
|
|
16
|
+
await applyPlaceholdersInSelectedFiles(absoluteTargetPath, placeholderMap);
|
|
17
|
+
await assertNoUnresolvedPlaceholders(absoluteTargetPath);
|
|
18
|
+
}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { INSTALL_COMMANDS } from './constants.js';
|
|
2
|
+
import { createApp } from './create-app.js';
|
|
3
|
+
import { parseArgs } from './helpers/cli-options.js';
|
|
4
|
+
import { dirname, resolve } from 'node:path';
|
|
5
|
+
import { fileURLToPath } from 'node:url';
|
|
6
|
+
import { resolvePromptOptions } from './prompts.js';
|
|
7
|
+
export async function run(argv, importMetaUrl) {
|
|
8
|
+
const parsedArgs = parseArgs(argv);
|
|
9
|
+
const args = await resolvePromptOptions(parsedArgs);
|
|
10
|
+
const generatorRoot = resolveGeneratorRoot(importMetaUrl);
|
|
11
|
+
const result = await createApp({
|
|
12
|
+
generatorRoot,
|
|
13
|
+
projectArg: args.projectArg,
|
|
14
|
+
packageManager: args.packageManager,
|
|
15
|
+
exampleSelection: args.exampleSelection,
|
|
16
|
+
exampleSource: args.exampleSource,
|
|
17
|
+
skipInstall: args.skipInstall,
|
|
18
|
+
disableGit: args.disableGit,
|
|
19
|
+
});
|
|
20
|
+
console.log(`\nScaffolded Aztec privacy starter at ${result.displayPath}`);
|
|
21
|
+
if (result.gitInitialized) {
|
|
22
|
+
console.log('Initialized a git repository.');
|
|
23
|
+
}
|
|
24
|
+
else if (args.disableGit) {
|
|
25
|
+
console.log('Skipped git initialization.');
|
|
26
|
+
}
|
|
27
|
+
else {
|
|
28
|
+
console.log('Git initialization was skipped (git unavailable or commit failed).');
|
|
29
|
+
}
|
|
30
|
+
if (result.remoteExample) {
|
|
31
|
+
if (result.remoteExample.applied) {
|
|
32
|
+
console.log(`Applied remote example source: ${result.remoteExample.source}`);
|
|
33
|
+
}
|
|
34
|
+
else {
|
|
35
|
+
const reason = result.remoteExample.fallbackReason
|
|
36
|
+
? ` (${result.remoteExample.fallbackReason})`
|
|
37
|
+
: '';
|
|
38
|
+
console.log(`Remote example source unavailable${reason}. Falling back to local built-in examples.`);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
console.log('\nNext steps:');
|
|
42
|
+
console.log(` cd ${result.displayPath}`);
|
|
43
|
+
if (!result.installedDependencies) {
|
|
44
|
+
console.log(` ${INSTALL_COMMANDS[result.packageManager]}`);
|
|
45
|
+
}
|
|
46
|
+
console.log(' make check');
|
|
47
|
+
}
|
|
48
|
+
function resolveGeneratorRoot(importMetaUrl) {
|
|
49
|
+
return resolve(dirname(fileURLToPath(importMetaUrl)), '..');
|
|
50
|
+
}
|