@produck/agent-toolkit 0.9.1 → 0.10.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.
@@ -42,6 +42,10 @@ import {
42
42
  printSyncPublishHelp,
43
43
  runSyncPublish,
44
44
  } from './command/sync-publish/index.mjs';
45
+ import {
46
+ printSyncTypescriptHelp,
47
+ runSyncTypescript,
48
+ } from './command/sync-typescript/index.mjs';
45
49
  import { hasFlag, parseCommonArgs } from './command/shared/args.mjs';
46
50
  import {
47
51
  printValidateCommitMsgHelp,
@@ -101,6 +105,10 @@ const COMMANDS = {
101
105
  printHelp: printSyncPublishHelp,
102
106
  run: runSyncPublish,
103
107
  },
108
+ 'sync-typescript': {
109
+ printHelp: printSyncTypescriptHelp,
110
+ run: runSyncTypescript,
111
+ },
104
112
  };
105
113
 
106
114
  const DEFAULT_COMMAND = 'enforce-node-baseline';
@@ -49,6 +49,7 @@ const OUTPUT_LERNA_PATH = path.resolve(
49
49
  PACKAGE_ROOT,
50
50
  'publish-assets/lerna.json',
51
51
  );
52
+ const ROOT_PACKAGE_JSON_PATH = path.resolve(REPO_ROOT, 'package.json');
52
53
  const LEGACY_OUTPUT_PATH = path.resolve(
53
54
  PACKAGE_ROOT,
54
55
  'publish-assets/instructions/org.instructions.md',
@@ -80,6 +81,10 @@ function validateSourceFile(fileName, text) {
80
81
  }
81
82
  }
82
83
 
84
+ function resolveSemverExact(text) {
85
+ return text.replace(/^[\^~>=<]+\s*/, '').trim();
86
+ }
87
+
83
88
  function readAndValidateToolingBaseline() {
84
89
  if (!fs.existsSync(SOURCE_TOOLING_BASELINE_PATH)) {
85
90
  throw new Error(
@@ -98,25 +103,40 @@ function readAndValidateToolingBaseline() {
98
103
  );
99
104
  }
100
105
 
101
- const c8Version = baseline?.tools?.c8?.version;
102
- const lernaVersion = baseline?.tools?.lerna?.version;
103
- const coverageScriptTemplate = baseline?.coverage?.scriptTemplate;
104
-
105
106
  if (typeof baseline.schemaVersion !== 'number') {
106
107
  throw new Error(
107
108
  `Invalid tooling baseline schemaVersion in: ${SOURCE_TOOLING_BASELINE_PATH}`,
108
109
  );
109
110
  }
110
111
 
112
+ // Auto-resolve tool versions: only override version when set to "auto"
113
+ const rootPkg = JSON.parse(fs.readFileSync(ROOT_PACKAGE_JSON_PATH, 'utf8'));
114
+ const devDeps = rootPkg.devDependencies || {};
115
+
116
+ for (const [toolName, entry] of Object.entries(baseline.tools)) {
117
+ if (entry.version === 'auto') {
118
+ const depVersion = devDeps[toolName];
119
+ if (typeof depVersion === 'string' && depVersion.trim()) {
120
+ entry.version = resolveSemverExact(depVersion);
121
+ }
122
+ }
123
+ }
124
+
125
+ const c8Version = baseline?.tools?.c8?.version;
126
+ const lernaVersion = baseline?.tools?.lerna?.version;
127
+ const coverageScriptTemplate = baseline?.coverage?.scriptTemplate;
128
+
111
129
  if (typeof c8Version !== 'string' || c8Version.trim() === '') {
112
130
  throw new Error(
113
- `Invalid tools.c8.version in: ${SOURCE_TOOLING_BASELINE_PATH}`,
131
+ `Invalid tools.c8.version in: ${SOURCE_TOOLING_BASELINE_PATH} ` +
132
+ '(c8 must be listed in root package.json devDependencies)',
114
133
  );
115
134
  }
116
135
 
117
136
  if (typeof lernaVersion !== 'string' || lernaVersion.trim() === '') {
118
137
  throw new Error(
119
- `Invalid tools.lerna.version in: ${SOURCE_TOOLING_BASELINE_PATH}`,
138
+ `Invalid tools.lerna.version in: ${SOURCE_TOOLING_BASELINE_PATH} ` +
139
+ '(lerna must be listed in root package.json devDependencies)',
120
140
  );
121
141
  }
122
142
 
@@ -129,6 +149,7 @@ function readAndValidateToolingBaseline() {
129
149
  );
130
150
  }
131
151
 
152
+ // Dynamically inject @produck/eslint-rules version from its own package.json
132
153
  const eslintRulesPkgPath = path.resolve(
133
154
  PACKAGE_ROOT,
134
155
  '../eslint-rules/package.json',
@@ -146,6 +167,7 @@ function readAndValidateToolingBaseline() {
146
167
  };
147
168
  }
148
169
  }
170
+
149
171
  return `${JSON.stringify(baseline, null, 2)}\n`;
150
172
  }
151
173
 
@@ -7,6 +7,7 @@ agent-toolkit commands:
7
7
  sync-editorconfig
8
8
  sync-git
9
9
  sync-format
10
+ sync-typescript
10
11
  sync-install
11
12
  sync-lint
12
13
  sync-publish
@@ -21,8 +21,13 @@ const TOOLING_BASELINE_CANDIDATE_PATHS = [
21
21
  ];
22
22
  const GLOB_TOKEN_PATTERN = /[*?{}[\]]/;
23
23
  const REQUIRED_ROOT_COVERAGE_SCRIPT_KEY = 'produck:coverage';
24
- const REQUIRED_ROOT_COVERAGE_SCRIPT_VALUE =
25
- 'c8 --config .c8rc.json npm run test --workspaces --if-present';
24
+ const REQUIRED_ROOT_COVERAGE_SCRIPT_VALUE = [
25
+ 'c8',
26
+ '--config .c8rc.json',
27
+ 'npm run test',
28
+ '--workspaces',
29
+ '--if-present',
30
+ ].join(' ');
26
31
  const REQUIRED_COVERAGE_SCRIPT_KEY = 'produck:coverage';
27
32
  const REQUIRED_TEST_SCRIPT_KEY = 'test';
28
33
  const DEFAULT_TEST_SCRIPT_VALUE =
@@ -46,6 +51,35 @@ function parseJsonFile(filePath, label) {
46
51
  }
47
52
  }
48
53
 
54
+ function resolveSemverExact(text) {
55
+ return text.replace(/^[\^~>=<]+\s*/, '').trim();
56
+ }
57
+
58
+ function resolveToolVersionFromDevDeps(baseline, toolName) {
59
+ // If baseline has a concrete version (not "auto"), use it directly.
60
+ // This is the case when reading the published publish-assets baseline.
61
+ const baselineVersion = String(
62
+ baseline?.tools?.[toolName]?.version || '',
63
+ ).trim();
64
+ if (baselineVersion && baselineVersion !== 'auto') {
65
+ return baselineVersion;
66
+ }
67
+
68
+ // Fall back to resolving from local root package.json devDependencies.
69
+ // This covers source baseline with version="auto" during local dev.
70
+ const repoRoot = path.resolve(PACKAGE_ROOT, '../..');
71
+ const pkgJsonPath = path.resolve(repoRoot, 'package.json');
72
+ if (fs.existsSync(pkgJsonPath)) {
73
+ const pkg = parseJsonFile(pkgJsonPath, 'root package.json');
74
+ const dep = pkg?.devDependencies?.[toolName];
75
+ if (typeof dep === 'string' && dep.trim()) {
76
+ return resolveSemverExact(dep);
77
+ }
78
+ }
79
+
80
+ return '';
81
+ }
82
+
49
83
  function loadToolingBaseline() {
50
84
  const toolingBaselinePath = TOOLING_BASELINE_CANDIDATE_PATHS.find(
51
85
  (candidatePath) => {
@@ -71,7 +105,7 @@ function loadToolingBaseline() {
71
105
  process.exit(2);
72
106
  }
73
107
 
74
- const c8Version = baseline?.tools?.c8?.version;
108
+ const c8Version = resolveToolVersionFromDevDeps(baseline, 'c8');
75
109
  if (typeof c8Version !== 'string' || c8Version.trim() === '') {
76
110
  console.error(
77
111
  `Tooling baseline tools.c8.version must be a non-empty string: ${toolingBaselinePath}`,
@@ -1,15 +1,20 @@
1
1
  {
2
+ "branches": 99.5,
2
3
  "check-coverage": true,
3
4
  "exclude": [
4
5
  "**/node_modules/**",
5
6
  "**/coverage/**",
6
7
  "**/dist/**",
7
8
  "**/build/**",
8
- "**/out/**"
9
+ "**/out/**",
10
+ "**/test/**"
9
11
  ],
10
- "reporter": ["lcov", "html", "text-summary"],
11
- "branches": 99.5,
12
12
  "functions": 99.5,
13
- "statements": 99.5,
14
- "lines": 99.5
13
+ "lines": 99.5,
14
+ "reporter": [
15
+ "lcov",
16
+ "html",
17
+ "text-summary"
18
+ ],
19
+ "statements": 99.5
15
20
  }
@@ -99,6 +99,35 @@ function parseJsonFile(filePath, label) {
99
99
  }
100
100
  }
101
101
 
102
+ function resolveSemverExact(text) {
103
+ return text.replace(/^[\^~>=<]+\s*/, '').trim();
104
+ }
105
+
106
+ function resolveToolVersionFromDevDeps(baseline, toolName) {
107
+ // If baseline has a concrete version (not "auto"), use it directly.
108
+ // This is the case when reading the published publish-assets baseline.
109
+ const baselineVersion = String(
110
+ baseline?.tools?.[toolName]?.version || '',
111
+ ).trim();
112
+ if (baselineVersion && baselineVersion !== 'auto') {
113
+ return baselineVersion;
114
+ }
115
+
116
+ // Fall back to resolving from local root package.json devDependencies.
117
+ // This covers source baseline with version="auto" during local dev.
118
+ const repoRoot = path.resolve(PACKAGE_ROOT, '../..');
119
+ const pkgJsonPath = path.resolve(repoRoot, 'package.json');
120
+ if (fs.existsSync(pkgJsonPath)) {
121
+ const pkg = parseJsonFile(pkgJsonPath, 'root package.json');
122
+ const dep = pkg?.devDependencies?.[toolName];
123
+ if (typeof dep === 'string' && dep.trim()) {
124
+ return resolveSemverExact(dep);
125
+ }
126
+ }
127
+
128
+ return '';
129
+ }
130
+
102
131
  function loadToolingBaseline() {
103
132
  const toolingBaselinePath = TOOLING_BASELINE_CANDIDATE_PATHS.find(
104
133
  (candidatePath) => {
@@ -117,9 +146,7 @@ function loadToolingBaseline() {
117
146
  }
118
147
 
119
148
  const baseline = parseJsonFile(toolingBaselinePath, 'Tooling baseline file');
120
- const prettierVersion = String(
121
- baseline?.tools?.prettier?.version || '',
122
- ).trim();
149
+ const prettierVersion = resolveToolVersionFromDevDeps(baseline, 'prettier');
123
150
 
124
151
  if (!prettierVersion) {
125
152
  console.error(
@@ -4,7 +4,7 @@ Usage:
4
4
 
5
5
  Behavior:
6
6
  - Applies organization-required root shared scripts:
7
- - scripts.produck:baseline = npm exec --package=@produck/agent-toolkit@latest -- agent-toolkit enforce-node-baseline --cwd .
7
+ - scripts.produck:baseline = npm exec --package=@produck/agent-toolkit@latest -- agent-toolkit enforce-node-baseline --cwd . && npm run produck:install
8
8
  - scripts.produck:commit:check = npm run produck:format && npm run produck:lint
9
9
  - scripts.prepare = husky
10
10
  - Applies organization-required root shared managed devDependencies:
@@ -29,11 +29,22 @@ const HUSKY_DIR = '.husky';
29
29
  const PRE_COMMIT_HOOK_FILE = 'pre-commit';
30
30
  const COMMIT_MSG_HOOK_FILE = 'commit-msg';
31
31
  const REQUIRED_BASELINE_SCRIPT_KEY = 'produck:baseline';
32
- const REQUIRED_BASELINE_SCRIPT_VALUE =
33
- 'npm exec --package=@produck/agent-toolkit@latest -- agent-toolkit enforce-node-baseline --cwd .';
32
+ const REQUIRED_BASELINE_SCRIPT_VALUE = [
33
+ [
34
+ 'npm exec',
35
+ '--package=@produck/agent-toolkit@latest',
36
+ '--',
37
+ 'agent-toolkit',
38
+ 'enforce-node-baseline',
39
+ '--cwd .',
40
+ ].join(' '),
41
+ 'npm run produck:install',
42
+ ].join(' && ');
34
43
  const REQUIRED_COMMIT_CHECK_SCRIPT_KEY = 'produck:commit:check';
35
- const REQUIRED_COMMIT_CHECK_SCRIPT_VALUE =
36
- 'npm run produck:format && npm run produck:lint';
44
+ const REQUIRED_COMMIT_CHECK_SCRIPT_VALUE = [
45
+ 'npm run produck:format',
46
+ 'npm run produck:lint',
47
+ ].join(' && ');
37
48
  const REQUIRED_PREPARE_SCRIPT_KEY = 'prepare';
38
49
  const REQUIRED_PREPARE_SCRIPT_VALUE = 'husky';
39
50
 
@@ -105,6 +116,35 @@ function getRequiredToolkitDevDependency() {
105
116
  return version;
106
117
  }
107
118
 
119
+ function resolveSemverExact(text) {
120
+ return text.replace(/^[\^~>=<]+\s*/, '').trim();
121
+ }
122
+
123
+ function resolveToolVersionFromDevDeps(baseline, toolName) {
124
+ // If baseline has a concrete version (not "auto"), use it directly.
125
+ // This is the case when reading the published publish-assets baseline.
126
+ const baselineVersion = String(
127
+ baseline?.tools?.[toolName]?.version || '',
128
+ ).trim();
129
+ if (baselineVersion && baselineVersion !== 'auto') {
130
+ return baselineVersion;
131
+ }
132
+
133
+ // Fall back to resolving from local root package.json devDependencies.
134
+ // This covers source baseline with version="auto" during local dev.
135
+ const repoRoot = path.resolve(PACKAGE_ROOT, '../..');
136
+ const pkgJsonPath = path.resolve(repoRoot, 'package.json');
137
+ if (fs.existsSync(pkgJsonPath)) {
138
+ const pkg = parseJsonFile(pkgJsonPath, 'root package.json');
139
+ const dep = pkg?.devDependencies?.[toolName];
140
+ if (typeof dep === 'string' && dep.trim()) {
141
+ return resolveSemverExact(dep);
142
+ }
143
+ }
144
+
145
+ return '';
146
+ }
147
+
108
148
  function loadToolingBaseline() {
109
149
  const toolingBaselinePath = TOOLING_BASELINE_CANDIDATE_PATHS.find(
110
150
  (candidatePath) => {
@@ -123,8 +163,8 @@ function loadToolingBaseline() {
123
163
  }
124
164
 
125
165
  const baseline = parseJsonFile(toolingBaselinePath, 'Tooling baseline file');
126
- const huskyVersion = String(baseline?.tools?.husky?.version || '').trim();
127
- const lernaVersion = String(baseline?.tools?.lerna?.version || '').trim();
166
+ const huskyVersion = resolveToolVersionFromDevDeps(baseline, 'husky');
167
+ const lernaVersion = resolveToolVersionFromDevDeps(baseline, 'lerna');
128
168
 
129
169
  if (!huskyVersion || !lernaVersion) {
130
170
  console.error(
@@ -9,7 +9,7 @@ const COMMAND_DIR = path.dirname(fileURLToPath(import.meta.url));
9
9
  const HELP_FILE = path.resolve(COMMAND_DIR, 'help.txt');
10
10
  const LEGACY_INSTALL_SCRIPT_KEY = 'deps:install';
11
11
  const REQUIRED_INSTALL_SCRIPT_KEY = 'produck:install';
12
- const REQUIRED_INSTALL_SCRIPT_VALUE = 'npm -v && npm install';
12
+ const REQUIRED_INSTALL_SCRIPT_VALUE = ['npm -v', 'npm install'].join(' && ');
13
13
 
14
14
  export function printSyncInstallHelp() {
15
15
  printTextResource(HELP_FILE);
@@ -1,8 +1,6 @@
1
1
  import js from '@eslint/js';
2
2
  import globals from 'globals';
3
3
  import tseslint from 'typescript-eslint';
4
- import json from '@eslint/json';
5
- import markdown from '@eslint/markdown';
6
4
  import { defineConfig } from 'eslint/config';
7
5
  import * as ProduckRule from '@produck/eslint-rules';
8
6
 
@@ -14,19 +12,8 @@ export default defineConfig([
14
12
  languageOptions: { globals: { ...globals.browser, ...globals.node } },
15
13
  },
16
14
  tseslint.configs.recommended,
17
- {
18
- files: ['**/*.json'],
19
- ignores: ['**/package-lock.json'],
20
- plugins: { json },
21
- language: 'json/json',
22
- extends: ['json/recommended'],
23
- },
24
- {
25
- files: ['**/*.md'],
26
- plugins: { markdown },
27
- language: 'markdown/gfm',
28
- extends: ['markdown/recommended'],
29
- },
30
- ProduckRule.config,
15
+ ProduckRule.config.ecma,
16
+ ProduckRule.config.json,
17
+ ProduckRule.config.markdown,
31
18
  ProduckRule.excludeGitIgnore(import.meta.url),
32
19
  ]);
@@ -21,6 +21,12 @@ const TOOLING_BASELINE_CANDIDATE_PATHS = [
21
21
  ];
22
22
  const ESLINT_RULES_PACKAGE_NAME = '@produck/eslint-rules';
23
23
  const ESLINT_CONFIG_FILE = 'eslint.config.mjs';
24
+ const REQUIRED_ESLINT_ENTRIES = [
25
+ 'ProduckRule.config.ecma',
26
+ 'ProduckRule.config.json',
27
+ 'ProduckRule.config.markdown',
28
+ 'ProduckRule.excludeGitIgnore(import.meta.url)',
29
+ ];
24
30
 
25
31
  const REQUIRED_LINT_SCRIPT_KEY = 'produck:lint';
26
32
  const REQUIRED_LINT_SCRIPT_VALUE = 'eslint --fix . --max-warnings=0';
@@ -122,16 +128,38 @@ function getRequiredEslintDevDependencies() {
122
128
  eslintRulesVersion = v;
123
129
  }
124
130
 
131
+ function resolveSemverExact(text) {
132
+ return text.replace(/^[\^~>=<]+\s*/, '').trim();
133
+ }
134
+
135
+ function resolveToolVersionFromDevDeps(name) {
136
+ // If baseline has a concrete version (not "auto"), use it directly.
137
+ // This is the case when reading the published publish-assets baseline.
138
+ const entry = baseline?.tools?.[name];
139
+ const v = typeof entry?.version === 'string' ? entry.version.trim() : '';
140
+ if (v && v !== 'auto') {
141
+ return v;
142
+ }
143
+
144
+ // Fall back to resolving from local root package.json devDependencies.
145
+ // This covers source baseline with version="auto" during local dev.
146
+ const repoRoot = path.resolve(PACKAGE_ROOT, '../..');
147
+ const pkgJsonPath = path.resolve(repoRoot, 'package.json');
148
+ if (fs.existsSync(pkgJsonPath)) {
149
+ const pkg = parseJsonFile(pkgJsonPath, 'root package.json');
150
+ const dep = pkg?.devDependencies?.[name];
151
+ if (typeof dep === 'string' && dep.trim()) {
152
+ return resolveSemverExact(dep);
153
+ }
154
+ }
155
+
156
+ return '';
157
+ }
158
+
125
159
  const deps = { [ESLINT_RULES_PACKAGE_NAME]: eslintRulesVersion };
126
160
 
127
161
  for (const name of ESLINT_TOOLING_PACKAGE_NAMES) {
128
- const entry = baseline?.tools?.[name];
129
- const v =
130
- typeof entry?.version === 'string'
131
- ? entry.version.trim()
132
- : /* c8 ignore next */
133
- '';
134
- /* c8 ignore next 6 */
162
+ const v = resolveToolVersionFromDevDeps(name);
135
163
  if (!v) {
136
164
  console.error(
137
165
  `Tooling baseline tools["${name}"].version must be a non-empty string: ${toolingBaselinePath}`,
@@ -169,7 +197,7 @@ function patchEslintConfig(existing) {
169
197
  }
170
198
 
171
199
  output =
172
- `${output.slice(0, exportEnd)} ProduckRule.config,\n ProduckRule.excludeGitIgnore(import.meta.url),\n` +
200
+ `${output.slice(0, exportEnd)} ProduckRule.config.ecma,\n ProduckRule.config.json,\n ProduckRule.config.markdown,\n ProduckRule.excludeGitIgnore(import.meta.url),\n` +
173
201
  output.slice(exportEnd);
174
202
 
175
203
  if (!output.endsWith('\n')) {
@@ -235,8 +263,14 @@ export function runSyncLint(options) {
235
263
  nextEslintConfigText = REQUIRED_ESLINT_CONFIG;
236
264
  } else if (previousEslintConfig === REQUIRED_ESLINT_CONFIG) {
237
265
  matchesRequiredEslintConfig = true;
238
- } else if (previousEslintConfig.includes('@produck/eslint-rules')) {
266
+ } else if (
267
+ previousEslintConfig.includes(ESLINT_RULES_PACKAGE_NAME) &&
268
+ REQUIRED_ESLINT_ENTRIES.every((e) => previousEslintConfig.includes(e))
269
+ ) {
239
270
  matchesRequiredEslintConfig = true;
271
+ } else if (previousEslintConfig.includes(ESLINT_RULES_PACKAGE_NAME)) {
272
+ eslintConfigAction = 'replaced';
273
+ nextEslintConfigText = REQUIRED_ESLINT_CONFIG;
240
274
  } else {
241
275
  const patched = patchEslintConfig(previousEslintConfig);
242
276
  if (patched.ok) {
@@ -16,11 +16,16 @@ const LERNA_TEMPLATE_CANDIDATE_PATHS = [
16
16
  ];
17
17
 
18
18
  const REQUIRED_PUBLISH_CHECK_SCRIPT_KEY = 'produck:publish:check';
19
- const REQUIRED_PUBLISH_CHECK_SCRIPT_VALUE =
20
- 'npm run produck:install && npm run produck:coverage && npm run produck:commit:check';
19
+ const REQUIRED_PUBLISH_CHECK_SCRIPT_VALUE = [
20
+ 'npm run produck:install',
21
+ 'npm run produck:coverage',
22
+ 'npm run produck:commit:check',
23
+ ].join(' && ');
21
24
  const REQUIRED_PUBLISH_SCRIPT_KEY = 'produck:publish';
22
- const REQUIRED_PUBLISH_SCRIPT_VALUE =
23
- 'npm run produck:publish:check && npm run publish --';
25
+ const REQUIRED_PUBLISH_SCRIPT_VALUE = [
26
+ 'npm run produck:publish:check',
27
+ 'npm run publish --',
28
+ ].join(' && ');
24
29
  const REQUIRED_LERNA_VERSION_COMMIT_HOOKS = false;
25
30
 
26
31
  export function printSyncPublishHelp() {
@@ -0,0 +1,14 @@
1
+ Usage:
2
+ agent-toolkit sync-typescript --package-root <pkg> [--cwd <dir>]
3
+ [--check] [--dry-run] [--json <file>]
4
+
5
+ Behavior:
6
+ - Creates a tsconfig.json for a sub-package that extends the root
7
+ tsconfig.json with the correct relative path
8
+ - If tsconfig.json already exists, skips without modifying
9
+
10
+ Rules:
11
+ - --package-root is required
12
+ - --check validates without writing and exits non-zero if file is missing
13
+ - --dry-run prints planned changes without writing
14
+ - --check takes precedence over --dry-run
@@ -0,0 +1,154 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import { fileURLToPath } from 'node:url';
4
+
5
+ import { getSingle, hasFlag } from '../shared/args.mjs';
6
+ import { printTextResource } from '../shared/text-resource.mjs';
7
+
8
+ const COMMAND_DIR = path.dirname(fileURLToPath(import.meta.url));
9
+ const HELP_FILE = path.resolve(COMMAND_DIR, 'help.txt');
10
+ const TSCONFIG_FILE = 'tsconfig.json';
11
+
12
+ const PACKAGE_TSCONFIG_TEMPLATE = {
13
+ compilerOptions: {
14
+ lib: ['ESNext'],
15
+ types: ['node'],
16
+ strictNullChecks: true,
17
+ allowJs: true,
18
+ noEmit: true,
19
+ module: 'NodeNext',
20
+ },
21
+ };
22
+
23
+ export function printSyncTypescriptHelp() {
24
+ printTextResource(HELP_FILE);
25
+ }
26
+
27
+ function readJsonIfExists(filePath) {
28
+ if (!fs.existsSync(filePath)) {
29
+ return null;
30
+ }
31
+ try {
32
+ return JSON.parse(fs.readFileSync(filePath, 'utf8'));
33
+ } catch {
34
+ return null;
35
+ }
36
+ }
37
+
38
+ function computeExtendsPath(packageRoot, repoRoot) {
39
+ const relative = path.relative(packageRoot, repoRoot);
40
+ const normalized = relative.replace(/\\/g, '/');
41
+ if (!normalized || normalized === '.') {
42
+ return './tsconfig.json';
43
+ }
44
+ return normalized.startsWith('..')
45
+ ? `${normalized}/tsconfig.json`
46
+ : `./${normalized}/tsconfig.json`;
47
+ }
48
+
49
+ export function runSyncTypescript(options) {
50
+ const cwd = path.resolve(getSingle(options, '--cwd', process.cwd()));
51
+ const check = hasFlag(options, '--check');
52
+ const dryRun = hasFlag(options, '--dry-run') && !check;
53
+ const jsonFile = getSingle(options, '--json', '');
54
+ const packageRoot = getSingle(options, '--package-root', '');
55
+ const mode = check ? 'check' : dryRun ? 'dry-run' : 'sync';
56
+
57
+ if (!packageRoot) {
58
+ console.error('--package-root is required');
59
+ process.exit(2);
60
+ }
61
+
62
+ if (!fs.existsSync(cwd)) {
63
+ console.error(`CWD does not exist: ${cwd}`);
64
+ process.exit(2);
65
+ }
66
+
67
+ const pkgDir = path.resolve(cwd, packageRoot);
68
+ if (!fs.existsSync(pkgDir)) {
69
+ console.error(`Package root does not exist: ${pkgDir}`);
70
+ process.exit(2);
71
+ }
72
+
73
+ const tsconfigPath = path.resolve(pkgDir, TSCONFIG_FILE);
74
+ const current = readJsonIfExists(tsconfigPath);
75
+ const fileExists = current !== null;
76
+
77
+ // If tsconfig.json already exists, skip without checking content
78
+ if (fileExists) {
79
+ const report = {
80
+ cwd,
81
+ mode,
82
+ ok: true,
83
+ tsconfigPath,
84
+ required: {
85
+ file: path.join(packageRoot, TSCONFIG_FILE),
86
+ },
87
+ status: {
88
+ fileExistsBefore: true,
89
+ mismatchesBefore: [],
90
+ fileExistsAfter: true,
91
+ mismatchesAfter: [],
92
+ updated: false,
93
+ skipped: true,
94
+ },
95
+ };
96
+
97
+ if (jsonFile) {
98
+ const outPath = path.resolve(cwd, jsonFile);
99
+ fs.mkdirSync(path.dirname(outPath), { recursive: true });
100
+ fs.writeFileSync(outPath, `${JSON.stringify(report, null, 2)}\n`, 'utf8');
101
+ }
102
+
103
+ process.stdout.write(`${JSON.stringify(report, null, 2)}\n`);
104
+ return;
105
+ }
106
+
107
+ const expectedExtends = computeExtendsPath(pkgDir, cwd);
108
+ const expectedPkgConfig = {
109
+ extends: expectedExtends,
110
+ ...PACKAGE_TSCONFIG_TEMPLATE,
111
+ };
112
+
113
+ const plannedContent = `${JSON.stringify(expectedPkgConfig, null, 2)}\n`;
114
+
115
+ if (mode === 'sync') {
116
+ fs.writeFileSync(tsconfigPath, plannedContent, 'utf8');
117
+ }
118
+
119
+ const mismatches = [
120
+ {
121
+ file: path.join(packageRoot, TSCONFIG_FILE),
122
+ expected: JSON.stringify(expectedPkgConfig, null, 2),
123
+ actual: 'missing',
124
+ },
125
+ ];
126
+
127
+ const report = {
128
+ cwd,
129
+ mode,
130
+ ok: mode !== 'check',
131
+ tsconfigPath,
132
+ required: {
133
+ file: path.join(packageRoot, TSCONFIG_FILE),
134
+ },
135
+ status: {
136
+ fileExistsBefore: false,
137
+ mismatchesBefore: mismatches,
138
+ fileExistsAfter: mode === 'sync',
139
+ mismatchesAfter: mode === 'sync' ? [] : mismatches,
140
+ updated: mode === 'sync',
141
+ },
142
+ };
143
+
144
+ if (jsonFile) {
145
+ const outPath = path.resolve(cwd, jsonFile);
146
+ fs.mkdirSync(path.dirname(outPath), { recursive: true });
147
+ fs.writeFileSync(outPath, `${JSON.stringify(report, null, 2)}\n`, 'utf8');
148
+ }
149
+
150
+ process.stdout.write(`${JSON.stringify(report, null, 2)}\n`);
151
+ if (!report.ok) {
152
+ process.exit(2);
153
+ }
154
+ }