@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.
- package/README.md +22 -22
- package/bin/agent-toolkit.mjs +33 -8
- package/bin/build-publish-assets.mjs +132 -24
- package/bin/command/enforce-node-baseline/index.mjs +18 -5
- package/bin/command/preflight/index.mjs +20 -4
- package/bin/command/run-capture/index.mjs +7 -1
- package/bin/command/shared/workspace-validation.mjs +9 -3
- package/bin/command/sync-coverage/index.mjs +103 -48
- package/bin/command/sync-coverage/required-c8-config.json +15 -0
- package/bin/command/sync-editorconfig/index.mjs +2 -1
- package/bin/command/sync-format/index.mjs +92 -37
- package/bin/command/sync-git/help.txt +1 -0
- package/bin/command/sync-git/index.mjs +110 -33
- package/bin/command/sync-install/index.mjs +10 -3
- package/bin/command/sync-instructions/index.mjs +35 -10
- package/bin/command/sync-lint/eslint.config.template.mjs +32 -0
- package/bin/command/sync-lint/index.mjs +63 -32
- package/bin/command/sync-publish/help.txt +4 -2
- package/bin/command/sync-publish/index.mjs +126 -35
- package/bin/command/validate-commit-msg/index.mjs +46 -25
- package/package.json +5 -3
- package/publish-assets/eslint.config.template.mjs +32 -0
- package/publish-assets/gitignore +3 -0
- package/publish-assets/instructions/produck/20-produck-commit.instructions.md +3 -3
- package/publish-assets/instructions/produck/stale.instructions.md +1 -0
- package/publish-assets/instructions/produck/tooling-version-baseline.json +36 -1
- package/publish-assets/lerna.json +14 -0
- package/publish-assets/prettierrc +11 -0
|
@@ -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(
|
|
17
|
-
|
|
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 =
|
|
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 =
|
|
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(
|
|
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(
|
|
65
|
-
|
|
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(
|
|
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(
|
|
86
|
-
|
|
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(
|
|
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(
|
|
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(
|
|
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 &&
|
|
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:
|
|
205
|
-
|
|
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 = !(
|
|
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 =
|
|
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 =
|
|
282
|
-
|
|
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(
|
|
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] =
|
|
339
|
-
|
|
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(
|
|
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
|
-
|
|
451
|
+
matchesRequiredPrepareBefore: matchesRequiredPrepare,
|
|
452
|
+
matchesRequiredManagedDevDependenciesBefore:
|
|
453
|
+
matchesRequiredManagedDevDependencies,
|
|
384
454
|
mismatchesBefore: mismatches,
|
|
385
455
|
fileExistsAfter: requiresUpdate && mode === 'sync' ? true : fileExists,
|
|
386
|
-
gitignoreExistsAfter:
|
|
387
|
-
|
|
388
|
-
|
|
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'
|
|
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 &&
|
|
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 =
|
|
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(
|
|
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 {
|
|
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(
|
|
12
|
-
|
|
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(
|
|
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(
|
|
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
|
|
92
|
+
const namespaceDisplayPath = path
|
|
93
|
+
.relative(cwd, namespaceDirPath)
|
|
94
|
+
.replace(/\\/g, '/');
|
|
79
95
|
let content = loadTextResource(USER_SPACE_BOOTSTRAP_FILE);
|
|
80
|
-
content = content.replace(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
14
|
-
|
|
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
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
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(
|
|
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(
|
|
66
|
-
|
|
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(
|
|
73
|
-
|
|
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
|
-
|
|
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 &&
|
|
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 =
|
|
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 ||
|
|
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(
|
|
218
|
-
|
|
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
|
|
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
|
|