@emeryld/manager 1.3.0 → 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.
Files changed (54) hide show
  1. package/README.md +96 -0
  2. package/dist/create-package/cli-args.js +78 -0
  3. package/dist/create-package/prompts.js +138 -0
  4. package/dist/create-package/shared/configs.js +309 -0
  5. package/dist/create-package/shared/constants.js +5 -0
  6. package/dist/create-package/shared/fs-utils.js +69 -0
  7. package/dist/create-package/tasks.js +89 -0
  8. package/dist/create-package/types.js +1 -0
  9. package/dist/create-package/variant-info.js +67 -0
  10. package/dist/create-package/variants/client/expo-react-native/lib-files.js +168 -0
  11. package/dist/create-package/variants/client/expo-react-native/package-files.js +94 -0
  12. package/dist/create-package/variants/client/expo-react-native/scaffold.js +59 -0
  13. package/dist/create-package/variants/client/expo-react-native/ui-files.js +215 -0
  14. package/dist/create-package/variants/client/vite-react/health-page.js +251 -0
  15. package/dist/create-package/variants/client/vite-react/lib-files.js +176 -0
  16. package/dist/create-package/variants/client/vite-react/package-files.js +79 -0
  17. package/dist/create-package/variants/client/vite-react/scaffold.js +68 -0
  18. package/dist/create-package/variants/client/vite-react/ui-files.js +154 -0
  19. package/dist/create-package/variants/fullstack/files.js +129 -0
  20. package/dist/create-package/variants/fullstack/index.js +86 -0
  21. package/dist/create-package/variants/fullstack/utils.js +241 -0
  22. package/dist/llm-pack.js +2 -0
  23. package/dist/robot/cli/prompts.js +84 -27
  24. package/dist/robot/cli/settings.js +131 -56
  25. package/dist/robot/config.js +123 -50
  26. package/dist/robot/coordinator.js +10 -105
  27. package/dist/robot/extractors/classes.js +14 -13
  28. package/dist/robot/extractors/components.js +17 -10
  29. package/dist/robot/extractors/constants.js +9 -6
  30. package/dist/robot/extractors/functions.js +11 -8
  31. package/dist/robot/extractors/shared.js +6 -1
  32. package/dist/robot/extractors/types.js +5 -8
  33. package/dist/robot/llm-pack.js +1226 -0
  34. package/dist/robot/pack/builder.js +374 -0
  35. package/dist/robot/pack/cli.js +65 -0
  36. package/dist/robot/pack/exemplars.js +573 -0
  37. package/dist/robot/pack/globs.js +119 -0
  38. package/dist/robot/pack/selection.js +44 -0
  39. package/dist/robot/pack/symbols.js +309 -0
  40. package/dist/robot/pack/type-registry.js +285 -0
  41. package/dist/robot/pack/types.js +48 -0
  42. package/dist/robot/pack/utils.js +36 -0
  43. package/dist/robot/serializer.js +97 -0
  44. package/dist/robot/v2/cli.js +86 -0
  45. package/dist/robot/v2/globs.js +103 -0
  46. package/dist/robot/v2/parser/bundles.js +55 -0
  47. package/dist/robot/v2/parser/candidates.js +63 -0
  48. package/dist/robot/v2/parser/exemplars.js +114 -0
  49. package/dist/robot/v2/parser/exports.js +57 -0
  50. package/dist/robot/v2/parser/symbols.js +179 -0
  51. package/dist/robot/v2/parser.js +114 -0
  52. package/dist/robot/v2/types.js +42 -0
  53. package/dist/utils/export.js +39 -18
  54. package/package.json +2 -1
package/README.md CHANGED
@@ -65,8 +65,104 @@ You can also decide where the report lands: the manager will ask whether to stre
65
65
 
66
66
  The `robot metadata` action now starts with the same interactive settings screen as the format checker: pick which kinds of symbols to include, decide whether to limit the scan to exported declarations, and adjust the column width. Use ↑/↓ to move between rows, type new values (comma-separated lists for the kinds), and press Enter once the validation message disappears.
67
67
 
68
+ The column width now controls how much context is emitted for each declaration. Symbols that start past the configured column keep their name/export status but drop heavyweight fields (IO signatures, docstrings, JSX locations, etc.), so you still see everything in the workspace but only the left-most definitions contribute rich metadata to your LLM prompt.
69
+
68
70
  Before the extraction runs you can also choose how to consume the results. The default `console` stream prints the JSON into your terminal, while `editor` writes the report to a temporary file (non-saved) and tries to open it in your editor via your configured `$EDITOR`, `code`, or the OS `open/xdg-open` helpers.
69
71
 
72
+ To keep the payload more token-friendly, the interactive settings screen now exposes `Condense output for compact JSON` and `Docstring length limit`. Enabling the condensed output replaces the verbose object format with a small `fields` + `rows` array layout, and the docstring limit trims comments to the first N characters (set the value to `0` for unlimited). Set your preferred defaults in `.vscode/settings.json` under `manager.robot.condenseOutput` and `manager.robot.maxDocStringLength`.
73
+
74
+ ### Running with arguments
75
+ Run `pnpm manager-cli`, pick the workspace (or “All packages”), then select the `robot metadata` action. The helper walks you through:
76
+
77
+ 1. Set the symbol kinds to capture (comma-separated list or `all`).
78
+ 2. Toggle `Only exported symbols`, adjust `Maximum columns`, `Condense output for compact JSON`, and `Docstring length limit`.
79
+ 3. Confirm the summary to trigger the run and choose whether to stream results to the console or editor.
80
+
81
+ The same knobs live in `.vscode/settings.json` under `manager.robot`, for example:
82
+
83
+ ```json
84
+ {
85
+ "manager.robot": {
86
+ "includeKinds": ["function", "type", "const"],
87
+ "exportedOnly": true,
88
+ "maxColumns": 120,
89
+ "condenseOutput": true,
90
+ "maxDocStringLength": 80
91
+ }
92
+ }
93
+ ```
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
+
126
+ ### Example output
127
+ `robot metadata` prints a summary followed by the parsed payload. With the default formatting you get readable JSON:
128
+
129
+ ```text
130
+ Robot extraction complete (functions=4 components=1 types=2 consts=0 classes=1)
131
+ Estimated tokens: 72
132
+ Detailed results:
133
+ {
134
+ "functions": [
135
+ {
136
+ "kind": "function",
137
+ "name": "buildManagerConfig",
138
+ "location": { "file": "src/menu.ts", "line": 45, "column": 3 },
139
+ "docString": "Builds the menu configuration for the helper CLI.",
140
+ "exported": true,
141
+ "inputs": ["packages: LoadedPackage[]"],
142
+ "output": "HelperScriptEntry[]"
143
+ }
144
+ ],
145
+ ...
146
+ }
147
+ ```
148
+
149
+ Enabling `Condense output for compact JSON` yields a compact payload that reduces tokens even when the detailed rows are long:
150
+
151
+ ```text
152
+ Robot extraction complete (functions=4 components=1 types=2 consts=0 classes=1)
153
+ Estimated tokens: 48
154
+ Detailed results (condensed):
155
+ {
156
+ "version": 1,
157
+ "summary": { "functions": 4, "components": 1, "types": 2, "consts": 0, "classes": 1 },
158
+ "fields": ["kind","file","line","column","name","signature","docString","exported"],
159
+ "rows": [
160
+ ["function","src/menu.ts",45,3,"buildManagerConfig","(packages: LoadedPackage[]) => HelperScriptEntry[]","Builds the menu configuration...",true],
161
+ ["component","src/ui/banner.ts",12,7,"Banner","(props: BannerProps) => JSX.Element","Lightweight banner used in the hero layout.",true]
162
+ ]
163
+ }
164
+ ```
165
+
70
166
  ## Non-interactive release (Codex/CI)
71
167
  - **Syntax**: `pnpm manager-cli <pkg|all> --non-interactive [publish flags]`
72
168
  - **Requirements**: provide the selection (`<pkg>` or `all`) and one of `--bump <type>`, `--sync <version>`, or `--noop` (skip the version change but still tag/publish). Use `--non-interactive`, `--ci`, `--yes`, or `-y` interchangeably to answer every prompt in the affirmative.
@@ -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,5 @@
1
+ import path from 'node:path';
2
+ export const workspaceRoot = process.cwd();
3
+ export function isWorkspaceRoot(dir) {
4
+ return path.resolve(dir) === path.resolve(workspaceRoot);
5
+ }