@produck/agent-toolkit 0.8.2 → 0.9.1

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,8 @@ 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';
30
37
  const REQUIRED_PREPARE_SCRIPT_KEY = 'prepare';
31
38
  const REQUIRED_PREPARE_SCRIPT_VALUE = 'husky';
32
39
 
@@ -39,7 +46,8 @@ const GITIGNORE_SOURCE_CANDIDATE_PATHS = [
39
46
  path.resolve(PACKAGE_ROOT, 'publish-assets/gitignore'),
40
47
  ];
41
48
 
42
- 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';
43
51
  const REQUIRED_COMMIT_MSG_HOOK =
44
52
  '#!/usr/bin/env sh\nnode ./node_modules/@produck/agent-toolkit/bin/agent-toolkit.mjs validate-commit-msg --file "$1"\n';
45
53
 
@@ -57,26 +65,40 @@ function parseJsonFile(filePath, label) {
57
65
  }
58
66
 
59
67
  function getRequiredToolkitDevDependency() {
60
- const overrideVersion = String(process.env.PRODUCK_TOOLKIT_VERSION_OVERRIDE || '').trim();
68
+ const overrideVersion = String(
69
+ process.env.PRODUCK_TOOLKIT_VERSION_OVERRIDE || '',
70
+ ).trim();
61
71
  if (overrideVersion) {
62
72
  return overrideVersion;
63
73
  }
64
-
74
+ // The 'npm' (non-.cmd) branch is only reached on non-Windows platforms.
75
+ // Tests run on Windows only.
76
+ /* c8 ignore next */
65
77
  const npmCommand = process.platform === 'win32' ? 'npm.cmd' : 'npm';
66
- const latestResult = spawnSync(npmCommand, ['view', '@produck/agent-toolkit', 'version'], {
67
- encoding: 'utf8',
68
- });
78
+ const latestResult = spawnSync(
79
+ npmCommand,
80
+ ['view', '@produck/agent-toolkit', 'version'],
81
+ {
82
+ encoding: 'utf8',
83
+ },
84
+ );
69
85
 
70
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 */
71
90
  if (latestResult.status === 0 && latestVersion) {
72
91
  return latestVersion;
73
92
  }
93
+ /* c8 ignore stop */
74
94
 
75
95
  const pkg = parseJsonFile(TOOLKIT_PACKAGE_JSON, 'Toolkit package.json');
76
96
  const version = typeof pkg.version === 'string' ? pkg.version.trim() : '';
77
97
 
78
98
  if (!version) {
79
- console.error(`Toolkit package version is missing: ${TOOLKIT_PACKAGE_JSON}`);
99
+ console.error(
100
+ `Toolkit package version is missing: ${TOOLKIT_PACKAGE_JSON}`,
101
+ );
80
102
  process.exit(2);
81
103
  }
82
104
 
@@ -84,12 +106,16 @@ function getRequiredToolkitDevDependency() {
84
106
  }
85
107
 
86
108
  function loadToolingBaseline() {
87
- const toolingBaselinePath = TOOLING_BASELINE_CANDIDATE_PATHS.find((candidatePath) => {
88
- return fs.existsSync(candidatePath);
89
- });
109
+ const toolingBaselinePath = TOOLING_BASELINE_CANDIDATE_PATHS.find(
110
+ (candidatePath) => {
111
+ return fs.existsSync(candidatePath);
112
+ },
113
+ );
90
114
 
91
115
  if (!toolingBaselinePath) {
92
- 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
+ );
93
119
  for (const candidatePath of TOOLING_BASELINE_CANDIDATE_PATHS) {
94
120
  console.error(`- ${candidatePath}`);
95
121
  }
@@ -134,16 +160,20 @@ function findMissingGitignoreEntries(currentContent, requiredEntries) {
134
160
  return [...requiredEntries];
135
161
  }
136
162
 
137
- 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
+ );
138
166
 
139
167
  return requiredEntries.filter((entry) => !existingLines.has(entry));
140
168
  }
141
169
 
142
170
  function loadGitSourceFiles() {
143
- 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) =>
144
175
  fs.existsSync(p),
145
176
  );
146
- const gitignoreSourcePath = GITIGNORE_SOURCE_CANDIDATE_PATHS.find((p) => fs.existsSync(p));
147
177
 
148
178
  if (!gitattributesSourcePath) {
149
179
  console.error('Org .gitattributes source not found in expected locations:');
@@ -175,7 +205,9 @@ function loadGitSourceFiles() {
175
205
 
176
206
  function buildScriptState(pkg) {
177
207
  const scripts =
178
- pkg.scripts && typeof pkg.scripts === 'object' && !Array.isArray(pkg.scripts)
208
+ pkg.scripts &&
209
+ typeof pkg.scripts === 'object' &&
210
+ !Array.isArray(pkg.scripts)
179
211
  ? { ...pkg.scripts }
180
212
  : {};
181
213
 
@@ -207,8 +239,14 @@ function buildDevDependencyState(pkg) {
207
239
  return {
208
240
  devDependencies,
209
241
  previousManaged: {
210
- husky: typeof devDependencies.husky === 'string' ? devDependencies.husky : null,
211
- 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,
212
250
  '@produck/agent-toolkit':
213
251
  typeof devDependencies['@produck/agent-toolkit'] === 'string'
214
252
  ? devDependencies['@produck/agent-toolkit']
@@ -260,11 +298,15 @@ export function runSyncGit(options) {
260
298
  requiredDevDependencies,
261
299
  );
262
300
 
263
- const matchesRequiredBaseline = !(REQUIRED_BASELINE_SCRIPT_KEY in scriptValidation.mismatches);
301
+ const matchesRequiredBaseline = !(
302
+ REQUIRED_BASELINE_SCRIPT_KEY in scriptValidation.mismatches
303
+ );
264
304
  const matchesRequiredCommitCheck = !(
265
305
  REQUIRED_COMMIT_CHECK_SCRIPT_KEY in scriptValidation.mismatches
266
306
  );
267
- const matchesRequiredPrepare = !(REQUIRED_PREPARE_SCRIPT_KEY in scriptValidation.mismatches);
307
+ const matchesRequiredPrepare = !(
308
+ REQUIRED_PREPARE_SCRIPT_KEY in scriptValidation.mismatches
309
+ );
268
310
  const matchesRequiredManagedDevDependencies = dependencyValidation.ok;
269
311
 
270
312
  const gitAttributesPath = path.resolve(cwd, GITATTRIBUTES_FILE);
@@ -280,14 +322,17 @@ export function runSyncGit(options) {
280
322
  const gitignoreExists = currentGitignoreContent !== null;
281
323
  const preCommitHookExists = currentPreCommitHook !== null;
282
324
  const commitMsgHookExists = currentCommitMsgHook !== null;
283
- const matchesRequiredGitAttributes = currentContent === requiredGitAttributesContent;
325
+ const matchesRequiredGitAttributes =
326
+ currentContent === requiredGitAttributesContent;
284
327
  const missingGitignoreEntries = findMissingGitignoreEntries(
285
328
  currentGitignoreContent,
286
329
  gitignoreRequiredEntries,
287
330
  );
288
331
  const matchesRequiredGitignore = missingGitignoreEntries.length === 0;
289
- const matchesRequiredPreCommitHook = currentPreCommitHook === REQUIRED_PRE_COMMIT_HOOK;
290
- 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;
291
336
 
292
337
  const mismatches = [];
293
338
  if (!matchesRequiredGitAttributes) {
@@ -337,16 +382,23 @@ export function runSyncGit(options) {
337
382
  fs.writeFileSync(gitignorePath, gitignoreOrgContent, 'utf8');
338
383
  } else {
339
384
  const appendText = `\n# produck:org-baseline\n${missingGitignoreEntries.join('\n')}\n`;
340
- fs.writeFileSync(gitignorePath, currentGitignoreContent + appendText, 'utf8');
385
+ fs.writeFileSync(
386
+ gitignorePath,
387
+ currentGitignoreContent + appendText,
388
+ 'utf8',
389
+ );
341
390
  }
342
391
  }
343
392
 
344
393
  fs.writeFileSync(preCommitHookPath, REQUIRED_PRE_COMMIT_HOOK, 'utf8');
345
394
  fs.writeFileSync(commitMsgHookPath, REQUIRED_COMMIT_MSG_HOOK, 'utf8');
346
395
 
347
- scriptState.scripts[REQUIRED_BASELINE_SCRIPT_KEY] = REQUIRED_BASELINE_SCRIPT_VALUE;
348
- scriptState.scripts[REQUIRED_COMMIT_CHECK_SCRIPT_KEY] = REQUIRED_COMMIT_CHECK_SCRIPT_VALUE;
349
- scriptState.scripts[REQUIRED_PREPARE_SCRIPT_KEY] = REQUIRED_PREPARE_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;
350
402
  pkg.scripts = scriptState.scripts;
351
403
 
352
404
  for (const [name, version] of Object.entries(requiredDevDependencies)) {
@@ -354,7 +406,11 @@ export function runSyncGit(options) {
354
406
  }
355
407
  pkg.devDependencies = dependencyState.devDependencies;
356
408
 
357
- 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
+ );
358
414
  }
359
415
 
360
416
  const report = {
@@ -393,12 +449,16 @@ export function runSyncGit(options) {
393
449
  matchesRequiredBaselineBefore: matchesRequiredBaseline,
394
450
  matchesRequiredCommitCheckBefore: matchesRequiredCommitCheck,
395
451
  matchesRequiredPrepareBefore: matchesRequiredPrepare,
396
- matchesRequiredManagedDevDependenciesBefore: matchesRequiredManagedDevDependencies,
452
+ matchesRequiredManagedDevDependenciesBefore:
453
+ matchesRequiredManagedDevDependencies,
397
454
  mismatchesBefore: mismatches,
398
455
  fileExistsAfter: requiresUpdate && mode === 'sync' ? true : fileExists,
399
- gitignoreExistsAfter: requiresUpdate && mode === 'sync' ? true : gitignoreExists,
400
- preCommitHookExistsAfter: requiresUpdate && mode === 'sync' ? true : preCommitHookExists,
401
- 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,
402
462
  matchesRequiredGitAttributesAfter:
403
463
  requiresUpdate && mode === 'sync' ? true : matchesRequiredGitAttributes,
404
464
  matchesRequiredGitignoreAfter:
@@ -416,7 +476,9 @@ export function runSyncGit(options) {
416
476
  matchesRequiredPrepareAfter:
417
477
  requiresUpdate && mode === 'sync' ? true : matchesRequiredPrepare,
418
478
  matchesRequiredManagedDevDependenciesAfter:
419
- requiresUpdate && mode === 'sync' ? true : matchesRequiredManagedDevDependencies,
479
+ requiresUpdate && mode === 'sync'
480
+ ? true
481
+ : matchesRequiredManagedDevDependencies,
420
482
  mismatchesAfter: requiresUpdate && mode === 'sync' ? [] : mismatches,
421
483
  updated: requiresUpdate && mode === 'sync',
422
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
+ ]);