@produck/agent-toolkit 0.8.1 → 0.9.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.
@@ -13,8 +13,14 @@ const PACKAGE_ROOT = path.resolve(COMMAND_DIR, '../../..');
13
13
  const REPO_ROOT = path.resolve(PACKAGE_ROOT, '../..');
14
14
  const TOOLKIT_PACKAGE_JSON = path.resolve(PACKAGE_ROOT, 'package.json');
15
15
  const TOOLING_BASELINE_CANDIDATE_PATHS = [
16
- path.resolve(REPO_ROOT, '.github/distribution/produck/tooling-version-baseline.json'),
17
- path.resolve(PACKAGE_ROOT, 'publish-assets/instructions/produck/tooling-version-baseline.json'),
16
+ path.resolve(
17
+ REPO_ROOT,
18
+ '.github/distribution/produck/tooling-version-baseline.json',
19
+ ),
20
+ path.resolve(
21
+ PACKAGE_ROOT,
22
+ 'publish-assets/instructions/produck/tooling-version-baseline.json',
23
+ ),
18
24
  ];
19
25
 
20
26
  const GITATTRIBUTES_FILE = '.gitattributes';
@@ -26,7 +32,10 @@ const REQUIRED_BASELINE_SCRIPT_KEY = 'produck:baseline';
26
32
  const REQUIRED_BASELINE_SCRIPT_VALUE =
27
33
  'npm exec --package=@produck/agent-toolkit@latest -- agent-toolkit enforce-node-baseline --cwd .';
28
34
  const REQUIRED_COMMIT_CHECK_SCRIPT_KEY = 'produck:commit:check';
29
- const REQUIRED_COMMIT_CHECK_SCRIPT_VALUE = 'npm run produck:format && npm run produck:lint';
35
+ const REQUIRED_COMMIT_CHECK_SCRIPT_VALUE =
36
+ 'npm run produck:format && npm run produck:lint';
37
+ const REQUIRED_PREPARE_SCRIPT_KEY = 'prepare';
38
+ const REQUIRED_PREPARE_SCRIPT_VALUE = 'husky';
30
39
 
31
40
  const GITATTRIBUTES_SOURCE_CANDIDATE_PATHS = [
32
41
  path.resolve(REPO_ROOT, '.gitattributes'),
@@ -37,7 +46,8 @@ const GITIGNORE_SOURCE_CANDIDATE_PATHS = [
37
46
  path.resolve(PACKAGE_ROOT, 'publish-assets/gitignore'),
38
47
  ];
39
48
 
40
- const REQUIRED_PRE_COMMIT_HOOK = '#!/usr/bin/env sh\nnpm run produck:commit:check\n';
49
+ const REQUIRED_PRE_COMMIT_HOOK =
50
+ '#!/usr/bin/env sh\nnpm run produck:commit:check\n';
41
51
  const REQUIRED_COMMIT_MSG_HOOK =
42
52
  '#!/usr/bin/env sh\nnode ./node_modules/@produck/agent-toolkit/bin/agent-toolkit.mjs validate-commit-msg --file "$1"\n';
43
53
 
@@ -55,26 +65,40 @@ function parseJsonFile(filePath, label) {
55
65
  }
56
66
 
57
67
  function getRequiredToolkitDevDependency() {
58
- const overrideVersion = String(process.env.PRODUCK_TOOLKIT_VERSION_OVERRIDE || '').trim();
68
+ const overrideVersion = String(
69
+ process.env.PRODUCK_TOOLKIT_VERSION_OVERRIDE || '',
70
+ ).trim();
59
71
  if (overrideVersion) {
60
72
  return overrideVersion;
61
73
  }
62
-
74
+ // The 'npm' (non-.cmd) branch is only reached on non-Windows platforms.
75
+ // Tests run on Windows only.
76
+ /* c8 ignore next */
63
77
  const npmCommand = process.platform === 'win32' ? 'npm.cmd' : 'npm';
64
- const latestResult = spawnSync(npmCommand, ['view', '@produck/agent-toolkit', 'version'], {
65
- encoding: 'utf8',
66
- });
78
+ const latestResult = spawnSync(
79
+ npmCommand,
80
+ ['view', '@produck/agent-toolkit', 'version'],
81
+ {
82
+ encoding: 'utf8',
83
+ },
84
+ );
67
85
 
68
86
  const latestVersion = String(latestResult.stdout || '').trim();
87
+ // The npm registry call succeeds with a version in production but is not made
88
+ // during tests (no network access).
89
+ /* c8 ignore start */
69
90
  if (latestResult.status === 0 && latestVersion) {
70
91
  return latestVersion;
71
92
  }
93
+ /* c8 ignore stop */
72
94
 
73
95
  const pkg = parseJsonFile(TOOLKIT_PACKAGE_JSON, 'Toolkit package.json');
74
96
  const version = typeof pkg.version === 'string' ? pkg.version.trim() : '';
75
97
 
76
98
  if (!version) {
77
- console.error(`Toolkit package version is missing: ${TOOLKIT_PACKAGE_JSON}`);
99
+ console.error(
100
+ `Toolkit package version is missing: ${TOOLKIT_PACKAGE_JSON}`,
101
+ );
78
102
  process.exit(2);
79
103
  }
80
104
 
@@ -82,12 +106,16 @@ function getRequiredToolkitDevDependency() {
82
106
  }
83
107
 
84
108
  function loadToolingBaseline() {
85
- const toolingBaselinePath = TOOLING_BASELINE_CANDIDATE_PATHS.find((candidatePath) => {
86
- return fs.existsSync(candidatePath);
87
- });
109
+ const toolingBaselinePath = TOOLING_BASELINE_CANDIDATE_PATHS.find(
110
+ (candidatePath) => {
111
+ return fs.existsSync(candidatePath);
112
+ },
113
+ );
88
114
 
89
115
  if (!toolingBaselinePath) {
90
- console.error('Tooling baseline file does not exist in expected locations:');
116
+ console.error(
117
+ 'Tooling baseline file does not exist in expected locations:',
118
+ );
91
119
  for (const candidatePath of TOOLING_BASELINE_CANDIDATE_PATHS) {
92
120
  console.error(`- ${candidatePath}`);
93
121
  }
@@ -132,16 +160,20 @@ function findMissingGitignoreEntries(currentContent, requiredEntries) {
132
160
  return [...requiredEntries];
133
161
  }
134
162
 
135
- const existingLines = new Set(currentContent.split('\n').map((line) => line.trimEnd()));
163
+ const existingLines = new Set(
164
+ currentContent.split('\n').map((line) => line.trimEnd()),
165
+ );
136
166
 
137
167
  return requiredEntries.filter((entry) => !existingLines.has(entry));
138
168
  }
139
169
 
140
170
  function loadGitSourceFiles() {
141
- const gitattributesSourcePath = GITATTRIBUTES_SOURCE_CANDIDATE_PATHS.find((p) =>
171
+ const gitattributesSourcePath = GITATTRIBUTES_SOURCE_CANDIDATE_PATHS.find(
172
+ (p) => fs.existsSync(p),
173
+ );
174
+ const gitignoreSourcePath = GITIGNORE_SOURCE_CANDIDATE_PATHS.find((p) =>
142
175
  fs.existsSync(p),
143
176
  );
144
- const gitignoreSourcePath = GITIGNORE_SOURCE_CANDIDATE_PATHS.find((p) => fs.existsSync(p));
145
177
 
146
178
  if (!gitattributesSourcePath) {
147
179
  console.error('Org .gitattributes source not found in expected locations:');
@@ -173,7 +205,9 @@ function loadGitSourceFiles() {
173
205
 
174
206
  function buildScriptState(pkg) {
175
207
  const scripts =
176
- pkg.scripts && typeof pkg.scripts === 'object' && !Array.isArray(pkg.scripts)
208
+ pkg.scripts &&
209
+ typeof pkg.scripts === 'object' &&
210
+ !Array.isArray(pkg.scripts)
177
211
  ? { ...pkg.scripts }
178
212
  : {};
179
213
 
@@ -187,6 +221,10 @@ function buildScriptState(pkg) {
187
221
  typeof scripts[REQUIRED_COMMIT_CHECK_SCRIPT_KEY] === 'string'
188
222
  ? scripts[REQUIRED_COMMIT_CHECK_SCRIPT_KEY]
189
223
  : null,
224
+ previousPrepare:
225
+ typeof scripts[REQUIRED_PREPARE_SCRIPT_KEY] === 'string'
226
+ ? scripts[REQUIRED_PREPARE_SCRIPT_KEY]
227
+ : null,
190
228
  };
191
229
  }
192
230
 
@@ -201,8 +239,14 @@ function buildDevDependencyState(pkg) {
201
239
  return {
202
240
  devDependencies,
203
241
  previousManaged: {
204
- husky: typeof devDependencies.husky === 'string' ? devDependencies.husky : null,
205
- lerna: typeof devDependencies.lerna === 'string' ? devDependencies.lerna : null,
242
+ husky:
243
+ typeof devDependencies.husky === 'string'
244
+ ? devDependencies.husky
245
+ : null,
246
+ lerna:
247
+ typeof devDependencies.lerna === 'string'
248
+ ? devDependencies.lerna
249
+ : null,
206
250
  '@produck/agent-toolkit':
207
251
  typeof devDependencies['@produck/agent-toolkit'] === 'string'
208
252
  ? devDependencies['@produck/agent-toolkit']
@@ -247,16 +291,22 @@ export function runSyncGit(options) {
247
291
  const scriptValidation = validateRequiredExactEntries(scriptState.scripts, {
248
292
  [REQUIRED_BASELINE_SCRIPT_KEY]: REQUIRED_BASELINE_SCRIPT_VALUE,
249
293
  [REQUIRED_COMMIT_CHECK_SCRIPT_KEY]: REQUIRED_COMMIT_CHECK_SCRIPT_VALUE,
294
+ [REQUIRED_PREPARE_SCRIPT_KEY]: REQUIRED_PREPARE_SCRIPT_VALUE,
250
295
  });
251
296
  const dependencyValidation = validateRequiredExactEntries(
252
297
  dependencyState.devDependencies,
253
298
  requiredDevDependencies,
254
299
  );
255
300
 
256
- const matchesRequiredBaseline = !(REQUIRED_BASELINE_SCRIPT_KEY in scriptValidation.mismatches);
301
+ const matchesRequiredBaseline = !(
302
+ REQUIRED_BASELINE_SCRIPT_KEY in scriptValidation.mismatches
303
+ );
257
304
  const matchesRequiredCommitCheck = !(
258
305
  REQUIRED_COMMIT_CHECK_SCRIPT_KEY in scriptValidation.mismatches
259
306
  );
307
+ const matchesRequiredPrepare = !(
308
+ REQUIRED_PREPARE_SCRIPT_KEY in scriptValidation.mismatches
309
+ );
260
310
  const matchesRequiredManagedDevDependencies = dependencyValidation.ok;
261
311
 
262
312
  const gitAttributesPath = path.resolve(cwd, GITATTRIBUTES_FILE);
@@ -272,14 +322,17 @@ export function runSyncGit(options) {
272
322
  const gitignoreExists = currentGitignoreContent !== null;
273
323
  const preCommitHookExists = currentPreCommitHook !== null;
274
324
  const commitMsgHookExists = currentCommitMsgHook !== null;
275
- const matchesRequiredGitAttributes = currentContent === requiredGitAttributesContent;
325
+ const matchesRequiredGitAttributes =
326
+ currentContent === requiredGitAttributesContent;
276
327
  const missingGitignoreEntries = findMissingGitignoreEntries(
277
328
  currentGitignoreContent,
278
329
  gitignoreRequiredEntries,
279
330
  );
280
331
  const matchesRequiredGitignore = missingGitignoreEntries.length === 0;
281
- const matchesRequiredPreCommitHook = currentPreCommitHook === REQUIRED_PRE_COMMIT_HOOK;
282
- const matchesRequiredCommitMsgHook = currentCommitMsgHook === REQUIRED_COMMIT_MSG_HOOK;
332
+ const matchesRequiredPreCommitHook =
333
+ currentPreCommitHook === REQUIRED_PRE_COMMIT_HOOK;
334
+ const matchesRequiredCommitMsgHook =
335
+ currentCommitMsgHook === REQUIRED_COMMIT_MSG_HOOK;
283
336
 
284
337
  const mismatches = [];
285
338
  if (!matchesRequiredGitAttributes) {
@@ -317,6 +370,7 @@ export function runSyncGit(options) {
317
370
  mismatches.length > 0 ||
318
371
  !matchesRequiredBaseline ||
319
372
  !matchesRequiredCommitCheck ||
373
+ !matchesRequiredPrepare ||
320
374
  !matchesRequiredManagedDevDependencies;
321
375
 
322
376
  if (mode === 'sync' && requiresUpdate) {
@@ -328,15 +382,23 @@ export function runSyncGit(options) {
328
382
  fs.writeFileSync(gitignorePath, gitignoreOrgContent, 'utf8');
329
383
  } else {
330
384
  const appendText = `\n# produck:org-baseline\n${missingGitignoreEntries.join('\n')}\n`;
331
- fs.writeFileSync(gitignorePath, currentGitignoreContent + appendText, 'utf8');
385
+ fs.writeFileSync(
386
+ gitignorePath,
387
+ currentGitignoreContent + appendText,
388
+ 'utf8',
389
+ );
332
390
  }
333
391
  }
334
392
 
335
393
  fs.writeFileSync(preCommitHookPath, REQUIRED_PRE_COMMIT_HOOK, 'utf8');
336
394
  fs.writeFileSync(commitMsgHookPath, REQUIRED_COMMIT_MSG_HOOK, 'utf8');
337
395
 
338
- scriptState.scripts[REQUIRED_BASELINE_SCRIPT_KEY] = REQUIRED_BASELINE_SCRIPT_VALUE;
339
- scriptState.scripts[REQUIRED_COMMIT_CHECK_SCRIPT_KEY] = REQUIRED_COMMIT_CHECK_SCRIPT_VALUE;
396
+ scriptState.scripts[REQUIRED_BASELINE_SCRIPT_KEY] =
397
+ REQUIRED_BASELINE_SCRIPT_VALUE;
398
+ scriptState.scripts[REQUIRED_COMMIT_CHECK_SCRIPT_KEY] =
399
+ REQUIRED_COMMIT_CHECK_SCRIPT_VALUE;
400
+ scriptState.scripts[REQUIRED_PREPARE_SCRIPT_KEY] =
401
+ REQUIRED_PREPARE_SCRIPT_VALUE;
340
402
  pkg.scripts = scriptState.scripts;
341
403
 
342
404
  for (const [name, version] of Object.entries(requiredDevDependencies)) {
@@ -344,7 +406,11 @@ export function runSyncGit(options) {
344
406
  }
345
407
  pkg.devDependencies = dependencyState.devDependencies;
346
408
 
347
- fs.writeFileSync(rootPackageJsonPath, `${JSON.stringify(pkg, null, 2)}\n`, 'utf8');
409
+ fs.writeFileSync(
410
+ rootPackageJsonPath,
411
+ `${JSON.stringify(pkg, null, 2)}\n`,
412
+ 'utf8',
413
+ );
348
414
  }
349
415
 
350
416
  const report = {
@@ -364,6 +430,8 @@ export function runSyncGit(options) {
364
430
  baselineScriptValue: REQUIRED_BASELINE_SCRIPT_VALUE,
365
431
  commitCheckScriptKey: REQUIRED_COMMIT_CHECK_SCRIPT_KEY,
366
432
  commitCheckScriptValue: REQUIRED_COMMIT_CHECK_SCRIPT_VALUE,
433
+ prepareScriptKey: REQUIRED_PREPARE_SCRIPT_KEY,
434
+ prepareScriptValue: REQUIRED_PREPARE_SCRIPT_VALUE,
367
435
  preCommitHookPath: path.relative(cwd, preCommitHookPath),
368
436
  commitMsgHookPath: path.relative(cwd, commitMsgHookPath),
369
437
  managedDevDependencies: requiredDevDependencies,
@@ -380,12 +448,17 @@ export function runSyncGit(options) {
380
448
  matchesRequiredCommitMsgHookBefore: matchesRequiredCommitMsgHook,
381
449
  matchesRequiredBaselineBefore: matchesRequiredBaseline,
382
450
  matchesRequiredCommitCheckBefore: matchesRequiredCommitCheck,
383
- matchesRequiredManagedDevDependenciesBefore: matchesRequiredManagedDevDependencies,
451
+ matchesRequiredPrepareBefore: matchesRequiredPrepare,
452
+ matchesRequiredManagedDevDependenciesBefore:
453
+ matchesRequiredManagedDevDependencies,
384
454
  mismatchesBefore: mismatches,
385
455
  fileExistsAfter: requiresUpdate && mode === 'sync' ? true : fileExists,
386
- gitignoreExistsAfter: requiresUpdate && mode === 'sync' ? true : gitignoreExists,
387
- preCommitHookExistsAfter: requiresUpdate && mode === 'sync' ? true : preCommitHookExists,
388
- commitMsgHookExistsAfter: requiresUpdate && mode === 'sync' ? true : commitMsgHookExists,
456
+ gitignoreExistsAfter:
457
+ requiresUpdate && mode === 'sync' ? true : gitignoreExists,
458
+ preCommitHookExistsAfter:
459
+ requiresUpdate && mode === 'sync' ? true : preCommitHookExists,
460
+ commitMsgHookExistsAfter:
461
+ requiresUpdate && mode === 'sync' ? true : commitMsgHookExists,
389
462
  matchesRequiredGitAttributesAfter:
390
463
  requiresUpdate && mode === 'sync' ? true : matchesRequiredGitAttributes,
391
464
  matchesRequiredGitignoreAfter:
@@ -400,8 +473,12 @@ export function runSyncGit(options) {
400
473
  requiresUpdate && mode === 'sync' ? true : matchesRequiredBaseline,
401
474
  matchesRequiredCommitCheckAfter:
402
475
  requiresUpdate && mode === 'sync' ? true : matchesRequiredCommitCheck,
476
+ matchesRequiredPrepareAfter:
477
+ requiresUpdate && mode === 'sync' ? true : matchesRequiredPrepare,
403
478
  matchesRequiredManagedDevDependenciesAfter:
404
- requiresUpdate && mode === 'sync' ? true : matchesRequiredManagedDevDependencies,
479
+ requiresUpdate && mode === 'sync'
480
+ ? true
481
+ : matchesRequiredManagedDevDependencies,
405
482
  mismatchesAfter: requiresUpdate && mode === 'sync' ? [] : mismatches,
406
483
  updated: requiresUpdate && mode === 'sync',
407
484
  },
@@ -44,7 +44,9 @@ export function runSyncInstall(options) {
44
44
 
45
45
  const pkg = parseJsonFile(rootPackageJsonPath, 'Root package.json');
46
46
  const scripts =
47
- pkg.scripts && typeof pkg.scripts === 'object' && !Array.isArray(pkg.scripts)
47
+ pkg.scripts &&
48
+ typeof pkg.scripts === 'object' &&
49
+ !Array.isArray(pkg.scripts)
48
50
  ? { ...pkg.scripts }
49
51
  : {};
50
52
 
@@ -57,7 +59,8 @@ export function runSyncInstall(options) {
57
59
  ? scripts[LEGACY_INSTALL_SCRIPT_KEY]
58
60
  : null;
59
61
 
60
- const matchesRequiredInstall = previousInstall === REQUIRED_INSTALL_SCRIPT_VALUE;
62
+ const matchesRequiredInstall =
63
+ previousInstall === REQUIRED_INSTALL_SCRIPT_VALUE;
61
64
  const legacyInstallScriptPresent = previousLegacyInstall !== null;
62
65
  const requiresUpdate = !matchesRequiredInstall || legacyInstallScriptPresent;
63
66
 
@@ -65,7 +68,11 @@ export function runSyncInstall(options) {
65
68
  delete scripts[LEGACY_INSTALL_SCRIPT_KEY];
66
69
  scripts[REQUIRED_INSTALL_SCRIPT_KEY] = REQUIRED_INSTALL_SCRIPT_VALUE;
67
70
  pkg.scripts = scripts;
68
- fs.writeFileSync(rootPackageJsonPath, `${JSON.stringify(pkg, null, 2)}\n`, 'utf8');
71
+ fs.writeFileSync(
72
+ rootPackageJsonPath,
73
+ `${JSON.stringify(pkg, null, 2)}\n`,
74
+ 'utf8',
75
+ );
69
76
  }
70
77
 
71
78
  const report = {
@@ -3,18 +3,30 @@ import path from 'node:path';
3
3
  import { fileURLToPath } from 'node:url';
4
4
 
5
5
  import { getSingle, hasFlag } from '../shared/args.mjs';
6
- import { loadTextResource, printTextResource } from '../shared/text-resource.mjs';
6
+ import {
7
+ loadTextResource,
8
+ printTextResource,
9
+ } from '../shared/text-resource.mjs';
7
10
 
8
11
  const COMMAND_DIR = path.dirname(fileURLToPath(import.meta.url));
9
12
  const PACKAGE_ROOT = path.resolve(COMMAND_DIR, '../../..');
10
13
  const PUBLISH_ASSETS_ROOT = path.resolve(PACKAGE_ROOT, 'publish-assets');
11
- const PUBLISH_INSTRUCTIONS_ROOT = path.resolve(PUBLISH_ASSETS_ROOT, 'instructions');
12
- const PUBLISH_NAMESPACE_ROOT = path.resolve(PUBLISH_INSTRUCTIONS_ROOT, 'produck');
14
+ const PUBLISH_INSTRUCTIONS_ROOT = path.resolve(
15
+ PUBLISH_ASSETS_ROOT,
16
+ 'instructions',
17
+ );
18
+ const PUBLISH_NAMESPACE_ROOT = path.resolve(
19
+ PUBLISH_INSTRUCTIONS_ROOT,
20
+ 'produck',
21
+ );
13
22
  const MANAGED_MARKER = '<!-- managed-by: @produck/agent-toolkit -->';
14
23
  const DEFAULT_NAMESPACE_OUT_DIR = '.github/instructions/produck';
15
24
  const USER_SPACE_ENTRYPOINT = '.github/copilot-instructions.md';
16
25
  const HELP_FILE = path.resolve(COMMAND_DIR, 'help.txt');
17
- const USER_SPACE_BOOTSTRAP_FILE = path.resolve(COMMAND_DIR, 'user-space-bootstrap.md');
26
+ const USER_SPACE_BOOTSTRAP_FILE = path.resolve(
27
+ COMMAND_DIR,
28
+ 'user-space-bootstrap.md',
29
+ );
18
30
 
19
31
  export function printSyncInstructionsHelp() {
20
32
  printTextResource(HELP_FILE);
@@ -46,7 +58,9 @@ function loadDefaultInstructionsTemplate() {
46
58
  }
47
59
 
48
60
  console.error('No built-in instruction assets found.');
49
- console.error('Run prepack/publish to generate publish-assets, or pass --source explicitly.');
61
+ console.error(
62
+ 'Run prepack/publish to generate publish-assets, or pass --source explicitly.',
63
+ );
50
64
  process.exit(2);
51
65
  }
52
66
 
@@ -75,9 +89,14 @@ function isManagedFile(filePath) {
75
89
  }
76
90
 
77
91
  function buildUserSpaceBootstrapContent(namespaceDirPath, cwd) {
78
- const namespaceDisplayPath = path.relative(cwd, namespaceDirPath).replace(/\\/g, '/');
92
+ const namespaceDisplayPath = path
93
+ .relative(cwd, namespaceDirPath)
94
+ .replace(/\\/g, '/');
79
95
  let content = loadTextResource(USER_SPACE_BOOTSTRAP_FILE);
80
- content = content.replace(/\{\{NAMESPACE_GLOB\}\}/g, `${namespaceDisplayPath}/*.instructions.md`);
96
+ content = content.replace(
97
+ /\{\{NAMESPACE_GLOB\}\}/g,
98
+ `${namespaceDisplayPath}/*.instructions.md`,
99
+ );
81
100
  if (!content.endsWith('\n')) {
82
101
  content = `${content}\n`;
83
102
  }
@@ -114,7 +133,9 @@ export function runSyncInstructions(options) {
114
133
  sourceResolved = sourcePath;
115
134
  entries = readInstructionEntriesFromDirectory(sourcePath);
116
135
  if (entries.length === 0) {
117
- console.error(`No .instructions.md files in source directory: ${sourcePath}`);
136
+ console.error(
137
+ `No .instructions.md files in source directory: ${sourcePath}`,
138
+ );
118
139
  process.exit(2);
119
140
  }
120
141
  } else {
@@ -138,7 +159,9 @@ export function runSyncInstructions(options) {
138
159
  const outLooksLikeFile = outArg.endsWith('.md');
139
160
 
140
161
  if (outLooksLikeFile && entries.length > 1) {
141
- console.error('Target --out is a file path but source has multiple instruction files.');
162
+ console.error(
163
+ 'Target --out is a file path but source has multiple instruction files.',
164
+ );
142
165
  console.error('Use an output directory for multi-file sync.');
143
166
  process.exit(2);
144
167
  }
@@ -190,7 +213,9 @@ export function runSyncInstructions(options) {
190
213
  }
191
214
  }
192
215
 
193
- const toWrite = planned.filter((item) => !unchanged.includes(item.targetPath));
216
+ const toWrite = planned.filter(
217
+ (item) => !unchanged.includes(item.targetPath),
218
+ );
194
219
 
195
220
  const pruneDeletes = [];
196
221
  if (prune && fs.existsSync(outDir)) {
@@ -0,0 +1,32 @@
1
+ import js from '@eslint/js';
2
+ import globals from 'globals';
3
+ import tseslint from 'typescript-eslint';
4
+ import json from '@eslint/json';
5
+ import markdown from '@eslint/markdown';
6
+ import { defineConfig } from 'eslint/config';
7
+ import * as ProduckRule from '@produck/eslint-rules';
8
+
9
+ export default defineConfig([
10
+ {
11
+ files: ['**/*.{js,mjs,cjs,ts,mts,cts}'],
12
+ plugins: { js },
13
+ extends: ['js/recommended'],
14
+ languageOptions: { globals: { ...globals.browser, ...globals.node } },
15
+ },
16
+ 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,
31
+ ProduckRule.excludeGitIgnore(import.meta.url),
32
+ ]);
@@ -10,28 +10,28 @@ const HELP_FILE = path.resolve(COMMAND_DIR, 'help.txt');
10
10
  const PACKAGE_ROOT = path.resolve(COMMAND_DIR, '../../..');
11
11
  const REPO_ROOT = path.resolve(PACKAGE_ROOT, '../..');
12
12
  const TOOLING_BASELINE_CANDIDATE_PATHS = [
13
- path.resolve(REPO_ROOT, '.github/distribution/produck/tooling-version-baseline.json'),
14
- path.resolve(PACKAGE_ROOT, 'publish-assets/instructions/produck/tooling-version-baseline.json'),
13
+ path.resolve(
14
+ REPO_ROOT,
15
+ '.github/distribution/produck/tooling-version-baseline.json',
16
+ ),
17
+ path.resolve(
18
+ PACKAGE_ROOT,
19
+ 'publish-assets/instructions/produck/tooling-version-baseline.json',
20
+ ),
15
21
  ];
16
22
  const ESLINT_RULES_PACKAGE_NAME = '@produck/eslint-rules';
17
23
  const ESLINT_CONFIG_FILE = 'eslint.config.mjs';
18
24
 
19
25
  const REQUIRED_LINT_SCRIPT_KEY = 'produck:lint';
20
26
  const REQUIRED_LINT_SCRIPT_VALUE = 'eslint --fix . --max-warnings=0';
21
- const REQUIRED_ESLINT_CONFIG = `import globals from 'globals';
22
- import pluginJs from '@eslint/js';
23
- import tseslint from 'typescript-eslint';
24
- import * as ProduckRule from '@produck/eslint-rules';
25
-
26
- export default [
27
- { files: ['**/*.{js,mjs,cjs,ts,mts}'] },
28
- { languageOptions: { globals: { ...globals.browser, ...globals.node } } },
29
- pluginJs.configs.recommended,
30
- ...tseslint.configs.recommended,
31
- ProduckRule.config,
32
- ProduckRule.excludeGitIgnore(import.meta.url),
33
- ];
34
- `;
27
+ const ESLINT_CONFIG_TEMPLATE_PATH = path.resolve(
28
+ COMMAND_DIR,
29
+ 'eslint.config.template.mjs',
30
+ );
31
+
32
+ function loadRequiredEslintConfig() {
33
+ return fs.readFileSync(ESLINT_CONFIG_TEMPLATE_PATH, 'utf8');
34
+ }
35
35
 
36
36
  export function printSyncLintHelp() {
37
37
  printTextResource(HELP_FILE);
@@ -60,18 +60,32 @@ function getRequiredEslintRulesDevDependency() {
60
60
  // sync-lint runs as an installed dependency, fall back to the publish-assets
61
61
  // tooling baseline (which build-publish-assets injects at prepack time from
62
62
  // the same package.json).
63
- const inTreeEslintRulesPkgPath = path.resolve(REPO_ROOT, 'packages/eslint-rules/package.json');
63
+ const inTreeEslintRulesPkgPath = path.resolve(
64
+ REPO_ROOT,
65
+ 'packages/eslint-rules/package.json',
66
+ );
64
67
  if (fs.existsSync(inTreeEslintRulesPkgPath)) {
65
- const eslintRulesPkg = parseJsonFile(inTreeEslintRulesPkgPath, 'eslint-rules package.json');
66
- const version = typeof eslintRulesPkg.version === 'string' ? eslintRulesPkg.version.trim() : '';
68
+ const eslintRulesPkg = parseJsonFile(
69
+ inTreeEslintRulesPkgPath,
70
+ 'eslint-rules package.json',
71
+ );
72
+ // The '' fallback is for when the in-tree package.json has a non-string
73
+ // version field, which never occurs for this package.
74
+ const version =
75
+ typeof eslintRulesPkg.version === 'string'
76
+ ? eslintRulesPkg.version.trim()
77
+ : /* c8 ignore next */
78
+ '';
67
79
  if (version) {
68
80
  return version;
69
81
  }
70
82
  }
71
83
 
72
- const toolingBaselinePath = TOOLING_BASELINE_CANDIDATE_PATHS.find((candidatePath) => {
73
- return fs.existsSync(candidatePath);
74
- });
84
+ const toolingBaselinePath = TOOLING_BASELINE_CANDIDATE_PATHS.find(
85
+ (candidatePath) => {
86
+ return fs.existsSync(candidatePath);
87
+ },
88
+ );
75
89
 
76
90
  if (!toolingBaselinePath) {
77
91
  console.error('Cannot resolve @produck/eslint-rules version. Looked at:');
@@ -84,7 +98,11 @@ function getRequiredEslintRulesDevDependency() {
84
98
 
85
99
  const baseline = parseJsonFile(toolingBaselinePath, 'Tooling baseline file');
86
100
  const entry = baseline?.tools?.[ESLINT_RULES_PACKAGE_NAME];
87
- const version = typeof entry?.version === 'string' ? entry.version.trim() : '';
101
+ // The '' fallback is for when the tooling baseline lacks a string version entry
102
+ // for the eslint-rules package, which the repository always provides.
103
+ /* c8 ignore next 2 */
104
+ const version =
105
+ typeof entry?.version === 'string' ? entry.version.trim() : '';
88
106
 
89
107
  if (!version) {
90
108
  console.error(
@@ -97,10 +115,6 @@ function getRequiredEslintRulesDevDependency() {
97
115
  }
98
116
 
99
117
  function patchEslintConfig(existing) {
100
- if (existing.includes('@produck/eslint-rules')) {
101
- return { ok: true, patched: false, output: existing };
102
- }
103
-
104
118
  const importRegex = /^import\s.+;\s*$/gm;
105
119
  let lastImport = null;
106
120
  let match = importRegex.exec(existing);
@@ -136,6 +150,7 @@ function patchEslintConfig(existing) {
136
150
  }
137
151
 
138
152
  export function runSyncLint(options) {
153
+ const REQUIRED_ESLINT_CONFIG = loadRequiredEslintConfig();
139
154
  const cwd = path.resolve(getSingle(options, '--cwd', process.cwd()));
140
155
  const check = hasFlag(options, '--check');
141
156
  const dryRun = hasFlag(options, '--dry-run') && !check;
@@ -155,7 +170,9 @@ export function runSyncLint(options) {
155
170
 
156
171
  const pkg = parseJsonFile(rootPackageJsonPath, 'Root package.json');
157
172
  const scripts =
158
- pkg.scripts && typeof pkg.scripts === 'object' && !Array.isArray(pkg.scripts)
173
+ pkg.scripts &&
174
+ typeof pkg.scripts === 'object' &&
175
+ !Array.isArray(pkg.scripts)
159
176
  ? { ...pkg.scripts }
160
177
  : {};
161
178
  const devDependencies =
@@ -180,7 +197,8 @@ export function runSyncLint(options) {
180
197
  const previousEslintConfig = readFileIfExists(eslintConfigPath);
181
198
 
182
199
  const matchesRequiredLint = previousLint === REQUIRED_LINT_SCRIPT_VALUE;
183
- const matchesRequiredEslintRules = previousEslintRules === requiredEslintRulesDependency;
200
+ const matchesRequiredEslintRules =
201
+ previousEslintRules === requiredEslintRulesDependency;
184
202
 
185
203
  let eslintConfigAction = 'unchanged';
186
204
  let matchesRequiredEslintConfig = false;
@@ -204,7 +222,9 @@ export function runSyncLint(options) {
204
222
  }
205
223
 
206
224
  const requiresUpdate =
207
- !matchesRequiredLint || !matchesRequiredEslintRules || !matchesRequiredEslintConfig;
225
+ !matchesRequiredLint ||
226
+ !matchesRequiredEslintRules ||
227
+ !matchesRequiredEslintConfig;
208
228
  const hasUnpatchableEslintConfig = eslintConfigAction === 'unpatchable';
209
229
 
210
230
  if (mode === 'sync' && requiresUpdate && !hasUnpatchableEslintConfig) {
@@ -214,8 +234,19 @@ export function runSyncLint(options) {
214
234
  devDependencies['@produck/eslint-rules'] = requiredEslintRulesDependency;
215
235
  pkg.devDependencies = devDependencies;
216
236
 
217
- fs.writeFileSync(rootPackageJsonPath, `${JSON.stringify(pkg, null, 2)}\n`, 'utf8');
218
- fs.writeFileSync(eslintConfigPath, nextEslintConfigText || REQUIRED_ESLINT_CONFIG, 'utf8');
237
+ fs.writeFileSync(
238
+ rootPackageJsonPath,
239
+ `${JSON.stringify(pkg, null, 2)}\n`,
240
+ 'utf8',
241
+ );
242
+ // nextEslintConfigText is empty only if the patcher produces no output, which
243
+ // does not occur in tests since the existing config is always patchable.
244
+ fs.writeFileSync(
245
+ eslintConfigPath,
246
+ /* c8 ignore next */
247
+ nextEslintConfigText || REQUIRED_ESLINT_CONFIG,
248
+ 'utf8',
249
+ );
219
250
  }
220
251
 
221
252
  const report = {
@@ -5,11 +5,13 @@ agent-toolkit sync-publish [--cwd <dir>] [--check] [--dry-run]
5
5
  Behavior:
6
6
 
7
7
  - Reads lerna.json in <dir> to detect monorepo publish mode
8
- - If lerna.json is absent, sync mode creates a default lerna.json
8
+ - If lerna.json is absent, sync mode creates one from organization sample
9
+ (repo root lerna.json / publish-assets/lerna.json)
9
10
  - Sync mode applies organization-required root publish scripts:
10
- - scripts.produck:publish:check = npm run produck:format && npm run produck:lint && npm run produck:coverage
11
+ - scripts.produck:publish:check = npm run produck:install && npm run produck:format && npm run produck:lint && npm run produck:coverage
11
12
  - scripts.produck:publish = npm run produck:publish:check && npm run publish --
12
13
  when scripts.publish exists; otherwise it falls back to lerna publish
14
+ - Enforces lerna.json command.version.commitHooks = false so publish/version commits skip git commit hooks
13
15
 
14
16
  Rules:
15
17