@emeryld/manager 1.3.1 → 1.4.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/README.md +31 -0
- package/dist/create-package/cli-args.js +78 -0
- package/dist/create-package/prompts.js +138 -0
- package/dist/create-package/shared/configs.js +309 -0
- package/dist/create-package/shared/constants.js +5 -0
- package/dist/create-package/shared/fs-utils.js +69 -0
- package/dist/create-package/tasks.js +89 -0
- package/dist/create-package/types.js +1 -0
- package/dist/create-package/variant-info.js +67 -0
- package/dist/create-package/variants/client/expo-react-native/lib-files.js +168 -0
- package/dist/create-package/variants/client/expo-react-native/package-files.js +94 -0
- package/dist/create-package/variants/client/expo-react-native/scaffold.js +59 -0
- package/dist/create-package/variants/client/expo-react-native/ui-files.js +215 -0
- package/dist/create-package/variants/client/vite-react/health-page.js +251 -0
- package/dist/create-package/variants/client/vite-react/lib-files.js +176 -0
- package/dist/create-package/variants/client/vite-react/package-files.js +79 -0
- package/dist/create-package/variants/client/vite-react/scaffold.js +68 -0
- package/dist/create-package/variants/client/vite-react/ui-files.js +154 -0
- package/dist/create-package/variants/fullstack/files.js +129 -0
- package/dist/create-package/variants/fullstack/index.js +86 -0
- package/dist/create-package/variants/fullstack/utils.js +241 -0
- package/dist/llm-pack.js +2 -0
- package/dist/robot/cli/prompts.js +84 -33
- package/dist/robot/cli/settings.js +128 -69
- package/dist/robot/config.js +121 -59
- package/dist/robot/coordinator.js +10 -109
- package/dist/robot/llm-pack.js +1226 -0
- package/dist/robot/pack/builder.js +374 -0
- package/dist/robot/pack/cli.js +65 -0
- package/dist/robot/pack/exemplars.js +573 -0
- package/dist/robot/pack/globs.js +119 -0
- package/dist/robot/pack/selection.js +44 -0
- package/dist/robot/pack/symbols.js +309 -0
- package/dist/robot/pack/type-registry.js +285 -0
- package/dist/robot/pack/types.js +48 -0
- package/dist/robot/pack/utils.js +36 -0
- package/dist/robot/v2/cli.js +86 -0
- package/dist/robot/v2/globs.js +103 -0
- package/dist/robot/v2/parser/bundles.js +55 -0
- package/dist/robot/v2/parser/candidates.js +63 -0
- package/dist/robot/v2/parser/exemplars.js +114 -0
- package/dist/robot/v2/parser/exports.js +57 -0
- package/dist/robot/v2/parser/symbols.js +179 -0
- package/dist/robot/v2/parser.js +114 -0
- package/dist/robot/v2/types.js +42 -0
- package/dist/utils/export.js +39 -18
- package/package.json +2 -1
package/README.md
CHANGED
|
@@ -92,6 +92,37 @@ The same knobs live in `.vscode/settings.json` under `manager.robot`, for exampl
|
|
|
92
92
|
}
|
|
93
93
|
```
|
|
94
94
|
|
|
95
|
+
### Robot pack settings reference
|
|
96
|
+
|
|
97
|
+
The same `manager.robot` block that backs the metadata helper is merged with the defaults listed in `src/robot/pack/types.ts` before every robot pack build. The helper always uses the directory you select at runtime as the `rootDir`, so you only need to override the other keys. Here is what each option controls:
|
|
98
|
+
|
|
99
|
+
| Setting | Description | Default |
|
|
100
|
+
| --- | --- | --- |
|
|
101
|
+
| `tsconfigPath` | Optional path (relative to the selected root) pointing to a `tsconfig.json`. When omitted the builder climbs parent directories until it finds one. | Auto-detect the nearest `tsconfig.json`. |
|
|
102
|
+
| `includeGlobs` | Glob patterns that determine which `.ts/.tsx` files are scanned. | `["src/**/*.{ts,tsx}"]` |
|
|
103
|
+
| `excludeGlobs` | Globs that are skipped (tests, build output, declaration files, etc.) so they never contribute symbols. | `["**/*.test.*", "**/*.spec.*", "**/__tests__/**", "**/dist/**", "**/build/**", "**/*.d.ts"]` |
|
|
104
|
+
| `entrypoints` | Modules that seed the exported-symbol walk when `exportMode` is `entrypoints`. If none of the globs match, the first included file is used as a fallback so the pack is never empty. | `["src/index.ts", "src/**/index.ts"]` |
|
|
105
|
+
| `exportMode` | Choose between `entrypoints` (only mirror the modules matched above) and `all-files` (treat every included file as an entrypoint). Use `entrypoints` to scope the pack to a few roots; `all-files` is the default so nothing is accidentally omitted. | `all-files` |
|
|
106
|
+
| `visibility` | `exported-only` (default) keeps only exported symbols, `exported+reexported` also follows re-exports from other modules, and `all` additionally captures the local declarations that live in the same file. | `exported-only` |
|
|
107
|
+
| `includeKinds` | Which symbol kinds to keep—choose any combination of `function`, `class`, `interface`, `type-alias`, `enum`, and `const` to filter the pack down to your preferred abstractions. | `["function", "class", "interface", "type-alias", "enum", "const"]` |
|
|
108
|
+
| `closure` | `surface-only` keeps just the exported declarations, while `surface+deps` (default) also includes their dependency closure. The latter also enables trimming dependency types when a `tokenBudget` is in play. | `surface+deps` |
|
|
109
|
+
| `maxExemplars` | Maximum number of exemplar functions/classes that are appended to the pack. The builder also caps each module to about half this number so the samples cover more files. | `8` |
|
|
110
|
+
| `tokenBudget` | Optional cap on estimated tokens (pack text length ÷ 4). When the estimate exceeds the budget the helper drops exemplars first, then—if `closure` is `surface+deps`—it drops dependency type declarations until the estimate fits. Leave blank for unlimited. | Unlimited |
|
|
111
|
+
| `preferTypeSurface` | Accepted for compatibility and shown in the interactive prompt, but the current builder ignores this flag. | `true` |
|
|
112
|
+
| `exemplarHeuristics` | Weights that tune the new utility-per-token scoring (usage, boundary, flow, clarity, redundancy) plus the token penalty scale. | `{"usage":6,"boundary":1.2,"flow":1.3,"clarity":0.8,"redundancy":0.6,"tau":280}` |
|
|
113
|
+
| `keepJSDocTags` | List of JSDoc tags that survive the minification step when exemplar bodies are emitted. Tags lacking an `@` automatically get one added. | `["@deprecated", "@throws", "@pure", "@sideEffects", "@internal", "@public", "@experimental", "@llm"]` |
|
|
114
|
+
|
|
115
|
+
`exemplarHeuristics` now drives a utility-per-token score that favors usage-focused, boundary-aware, and flow-respecting chunks while penalizing redundancy and excessive size.
|
|
116
|
+
|
|
117
|
+
- `usage` (default `6`) strongly weights snippets that show real call-site arguments, defaults, or error handling.
|
|
118
|
+
- `boundary` (default `1.2`) favors chunks that touch configuration, CLI wiring, filesystem/serialization, or output paths.
|
|
119
|
+
- `flow` (default `1.3`) boosts exemplars on entrypoint-to-output paths, including the traced flow exemplar when available.
|
|
120
|
+
- `clarity` (default `0.8`) rewards clear naming, preserved doc tags, and explicit error strings.
|
|
121
|
+
- `redundancy` (default `0.6`) subtracts from candidates that closely overlap already-selected snippets.
|
|
122
|
+
- `tau` (default `280`) sets the denominator for the size penalty, anchoring chunk length around ~200–300 tokens.
|
|
123
|
+
|
|
124
|
+
Every exemplar is paired with metadata (`purpose`, `when_relevant`, `symbols_shown`) plus a declared purpose bucket (orchestration/pipeline, configuration/normalization, parsing/validation, selection/filtering, core domain algorithm, or I/O boundary) so downstream LLM consumers can immediately understand why the snippet matters, when to consult it, and which angle it covers.
|
|
125
|
+
|
|
95
126
|
### Example output
|
|
96
127
|
`robot metadata` prints a summary followed by the parsed payload. With the default formatting you get readable JSON:
|
|
97
128
|
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { colors, logGlobal } from '../utils/log.js';
|
|
2
|
+
export function parseCreateCliArgs(argv) {
|
|
3
|
+
const options = {};
|
|
4
|
+
for (let i = 0; i < argv.length; i++) {
|
|
5
|
+
const arg = argv[i];
|
|
6
|
+
if (arg === '--list' || arg === '-l' || arg === 'list' || arg === 'ls') {
|
|
7
|
+
options.list = true;
|
|
8
|
+
continue;
|
|
9
|
+
}
|
|
10
|
+
if (arg === '--describe' || arg === '-d' || arg === 'describe') {
|
|
11
|
+
options.describe = argv[++i];
|
|
12
|
+
continue;
|
|
13
|
+
}
|
|
14
|
+
if (arg === '--variant' || arg === '-v') {
|
|
15
|
+
options.variant = argv[++i];
|
|
16
|
+
continue;
|
|
17
|
+
}
|
|
18
|
+
if (arg === '--dir' || arg === '--path' || arg === '-p') {
|
|
19
|
+
options.targetDir = argv[++i];
|
|
20
|
+
continue;
|
|
21
|
+
}
|
|
22
|
+
if (arg === '--name' || arg === '-n') {
|
|
23
|
+
options.pkgName = argv[++i];
|
|
24
|
+
continue;
|
|
25
|
+
}
|
|
26
|
+
if (arg === '--contract') {
|
|
27
|
+
options.contractName = argv[++i];
|
|
28
|
+
continue;
|
|
29
|
+
}
|
|
30
|
+
if (arg === '--client-kind' || arg === '--client') {
|
|
31
|
+
options.clientKind = argv[++i];
|
|
32
|
+
continue;
|
|
33
|
+
}
|
|
34
|
+
if (arg === '--skip-install') {
|
|
35
|
+
options.skipInstall = true;
|
|
36
|
+
continue;
|
|
37
|
+
}
|
|
38
|
+
if (arg === '--skip-build') {
|
|
39
|
+
options.skipBuild = true;
|
|
40
|
+
continue;
|
|
41
|
+
}
|
|
42
|
+
if (arg === '--reset') {
|
|
43
|
+
options.reset = true;
|
|
44
|
+
continue;
|
|
45
|
+
}
|
|
46
|
+
if (arg === '--help' || arg === '-h') {
|
|
47
|
+
options.help = true;
|
|
48
|
+
continue;
|
|
49
|
+
}
|
|
50
|
+
if (!arg.startsWith('-') && !options.variant) {
|
|
51
|
+
options.variant = arg;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
return options;
|
|
55
|
+
}
|
|
56
|
+
export function printCreateHelp() {
|
|
57
|
+
logGlobal('Create package help', colors.magenta);
|
|
58
|
+
console.log('Usage:');
|
|
59
|
+
console.log(' pnpm manager-cli create # interactive prompts');
|
|
60
|
+
console.log(' pnpm manager-cli create --list # list templates');
|
|
61
|
+
console.log(' pnpm manager-cli create --describe rrr-server');
|
|
62
|
+
console.log(' pnpm manager-cli create --variant rrr-client --dir packages/rrr-client --name @scope/client');
|
|
63
|
+
console.log(' pnpm manager-cli create --variant rrr-server --contract @scope/contract --skip-install');
|
|
64
|
+
console.log(' pnpm manager-cli create --variant rrr-server --reset # blow away an existing target before scaffolding');
|
|
65
|
+
console.log('');
|
|
66
|
+
console.log('Flags:');
|
|
67
|
+
console.log(' --list, -l Show available templates');
|
|
68
|
+
console.log(' --describe, -d Print details for a template');
|
|
69
|
+
console.log(' --variant, -v Pick a template by id/label (skips variant prompt)');
|
|
70
|
+
console.log(' --dir, --path, -p Target directory (skips path prompt)');
|
|
71
|
+
console.log(' --name, -n Package name (skips name prompt)');
|
|
72
|
+
console.log(' --contract Contract import to inject (server/client variants)');
|
|
73
|
+
console.log(' --client-kind Client stack (vite-react | expo-react-native)');
|
|
74
|
+
console.log(' --reset Remove the target directory if it already exists');
|
|
75
|
+
console.log(' --skip-install Do not run pnpm install after scaffolding');
|
|
76
|
+
console.log(' --skip-build Skip build after scaffolding');
|
|
77
|
+
console.log(' --help, -h Show this help');
|
|
78
|
+
}
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import { colors, logGlobal } from '../utils/log.js';
|
|
3
|
+
import { runHelperCli } from '../helper-cli.js';
|
|
4
|
+
import { askLine } from '../prompts.js';
|
|
5
|
+
import { loadPackages } from '../packages.js';
|
|
6
|
+
import { ensureTargetDir } from './tasks.js';
|
|
7
|
+
import { derivePackageName, VARIANTS } from './variant-info.js';
|
|
8
|
+
import { CLIENT_KIND_OPTIONS, DEFAULT_CLIENT_KIND, normalizeClientKind, } from './variants/client.js';
|
|
9
|
+
import { workspaceRoot } from './shared.js';
|
|
10
|
+
async function promptForContractNameWithHelper(title, options) {
|
|
11
|
+
let selection;
|
|
12
|
+
const scripts = options.map((opt) => ({
|
|
13
|
+
name: opt.label,
|
|
14
|
+
description: opt.meta ?? '',
|
|
15
|
+
emoji: '📦',
|
|
16
|
+
handler: () => {
|
|
17
|
+
selection = opt.value;
|
|
18
|
+
},
|
|
19
|
+
}));
|
|
20
|
+
scripts.push({
|
|
21
|
+
name: 'Enter manually',
|
|
22
|
+
emoji: '⌨️',
|
|
23
|
+
description: 'Type a contract package name',
|
|
24
|
+
handler: async () => {
|
|
25
|
+
const manual = await askLine('Contract package name (e.g. @scope/contract): ');
|
|
26
|
+
selection = manual.trim() || undefined;
|
|
27
|
+
},
|
|
28
|
+
});
|
|
29
|
+
scripts.push({
|
|
30
|
+
name: 'Skip (no contract)',
|
|
31
|
+
emoji: '⏭️',
|
|
32
|
+
description: 'Continue without a contract dependency',
|
|
33
|
+
handler: () => {
|
|
34
|
+
selection = undefined;
|
|
35
|
+
},
|
|
36
|
+
});
|
|
37
|
+
await runHelperCli({ title, scripts, argv: [] });
|
|
38
|
+
return selection;
|
|
39
|
+
}
|
|
40
|
+
export async function promptForContractName() {
|
|
41
|
+
let packages = [];
|
|
42
|
+
try {
|
|
43
|
+
packages = await loadPackages();
|
|
44
|
+
}
|
|
45
|
+
catch (error) {
|
|
46
|
+
const message = error instanceof Error ? error.message : 'unknown error discovering packages';
|
|
47
|
+
logGlobal(`Could not auto-discover packages (${message}).`, colors.yellow);
|
|
48
|
+
}
|
|
49
|
+
const contractOptions = packages
|
|
50
|
+
.filter((pkg) => pkg.relativeDir !== '.')
|
|
51
|
+
.map((pkg) => ({
|
|
52
|
+
value: pkg.name,
|
|
53
|
+
label: pkg.name,
|
|
54
|
+
meta: pkg.relativeDir,
|
|
55
|
+
}));
|
|
56
|
+
if (contractOptions.length === 0) {
|
|
57
|
+
const manual = await askLine('Contract package name? (Enter to skip, e.g. @scope/contract): ');
|
|
58
|
+
return manual.trim() || undefined;
|
|
59
|
+
}
|
|
60
|
+
return promptForContractNameWithHelper('Select a contract package (or skip)', contractOptions);
|
|
61
|
+
}
|
|
62
|
+
export async function promptForClientKind(existing) {
|
|
63
|
+
const defaultKind = normalizeClientKind(existing) ?? DEFAULT_CLIENT_KIND;
|
|
64
|
+
let selection;
|
|
65
|
+
const scripts = CLIENT_KIND_OPTIONS.map((opt) => ({
|
|
66
|
+
name: opt.label,
|
|
67
|
+
emoji: opt.id == 'vite-react' ? '💻' : opt.id == 'expo-react-native' ? '📱' : '🌐',
|
|
68
|
+
description: opt.summary,
|
|
69
|
+
handler: () => {
|
|
70
|
+
selection = opt.id;
|
|
71
|
+
},
|
|
72
|
+
}));
|
|
73
|
+
await runHelperCli({
|
|
74
|
+
title: 'Select a client template',
|
|
75
|
+
scripts,
|
|
76
|
+
argv: [],
|
|
77
|
+
});
|
|
78
|
+
return selection ?? defaultKind;
|
|
79
|
+
}
|
|
80
|
+
export async function promptForVariant() {
|
|
81
|
+
let selection;
|
|
82
|
+
const scripts = VARIANTS.map((variant) => ({
|
|
83
|
+
name: variant.label,
|
|
84
|
+
emoji: '✨',
|
|
85
|
+
description: variant.summary
|
|
86
|
+
? `${variant.summary} · ${variant.defaultDir}`
|
|
87
|
+
: variant.defaultDir,
|
|
88
|
+
handler: () => {
|
|
89
|
+
selection = variant;
|
|
90
|
+
},
|
|
91
|
+
}));
|
|
92
|
+
while (!selection) {
|
|
93
|
+
await runHelperCli({
|
|
94
|
+
title: 'Pick a package template',
|
|
95
|
+
scripts,
|
|
96
|
+
argv: [],
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
return selection;
|
|
100
|
+
}
|
|
101
|
+
export async function promptForTargetDir(fallback) {
|
|
102
|
+
const answer = await askLine(`Path for the new package? (${fallback}): `);
|
|
103
|
+
const normalized = answer || fallback;
|
|
104
|
+
return path.resolve(workspaceRoot, normalized);
|
|
105
|
+
}
|
|
106
|
+
export async function gatherTarget(initial = {}) {
|
|
107
|
+
const variant = initial.variant ?? (await promptForVariant());
|
|
108
|
+
const targetDir = initial.targetDir !== undefined
|
|
109
|
+
? path.resolve(workspaceRoot, initial.targetDir)
|
|
110
|
+
: await promptForTargetDir(variant.defaultDir);
|
|
111
|
+
const fallbackName = derivePackageName(targetDir);
|
|
112
|
+
const nameAnswer = initial.pkgName === undefined
|
|
113
|
+
? await askLine(`Package name? (${fallbackName}): `)
|
|
114
|
+
: initial.pkgName;
|
|
115
|
+
const pkgName = (nameAnswer || fallbackName).trim() || fallbackName;
|
|
116
|
+
await ensureTargetDir(targetDir, { reset: initial.reset });
|
|
117
|
+
let contractName = initial.contractName;
|
|
118
|
+
if ((variant.id === 'rrr-server' || variant.id === 'rrr-client') &&
|
|
119
|
+
contractName === undefined) {
|
|
120
|
+
contractName = await promptForContractName();
|
|
121
|
+
}
|
|
122
|
+
let clientKind = initial.clientKind;
|
|
123
|
+
if (variant.id === 'rrr-client' || variant.id === 'rrr-fullstack') {
|
|
124
|
+
const normalizedKind = normalizeClientKind(clientKind);
|
|
125
|
+
clientKind = normalizedKind ?? clientKind;
|
|
126
|
+
if (!clientKind) {
|
|
127
|
+
clientKind = await promptForClientKind();
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
return {
|
|
131
|
+
variant,
|
|
132
|
+
targetDir,
|
|
133
|
+
pkgName,
|
|
134
|
+
contractName,
|
|
135
|
+
clientKind,
|
|
136
|
+
reset: initial.reset,
|
|
137
|
+
};
|
|
138
|
+
}
|
|
@@ -0,0 +1,309 @@
|
|
|
1
|
+
import { access } from 'node:fs/promises';
|
|
2
|
+
import { readFileSync } from 'node:fs';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { ModuleResolutionKind } from 'typescript';
|
|
5
|
+
import { workspaceRoot } from './constants.js';
|
|
6
|
+
export function toPosixPath(value) {
|
|
7
|
+
return value.split(path.sep).join('/');
|
|
8
|
+
}
|
|
9
|
+
function stripUndefined(obj) {
|
|
10
|
+
return Object.fromEntries(Object.entries(obj).filter(([, value]) => value !== undefined));
|
|
11
|
+
}
|
|
12
|
+
export async function pathExists(target) {
|
|
13
|
+
try {
|
|
14
|
+
await access(target);
|
|
15
|
+
return true;
|
|
16
|
+
}
|
|
17
|
+
catch {
|
|
18
|
+
return false;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
export function baseTsConfig(options) {
|
|
22
|
+
const config = {
|
|
23
|
+
...(options?.extends ? { extends: options.extends } : {}),
|
|
24
|
+
compilerOptions: {
|
|
25
|
+
target: 'ES2020',
|
|
26
|
+
module: 'ESNext',
|
|
27
|
+
moduleResolution: 'Bundler',
|
|
28
|
+
...(options?.outDir ? { outDir: options.outDir } : {}),
|
|
29
|
+
...(options?.rootDir ? { rootDir: options.rootDir } : {}),
|
|
30
|
+
declaration: options?.declaration ?? true,
|
|
31
|
+
sourceMap: options?.sourceMap ?? true,
|
|
32
|
+
strict: options?.strict ?? true,
|
|
33
|
+
esModuleInterop: options?.esModuleInterop ?? true,
|
|
34
|
+
skipLibCheck: options?.skipLibCheck ?? true,
|
|
35
|
+
resolveJsonModule: options?.resolveJsonModule ?? true,
|
|
36
|
+
forceConsistentCasingInFileNames: options?.forceConsistentCasingInFileNames ?? true,
|
|
37
|
+
baseUrl: options?.baseUrl ?? '.',
|
|
38
|
+
lib: options?.lib,
|
|
39
|
+
types: options?.types,
|
|
40
|
+
jsx: options?.jsx,
|
|
41
|
+
},
|
|
42
|
+
include: options?.include,
|
|
43
|
+
exclude: options?.exclude,
|
|
44
|
+
};
|
|
45
|
+
return `${JSON.stringify(config, null, 2)}\n`;
|
|
46
|
+
}
|
|
47
|
+
export function baseEslintConfig(tsconfigPath = './tsconfig.json') {
|
|
48
|
+
return `import tseslint from 'typescript-eslint'
|
|
49
|
+
import prettierPlugin from 'eslint-plugin-prettier'
|
|
50
|
+
import { fileURLToPath } from 'node:url'
|
|
51
|
+
|
|
52
|
+
const __dirname = fileURLToPath(new URL('.', import.meta.url))
|
|
53
|
+
|
|
54
|
+
export default tseslint.config(
|
|
55
|
+
{ ignores: ['dist', 'node_modules'] },
|
|
56
|
+
...tseslint.configs.recommendedTypeChecked,
|
|
57
|
+
{
|
|
58
|
+
files: ['**/*.ts', '**/*.tsx'],
|
|
59
|
+
languageOptions: {
|
|
60
|
+
parserOptions: {
|
|
61
|
+
project: ${JSON.stringify(tsconfigPath)},
|
|
62
|
+
tsconfigRootDir: __dirname,
|
|
63
|
+
},
|
|
64
|
+
},
|
|
65
|
+
plugins: {
|
|
66
|
+
prettier: prettierPlugin,
|
|
67
|
+
},
|
|
68
|
+
rules: {
|
|
69
|
+
'prettier/prettier': 'error',
|
|
70
|
+
'@typescript-eslint/consistent-type-imports': 'warn',
|
|
71
|
+
'@typescript-eslint/no-unused-vars': [
|
|
72
|
+
'warn',
|
|
73
|
+
{ argsIgnorePattern: '^_', caughtErrorsIgnorePattern: '^ignore' },
|
|
74
|
+
],
|
|
75
|
+
},
|
|
76
|
+
},
|
|
77
|
+
)
|
|
78
|
+
`;
|
|
79
|
+
}
|
|
80
|
+
export function basePrettierConfig() {
|
|
81
|
+
return `export default {
|
|
82
|
+
singleQuote: true,
|
|
83
|
+
semi: false,
|
|
84
|
+
trailingComma: 'all',
|
|
85
|
+
printWidth: 100,
|
|
86
|
+
}
|
|
87
|
+
`;
|
|
88
|
+
}
|
|
89
|
+
export const PRETTIER_IGNORE = ['node_modules', 'dist', '.turbo', 'coverage', '*.log'].join('\n');
|
|
90
|
+
export const LINT_STAGED_CONFIG = {
|
|
91
|
+
'*.{ts,tsx}': ['eslint --fix'],
|
|
92
|
+
'*.{ts,tsx,js,jsx,json,md,css,html}': ['prettier --write'],
|
|
93
|
+
};
|
|
94
|
+
export const HUSKY_PRE_COMMIT = `#!/usr/bin/env sh
|
|
95
|
+
. "$(dirname -- "$0")/_/husky.sh"
|
|
96
|
+
|
|
97
|
+
pnpm lint-staged
|
|
98
|
+
`;
|
|
99
|
+
export function vscodeSettings() {
|
|
100
|
+
return `${JSON.stringify({
|
|
101
|
+
'editor.defaultFormatter': 'esbenp.prettier-vscode',
|
|
102
|
+
'editor.formatOnSave': true,
|
|
103
|
+
'editor.codeActionsOnSave': {
|
|
104
|
+
'source.fixAll.eslint': 'always',
|
|
105
|
+
'source.organizeImports': true,
|
|
106
|
+
},
|
|
107
|
+
'eslint.useFlatConfig': true,
|
|
108
|
+
'eslint.format.enable': true,
|
|
109
|
+
'eslint.validate': ['typescript', 'javascript'],
|
|
110
|
+
'files.eol': '\n',
|
|
111
|
+
'prettier.requireConfig': true,
|
|
112
|
+
}, null, 2)}\n`;
|
|
113
|
+
}
|
|
114
|
+
export const BASE_LINT_DEV_DEPENDENCIES = {
|
|
115
|
+
typescript: '^5.9.3',
|
|
116
|
+
eslint: '^9.12.0',
|
|
117
|
+
'eslint-plugin-prettier': '^5.2.1',
|
|
118
|
+
prettier: '^3.3.3',
|
|
119
|
+
'typescript-eslint': '^8.10.0',
|
|
120
|
+
rimraf: '^6.0.1',
|
|
121
|
+
tsx: '^4.19.0',
|
|
122
|
+
husky: '^9.1.6',
|
|
123
|
+
'lint-staged': '^15.2.10',
|
|
124
|
+
'@emeryld/manager': 'latest',
|
|
125
|
+
};
|
|
126
|
+
const DEFAULT_GITIGNORE_ENTRIES = [
|
|
127
|
+
'node_modules',
|
|
128
|
+
'dist',
|
|
129
|
+
'.turbo',
|
|
130
|
+
'.DS_Store',
|
|
131
|
+
'.env',
|
|
132
|
+
'coverage',
|
|
133
|
+
'*.log',
|
|
134
|
+
'.vscode',
|
|
135
|
+
'.husky',
|
|
136
|
+
];
|
|
137
|
+
export function gitignoreFrom(entries = DEFAULT_GITIGNORE_ENTRIES) {
|
|
138
|
+
return entries.join('\n');
|
|
139
|
+
}
|
|
140
|
+
export function baseScripts(devCommand, extras, options) {
|
|
141
|
+
const includePrepare = options?.includePrepare ?? true;
|
|
142
|
+
return {
|
|
143
|
+
dev: devCommand,
|
|
144
|
+
build: 'tsc -p tsconfig.json',
|
|
145
|
+
typecheck: 'tsc -p tsconfig.json --noEmit',
|
|
146
|
+
lint: 'eslint . --max-warnings=0',
|
|
147
|
+
'lint:fix': 'eslint . --fix',
|
|
148
|
+
'lint-staged': 'lint-staged',
|
|
149
|
+
format: 'prettier . --write',
|
|
150
|
+
'format:check': 'prettier . --check',
|
|
151
|
+
clean: 'rimraf dist .turbo coverage',
|
|
152
|
+
test: "node -e \"console.log('No tests yet')\"",
|
|
153
|
+
...(includePrepare
|
|
154
|
+
? { prepare: 'git rev-parse --is-inside-work-tree >/dev/null 2>&1 && husky || true' }
|
|
155
|
+
: {}),
|
|
156
|
+
...extras,
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
export function basePackageFiles(options) {
|
|
160
|
+
return {
|
|
161
|
+
'.gitignore': gitignoreFrom(options?.gitignoreEntries),
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
export const SCRIPT_DESCRIPTIONS = {
|
|
165
|
+
dev: 'Watch and rebuild on change',
|
|
166
|
+
build: 'Type-check and emit to dist/',
|
|
167
|
+
typecheck: 'Type-check only (no emit)',
|
|
168
|
+
lint: 'Run ESLint with flat config',
|
|
169
|
+
'lint:fix': 'Fix lint issues automatically',
|
|
170
|
+
'lint-staged': 'Run lint/format on staged files',
|
|
171
|
+
format: 'Format code with Prettier',
|
|
172
|
+
'format:check': 'Check formatting without writing',
|
|
173
|
+
clean: 'Remove build artifacts and caches',
|
|
174
|
+
test: 'Placeholder test script',
|
|
175
|
+
start: 'Run the built output',
|
|
176
|
+
'docker:build': 'Build docker image',
|
|
177
|
+
'docker:up': 'Build and start container',
|
|
178
|
+
'docker:dev': 'Build, start, and tail logs',
|
|
179
|
+
'docker:logs': 'Tail docker logs',
|
|
180
|
+
'docker:stop': 'Stop running container',
|
|
181
|
+
'docker:clean': 'Stop and remove container',
|
|
182
|
+
'docker:reset': 'Remove container and image',
|
|
183
|
+
};
|
|
184
|
+
export function buildReadme(options) {
|
|
185
|
+
const scripts = options.scripts ?? [];
|
|
186
|
+
const scriptLines = scripts.map((script) => {
|
|
187
|
+
const desc = SCRIPT_DESCRIPTIONS[script];
|
|
188
|
+
return desc ? `- \`npm run ${script}\` - ${desc}` : `- \`npm run ${script}\``;
|
|
189
|
+
});
|
|
190
|
+
const sections = [...(options.sections ?? [])];
|
|
191
|
+
if (scriptLines.length > 0) {
|
|
192
|
+
sections.push({ title: 'Scripts', lines: scriptLines });
|
|
193
|
+
}
|
|
194
|
+
const lines = [`# ${options.name}`, ''];
|
|
195
|
+
if (options.description) {
|
|
196
|
+
lines.push(options.description, '');
|
|
197
|
+
}
|
|
198
|
+
sections.forEach((section) => {
|
|
199
|
+
lines.push(`## ${section.title}`, ...section.lines, '');
|
|
200
|
+
});
|
|
201
|
+
return `${lines.join('\n').trim()}\n`;
|
|
202
|
+
}
|
|
203
|
+
export function basePackageJson(options) {
|
|
204
|
+
const applyDefaults = options.useDefaults ?? true;
|
|
205
|
+
const inheritPackageManager = options.inheritPackageManager ?? true;
|
|
206
|
+
const packageManager = inheritPackageManager && !options.extraFields?.packageManager
|
|
207
|
+
? readRootPackageManager()
|
|
208
|
+
: undefined;
|
|
209
|
+
const pkg = stripUndefined({
|
|
210
|
+
name: options.name,
|
|
211
|
+
version: options.version ?? '0.1.0',
|
|
212
|
+
private: options.private ?? true,
|
|
213
|
+
...(applyDefaults
|
|
214
|
+
? {
|
|
215
|
+
type: options.type ?? 'module',
|
|
216
|
+
main: options.main ?? 'dist/index.js',
|
|
217
|
+
types: options.types ?? 'dist/index.d.ts',
|
|
218
|
+
files: options.files ?? ['dist'],
|
|
219
|
+
}
|
|
220
|
+
: stripUndefined({
|
|
221
|
+
type: options.type,
|
|
222
|
+
main: options.main,
|
|
223
|
+
types: options.types,
|
|
224
|
+
files: options.files,
|
|
225
|
+
})),
|
|
226
|
+
exports: options.exports,
|
|
227
|
+
scripts: options.scripts,
|
|
228
|
+
dependencies: options.dependencies,
|
|
229
|
+
devDependencies: options.devDependencies,
|
|
230
|
+
'lint-staged': LINT_STAGED_CONFIG,
|
|
231
|
+
packageManager,
|
|
232
|
+
...options.extraFields,
|
|
233
|
+
});
|
|
234
|
+
return `${JSON.stringify(pkg, null, 2)}\n`;
|
|
235
|
+
}
|
|
236
|
+
function readRootPackageManager() {
|
|
237
|
+
try {
|
|
238
|
+
const raw = readFileSync(path.join(workspaceRoot, 'package.json'), 'utf8');
|
|
239
|
+
const pkg = JSON.parse(raw);
|
|
240
|
+
return pkg.packageManager;
|
|
241
|
+
}
|
|
242
|
+
catch (error) {
|
|
243
|
+
if (error &&
|
|
244
|
+
typeof error === 'object' &&
|
|
245
|
+
error.code !== 'ENOENT') {
|
|
246
|
+
console.warn(`Could not read root package.json for packageManager: ${error instanceof Error ? error.message : String(error)}`);
|
|
247
|
+
}
|
|
248
|
+
return undefined;
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
export async function resolveRootTsconfig(baseDir = workspaceRoot) {
|
|
252
|
+
const candidates = ['tsconfig.base.json', 'tsconfig.json'];
|
|
253
|
+
for (const candidate of candidates) {
|
|
254
|
+
const fullPath = path.join(baseDir, candidate);
|
|
255
|
+
if (await pathExists(fullPath)) {
|
|
256
|
+
return fullPath;
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
return undefined;
|
|
260
|
+
}
|
|
261
|
+
export async function findNearestTsconfig(startDir) {
|
|
262
|
+
let current = path.resolve(startDir);
|
|
263
|
+
// eslint-disable-next-line no-constant-condition
|
|
264
|
+
while (true) {
|
|
265
|
+
const found = await resolveRootTsconfig(current);
|
|
266
|
+
if (found)
|
|
267
|
+
return found;
|
|
268
|
+
const parent = path.dirname(current);
|
|
269
|
+
if (parent === current)
|
|
270
|
+
return undefined;
|
|
271
|
+
current = parent;
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
export async function packageTsConfig(targetDir, options) {
|
|
275
|
+
const extendsFromRoot = options?.extendsFromRoot ?? true;
|
|
276
|
+
let extendsPath;
|
|
277
|
+
if (extendsFromRoot) {
|
|
278
|
+
const rootConfig = await findNearestTsconfig(targetDir);
|
|
279
|
+
if (rootConfig) {
|
|
280
|
+
const relative = path.relative(targetDir, rootConfig);
|
|
281
|
+
const normalized = toPosixPath(relative || './tsconfig.base.json');
|
|
282
|
+
extendsPath = normalized.startsWith('.') ? normalized : `./${normalized}`;
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
const compilerOptions = stripUndefined({
|
|
286
|
+
rootDir: options?.rootDir ?? 'src',
|
|
287
|
+
outDir: options?.outDir ?? 'dist',
|
|
288
|
+
tsBuildInfoFile: 'dist/.tsbuildinfo',
|
|
289
|
+
jsx: options?.jsx,
|
|
290
|
+
types: options?.types,
|
|
291
|
+
lib: options?.lib,
|
|
292
|
+
esModuleInterop: options?.esModuleInterop ?? true,
|
|
293
|
+
allowSyntheticDefaultImports: options?.esModuleInterop ?? true,
|
|
294
|
+
skipLibCheck: options?.skipLibCheck ?? true,
|
|
295
|
+
target: options?.target,
|
|
296
|
+
module: options?.module,
|
|
297
|
+
moduleResolution: options?.ModuleResolutionKind !== undefined
|
|
298
|
+
? ModuleResolutionKind[options.ModuleResolutionKind]
|
|
299
|
+
: undefined,
|
|
300
|
+
declaration: options?.declarationMap,
|
|
301
|
+
declarationMap: options?.declarationMap,
|
|
302
|
+
});
|
|
303
|
+
const config = stripUndefined({
|
|
304
|
+
extends: extendsPath,
|
|
305
|
+
compilerOptions,
|
|
306
|
+
include: options?.include ?? ['src/**/*.ts'],
|
|
307
|
+
});
|
|
308
|
+
return `${JSON.stringify(config, null, 2)}\n`;
|
|
309
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { access, mkdir, writeFile } from 'node:fs/promises';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { promptYesNoAll } from '../../prompts.js';
|
|
4
|
+
import { baseEslintConfig, basePrettierConfig, HUSKY_PRE_COMMIT, pathExists, PRETTIER_IGNORE, resolveRootTsconfig, toPosixPath, vscodeSettings, } from './configs.js';
|
|
5
|
+
import { workspaceRoot } from './constants.js';
|
|
6
|
+
export async function writeFileIfMissing(baseDir, relative, contents) {
|
|
7
|
+
const fullPath = path.join(baseDir, relative);
|
|
8
|
+
await mkdir(path.dirname(fullPath), { recursive: true });
|
|
9
|
+
try {
|
|
10
|
+
await access(fullPath);
|
|
11
|
+
const rel = path.relative(workspaceRoot, fullPath);
|
|
12
|
+
console.log(` skipped ${rel} (already exists)`);
|
|
13
|
+
return 'skipped';
|
|
14
|
+
}
|
|
15
|
+
catch (error) {
|
|
16
|
+
if (error &&
|
|
17
|
+
typeof error === 'object' &&
|
|
18
|
+
error.code !== 'ENOENT') {
|
|
19
|
+
throw error;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
await writeFile(fullPath, contents, 'utf8');
|
|
23
|
+
const rel = path.relative(workspaceRoot, fullPath);
|
|
24
|
+
console.log(` created ${rel}`);
|
|
25
|
+
return 'created';
|
|
26
|
+
}
|
|
27
|
+
export async function writeFileWithPrompt(baseDir, relative, contents) {
|
|
28
|
+
const fullPath = path.join(baseDir, relative);
|
|
29
|
+
await mkdir(path.dirname(fullPath), { recursive: true });
|
|
30
|
+
const rel = path.relative(workspaceRoot, fullPath);
|
|
31
|
+
const exists = await pathExists(fullPath);
|
|
32
|
+
if (exists) {
|
|
33
|
+
const answer = await promptYesNoAll(`Overwrite existing ${rel}?`);
|
|
34
|
+
if (answer !== 'yes') {
|
|
35
|
+
console.log(` kept existing ${rel}`);
|
|
36
|
+
return 'skipped';
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
await writeFile(fullPath, contents, 'utf8');
|
|
40
|
+
console.log(` ${exists ? 'updated' : 'created'} ${rel}`);
|
|
41
|
+
return exists ? 'updated' : 'created';
|
|
42
|
+
}
|
|
43
|
+
export function workspaceToolingFiles(tsconfigPath) {
|
|
44
|
+
return {
|
|
45
|
+
'eslint.config.js': baseEslintConfig(tsconfigPath),
|
|
46
|
+
'prettier.config.js': basePrettierConfig(),
|
|
47
|
+
'.prettierignore': `${PRETTIER_IGNORE}\n`,
|
|
48
|
+
'.vscode/settings.json': vscodeSettings(),
|
|
49
|
+
'.husky/pre-commit': HUSKY_PRE_COMMIT,
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
export async function ensureWorkspaceToolingFiles(baseDir, options) {
|
|
53
|
+
const defaultTsconfig = path.join(baseDir, 'tsconfig.json');
|
|
54
|
+
const resolvedRootConfig = options?.tsconfigPath
|
|
55
|
+
? path.resolve(baseDir, options.tsconfigPath)
|
|
56
|
+
: await resolveRootTsconfig(baseDir);
|
|
57
|
+
const relativeTsconfig = path.relative(baseDir, resolvedRootConfig ?? defaultTsconfig);
|
|
58
|
+
const normalized = toPosixPath(relativeTsconfig || './tsconfig.json');
|
|
59
|
+
const tsconfigPath = normalized.startsWith('.') ? normalized : `./${normalized}`;
|
|
60
|
+
const files = workspaceToolingFiles(tsconfigPath);
|
|
61
|
+
for (const [relative, contents] of Object.entries(files)) {
|
|
62
|
+
await writeFileWithPrompt(baseDir, relative, contents);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
export async function writeFilesIfMissing(baseDir, files) {
|
|
66
|
+
for (const [relative, contents] of Object.entries(files)) {
|
|
67
|
+
await writeFileIfMissing(baseDir, relative, contents);
|
|
68
|
+
}
|
|
69
|
+
}
|