@synoikia/create-plugin 0.3.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +15 -0
- package/dist/cli.js +4 -0
- package/dist/cli.js.map +1 -1
- package/dist/scaffold.js +6 -1
- package/dist/scaffold.js.map +1 -1
- package/dist/workflow/branch.d.ts +2 -0
- package/dist/workflow/branch.js +31 -0
- package/dist/workflow/branch.js.map +1 -0
- package/dist/workflow/config.d.ts +24 -0
- package/dist/workflow/config.js +25 -0
- package/dist/workflow/config.js.map +1 -0
- package/dist/workflow/features.d.ts +20 -0
- package/dist/workflow/features.js +165 -0
- package/dist/workflow/features.js.map +1 -0
- package/dist/workflow/git.d.ts +12 -0
- package/dist/workflow/git.js +42 -0
- package/dist/workflow/git.js.map +1 -0
- package/dist/workflow/glob.d.ts +3 -0
- package/dist/workflow/glob.js +21 -0
- package/dist/workflow/glob.js.map +1 -0
- package/dist/workflow/index.d.ts +2 -0
- package/dist/workflow/index.js +162 -0
- package/dist/workflow/index.js.map +1 -0
- package/dist/workflow/phases.d.ts +22 -0
- package/dist/workflow/phases.js +50 -0
- package/dist/workflow/phases.js.map +1 -0
- package/dist/workflow/prose.d.ts +2 -0
- package/dist/workflow/prose.js +113 -0
- package/dist/workflow/prose.js.map +1 -0
- package/dist/workflow/red.d.ts +2 -0
- package/dist/workflow/red.js +36 -0
- package/dist/workflow/red.js.map +1 -0
- package/dist/workflow/secrets.d.ts +2 -0
- package/dist/workflow/secrets.js +5 -0
- package/dist/workflow/secrets.js.map +1 -0
- package/dist/workflow/security-line.d.ts +6 -0
- package/dist/workflow/security-line.js +14 -0
- package/dist/workflow/security-line.js.map +1 -0
- package/dist/workflow/sync.d.ts +4 -0
- package/dist/workflow/sync.js +47 -0
- package/dist/workflow/sync.js.map +1 -0
- package/dist/workflow/tests.d.ts +2 -0
- package/dist/workflow/tests.js +6 -0
- package/dist/workflow/tests.js.map +1 -0
- package/package.json +5 -3
- package/templates/repo/base/package.json +13 -2
- package/templates/repo/claude/CLAUDE.md +7 -1
- package/templates/repo/claude/_claude/settings.json +4 -0
- package/templates/workflow/base/_githooks/pre-commit +3 -0
- package/templates/workflow/base/_githooks/pre-push +4 -0
- package/templates/workflow/base/_github/workflows/workflow.yml +45 -0
- package/templates/workflow/base/docs/workflow.md +100 -0
- package/templates/workflow/claude/_claude/hooks/guard-branch.mjs +70 -0
- package/templates/workflow/claude/_claude/skills/architect/SKILL.md +44 -0
- package/templates/workflow/claude/_claude/skills/change-workflow/SKILL.md +70 -0
- package/templates/workflow/claude/_claude/skills/developer/SKILL.md +57 -0
- package/templates/workflow/claude/_claude/skills/requirements-engineer/SKILL.md +59 -0
- package/templates/workflow/claude/_claude/skills/technical-writing/SKILL.md +35 -0
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
import { spawnSync } from 'node:child_process';
|
|
2
|
+
import { readFileSync } from 'node:fs';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { parseArgs } from 'node:util';
|
|
5
|
+
import { checkBranch } from './branch.js';
|
|
6
|
+
import { loadConfig } from './config.js';
|
|
7
|
+
import { checkFeatures, checkSlice } from './features.js';
|
|
8
|
+
import { addedLines, changedSince, commitsSince, currentBranch, git, mergeBase, showFile, stagedFiles, trackedFiles, } from './git.js';
|
|
9
|
+
import { matchesAny } from './glob.js';
|
|
10
|
+
import { checkPhases } from './phases.js';
|
|
11
|
+
import { checkProse } from './prose.js';
|
|
12
|
+
import { featuresFailAt } from './red.js';
|
|
13
|
+
import { credentialPaths } from './secrets.js';
|
|
14
|
+
import { checkSecurityLine } from './security-line.js';
|
|
15
|
+
import { syncWorkflow } from './sync.js';
|
|
16
|
+
import { unlistedTests } from './tests.js';
|
|
17
|
+
export const WORKFLOW_USAGE = `synoikia-plugin workflow <command> (rules: docs/workflow.md)
|
|
18
|
+
|
|
19
|
+
check-branch [name] Branch name (default: the PR head or the current branch)
|
|
20
|
+
check-phases --base <ref> [--red-green] [--backfill]
|
|
21
|
+
One phase per commit. --red-green runs the scenarios at the test: commit
|
|
22
|
+
check-features [--base <ref>] [--backfill]
|
|
23
|
+
Feature files. With --base: the slice size and new test files
|
|
24
|
+
check-prose [--base <ref>] [--pr-body] [files...]
|
|
25
|
+
Writing rules on files, on lines added since <ref>, or on the PR description
|
|
26
|
+
check-security-line --base <ref> A ready PR that changes a security path has a "Security audit:" line
|
|
27
|
+
pre-commit Credential file names, then gitleaks on staged changes when installed
|
|
28
|
+
pre-push [--base <ref>] check-branch, check-features, check-prose and check-phases (default base origin/main)
|
|
29
|
+
hooks install Use .githooks for this clone
|
|
30
|
+
sync [--check] Write the shared workflow files, or list the ones out of date
|
|
31
|
+
`;
|
|
32
|
+
const OPTIONS = {
|
|
33
|
+
base: { type: 'string' },
|
|
34
|
+
'red-green': { type: 'boolean' },
|
|
35
|
+
backfill: { type: 'boolean' },
|
|
36
|
+
'pr-body': { type: 'boolean' },
|
|
37
|
+
check: { type: 'boolean' },
|
|
38
|
+
};
|
|
39
|
+
const report = (title, issues) => {
|
|
40
|
+
for (const issue of issues)
|
|
41
|
+
console.error(`${title}: ${issue}`);
|
|
42
|
+
return issues.length ? 1 : 0;
|
|
43
|
+
};
|
|
44
|
+
const pullRequest = () => {
|
|
45
|
+
const file = process.env.GITHUB_EVENT_PATH;
|
|
46
|
+
if (!file)
|
|
47
|
+
return undefined;
|
|
48
|
+
return JSON.parse(readFileSync(file, 'utf8'))
|
|
49
|
+
.pull_request;
|
|
50
|
+
};
|
|
51
|
+
function branch({ root, config }, name) {
|
|
52
|
+
const branchName = name ?? currentBranch(root);
|
|
53
|
+
if (!branchName)
|
|
54
|
+
return report('check-branch', ['no branch (detached HEAD). Pass the name']);
|
|
55
|
+
return report('check-branch', checkBranch(branchName, config.components, config.ignoreBranches));
|
|
56
|
+
}
|
|
57
|
+
const readFeatures = (root, ref, config) => trackedFiles(root, ref ?? 'HEAD')
|
|
58
|
+
.filter((f) => matchesAny(f, config.features))
|
|
59
|
+
.map((f) => ({ path: f, text: (ref ? showFile(root, ref, f) : readFileSync(path.join(root, f), 'utf8')) ?? '' }));
|
|
60
|
+
function features({ root, config }, base, backfill) {
|
|
61
|
+
const issues = checkFeatures(readFeatures(root, undefined, config));
|
|
62
|
+
if (base) {
|
|
63
|
+
if (!backfill)
|
|
64
|
+
issues.push(...checkSlice(readFeatures(root, mergeBase(root, base), config), readFeatures(root, undefined, config)));
|
|
65
|
+
issues.push(...unlistedTests(changedSince(root, base, 'A'), config.unitTests));
|
|
66
|
+
}
|
|
67
|
+
return report('check-features', issues);
|
|
68
|
+
}
|
|
69
|
+
function prose({ root, config }, base, prBody, files) {
|
|
70
|
+
const issues = [];
|
|
71
|
+
if (files.length)
|
|
72
|
+
for (const f of files)
|
|
73
|
+
issues.push(...checkProse(f, readFileSync(path.join(root, f), 'utf8')));
|
|
74
|
+
else if (base) {
|
|
75
|
+
const changed = changedSince(root, base).filter((f) => matchesAny(f, config.prose));
|
|
76
|
+
const added = addedLines(root, base, changed);
|
|
77
|
+
for (const f of changed)
|
|
78
|
+
issues.push(...checkProse(f, readFileSync(path.join(root, f), 'utf8'), added.get(f) ?? new Set()));
|
|
79
|
+
}
|
|
80
|
+
if (prBody) {
|
|
81
|
+
const pr = pullRequest();
|
|
82
|
+
if (pr?.body)
|
|
83
|
+
issues.push(...checkProse('PR description', pr.body.replace(/\r\n/g, '\n')));
|
|
84
|
+
}
|
|
85
|
+
return report('check-prose', issues);
|
|
86
|
+
}
|
|
87
|
+
function phases({ root, config }, base, redGreen, backfill) {
|
|
88
|
+
const result = checkPhases(commitsSince(root, base), config, { backfill });
|
|
89
|
+
if (result.issues.length || !redGreen || !result.redAt)
|
|
90
|
+
return report('check-phases', result.issues);
|
|
91
|
+
const sha = result.redAt.sha;
|
|
92
|
+
console.log(`check-phases: running ${result.redFeatures.length} feature file(s) at ${sha.slice(0, 7)}, expecting red`);
|
|
93
|
+
if (!featuresFailAt(root, sha, result.redFeatures))
|
|
94
|
+
return report('check-phases', [
|
|
95
|
+
`the scenarios pass at ${sha.slice(0, 7)} ${result.redAt.subject}. Commit the steps before the code, so they fail first`,
|
|
96
|
+
]);
|
|
97
|
+
console.log('check-phases: red at the test: commit. CI runs them green at the head');
|
|
98
|
+
return 0;
|
|
99
|
+
}
|
|
100
|
+
function securityLine({ root, config }, base) {
|
|
101
|
+
const pr = pullRequest();
|
|
102
|
+
if (!pr)
|
|
103
|
+
return report('check-security-line', ['no pull request event (GITHUB_EVENT_PATH)']);
|
|
104
|
+
return report('check-security-line', checkSecurityLine(pr, changedSince(root, base, 'ACMRD'), config.securityPaths));
|
|
105
|
+
}
|
|
106
|
+
function preCommit({ root }) {
|
|
107
|
+
const found = credentialPaths(stagedFiles(root));
|
|
108
|
+
if (found.length)
|
|
109
|
+
return report('pre-commit', found.map((f) => `${f} looks like a credential file. If it is tracked already: git rm --cached ${f}`));
|
|
110
|
+
const leaks = spawnSync('gitleaks', ['git', '--staged', '--no-banner', '--redact'], { cwd: root, stdio: 'inherit' });
|
|
111
|
+
if (leaks.error)
|
|
112
|
+
return 0;
|
|
113
|
+
return leaks.status === 0 ? 0 : report('pre-commit', ['gitleaks found a secret in the staged changes']);
|
|
114
|
+
}
|
|
115
|
+
export async function runWorkflow(argv, root) {
|
|
116
|
+
const [command, ...rest] = argv;
|
|
117
|
+
const { values, positionals } = parseArgs({ args: rest, allowPositionals: true, options: OPTIONS });
|
|
118
|
+
const ctx = { root, config: loadConfig(root) };
|
|
119
|
+
const base = values.base;
|
|
120
|
+
const needBase = () => {
|
|
121
|
+
if (!base)
|
|
122
|
+
throw new Error(`${command} needs --base <ref>`);
|
|
123
|
+
return base;
|
|
124
|
+
};
|
|
125
|
+
switch (command) {
|
|
126
|
+
case 'check-branch':
|
|
127
|
+
return branch(ctx, positionals[0]);
|
|
128
|
+
case 'check-phases':
|
|
129
|
+
return phases(ctx, needBase(), !!values['red-green'], !!values.backfill);
|
|
130
|
+
case 'check-features':
|
|
131
|
+
return features(ctx, base, !!values.backfill);
|
|
132
|
+
case 'check-prose':
|
|
133
|
+
return prose(ctx, base, !!values['pr-body'], positionals);
|
|
134
|
+
case 'check-security-line':
|
|
135
|
+
return securityLine(ctx, needBase());
|
|
136
|
+
case 'pre-commit':
|
|
137
|
+
return preCommit(ctx);
|
|
138
|
+
case 'pre-push': {
|
|
139
|
+
const b = base ?? 'origin/main';
|
|
140
|
+
if (currentBranch(root) === 'main')
|
|
141
|
+
return 0;
|
|
142
|
+
git(root, 'rev-parse', '--verify', '--quiet', b);
|
|
143
|
+
return branch(ctx) | features(ctx, b, false) | prose(ctx, b, false, []) | phases(ctx, b, false, false);
|
|
144
|
+
}
|
|
145
|
+
case 'hooks':
|
|
146
|
+
if (positionals[0] !== 'install')
|
|
147
|
+
break;
|
|
148
|
+
git(root, 'config', 'core.hooksPath', '.githooks');
|
|
149
|
+
console.log('git hooks: .githooks');
|
|
150
|
+
return 0;
|
|
151
|
+
case 'sync': {
|
|
152
|
+
const changed = syncWorkflow(root, !!values.check);
|
|
153
|
+
if (values.check)
|
|
154
|
+
return report('sync', changed.map((f) => `${f} differs from the template. Run the sync`));
|
|
155
|
+
console.log(changed.length ? `wrote ${changed.join(', ')}` : 'workflow files are current');
|
|
156
|
+
return 0;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
console.error(WORKFLOW_USAGE);
|
|
160
|
+
return 2;
|
|
161
|
+
}
|
|
162
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/workflow/index.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { WorkflowConfig } from './config.js';
|
|
2
|
+
export interface Commit {
|
|
3
|
+
sha: string;
|
|
4
|
+
subject: string;
|
|
5
|
+
files: string[];
|
|
6
|
+
}
|
|
7
|
+
export declare const PHASES: readonly ["spec", "arch", "test", "feat", "fix", "refactor", "docs", "chore", "ci"];
|
|
8
|
+
export type Phase = (typeof PHASES)[number];
|
|
9
|
+
export declare const phaseOf: (subject: string) => Phase | undefined;
|
|
10
|
+
type Paths = Pick<WorkflowConfig, 'features' | 'steps' | 'architecture' | 'unitTests' | 'source'>;
|
|
11
|
+
export interface PhaseResult {
|
|
12
|
+
issues: string[];
|
|
13
|
+
/** The last `test:` commit before the first code commit: its new scenarios must fail there. */
|
|
14
|
+
redAt?: Commit;
|
|
15
|
+
/** Feature files that the spec and test commits changed. */
|
|
16
|
+
redFeatures: string[];
|
|
17
|
+
}
|
|
18
|
+
/** Commits of a branch, oldest first: one phase each, and code changes come after a red test commit. */
|
|
19
|
+
export declare function checkPhases(commits: readonly Commit[], paths: Paths, opts?: {
|
|
20
|
+
backfill?: boolean;
|
|
21
|
+
}): PhaseResult;
|
|
22
|
+
export {};
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { matchesAny } from './glob.js';
|
|
2
|
+
export const PHASES = ['spec', 'arch', 'test', 'feat', 'fix', 'refactor', 'docs', 'chore', 'ci'];
|
|
3
|
+
const PREFIX = new RegExp(`^(${PHASES.join('|')})(?:\\([a-z0-9-]+\\))?!?: \\S`);
|
|
4
|
+
export const phaseOf = (subject) => subject.match(PREFIX)?.[1];
|
|
5
|
+
const TESTS = ['**/test/**', '**/*.test.ts', '**/*.spec.ts'];
|
|
6
|
+
/** What each phase may change. */
|
|
7
|
+
function allowed(phase, file, paths) {
|
|
8
|
+
const feature = matchesAny(file, paths.features);
|
|
9
|
+
const model = matchesAny(file, paths.architecture);
|
|
10
|
+
if (phase === 'spec')
|
|
11
|
+
return feature;
|
|
12
|
+
if (phase === 'arch')
|
|
13
|
+
return model;
|
|
14
|
+
if (phase === 'test')
|
|
15
|
+
return matchesAny(file, [...paths.steps, ...TESTS, ...paths.unitTests]);
|
|
16
|
+
return !feature && !model;
|
|
17
|
+
}
|
|
18
|
+
/** Commits of a branch, oldest first: one phase each, and code changes come after a red test commit. */
|
|
19
|
+
export function checkPhases(commits, paths, opts = {}) {
|
|
20
|
+
const issues = [];
|
|
21
|
+
const redFeatures = new Set();
|
|
22
|
+
let lastTest;
|
|
23
|
+
let redAt;
|
|
24
|
+
let needsRed = false;
|
|
25
|
+
for (const c of commits) {
|
|
26
|
+
const short = `${c.sha.slice(0, 7)} ${c.subject}`;
|
|
27
|
+
const phase = phaseOf(c.subject);
|
|
28
|
+
if (!phase) {
|
|
29
|
+
issues.push(`${short}: start the subject with one of ${PHASES.map((p) => `${p}:`).join(' ')}`);
|
|
30
|
+
continue;
|
|
31
|
+
}
|
|
32
|
+
for (const f of c.files)
|
|
33
|
+
if (!allowed(phase, f, paths))
|
|
34
|
+
issues.push(`${short}: ${f} is outside the ${phase} phase`);
|
|
35
|
+
if (phase === 'spec' || phase === 'test')
|
|
36
|
+
for (const f of c.files)
|
|
37
|
+
if (matchesAny(f, paths.features))
|
|
38
|
+
redFeatures.add(f);
|
|
39
|
+
if (phase === 'test')
|
|
40
|
+
lastTest = c;
|
|
41
|
+
if ((phase === 'feat' || phase === 'fix') && c.files.some((f) => matchesAny(f, paths.source)) && !redAt) {
|
|
42
|
+
needsRed = true;
|
|
43
|
+
redAt = lastTest;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
if (needsRed && !opts.backfill && (!redAt || !redFeatures.size))
|
|
47
|
+
issues.push('a feat: or fix: commit changes code with no test: commit before it. Show the scenarios red first');
|
|
48
|
+
return { issues, redAt: opts.backfill ? undefined : redAt, redFeatures: [...redFeatures] };
|
|
49
|
+
}
|
|
50
|
+
//# sourceMappingURL=phases.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"phases.js","sourceRoot":"","sources":["../../src/workflow/phases.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
const MAX_WORDS = 25;
|
|
2
|
+
/** Inline code and link targets are not prose. */
|
|
3
|
+
const stripInline = (text) => text
|
|
4
|
+
.replace(/`[^`]*`/g, 'Code')
|
|
5
|
+
.replace(/\]\([^)]*\)/g, ']')
|
|
6
|
+
.replace(/<!--.*?-->/g, '')
|
|
7
|
+
.replace(/https?:\/\/\S+/g, 'link');
|
|
8
|
+
function markdownBlocks(text) {
|
|
9
|
+
const lines = text.split('\n');
|
|
10
|
+
const blocks = [];
|
|
11
|
+
let fence;
|
|
12
|
+
let comment = false;
|
|
13
|
+
let para;
|
|
14
|
+
const flush = () => {
|
|
15
|
+
if (para)
|
|
16
|
+
blocks.push(para);
|
|
17
|
+
para = undefined;
|
|
18
|
+
};
|
|
19
|
+
let i = 0;
|
|
20
|
+
if (lines[0] === '---') {
|
|
21
|
+
const end = lines.indexOf('---', 1);
|
|
22
|
+
if (end > 0)
|
|
23
|
+
i = end + 1;
|
|
24
|
+
}
|
|
25
|
+
for (; i < lines.length; i++) {
|
|
26
|
+
const raw = lines[i];
|
|
27
|
+
const line = i + 1;
|
|
28
|
+
const fenceMark = raw.match(/^\s*(`{3,}|~{3,})/)?.[1];
|
|
29
|
+
if (fence) {
|
|
30
|
+
if (fenceMark?.startsWith(fence))
|
|
31
|
+
fence = undefined;
|
|
32
|
+
continue;
|
|
33
|
+
}
|
|
34
|
+
if (fenceMark) {
|
|
35
|
+
flush();
|
|
36
|
+
fence = fenceMark;
|
|
37
|
+
continue;
|
|
38
|
+
}
|
|
39
|
+
if (comment || raw.trim().startsWith('<!--')) {
|
|
40
|
+
comment = !raw.includes('-->');
|
|
41
|
+
continue;
|
|
42
|
+
}
|
|
43
|
+
if (!raw.trim() || (/^\s{4,}\S/.test(raw) && !para)) {
|
|
44
|
+
flush();
|
|
45
|
+
continue;
|
|
46
|
+
}
|
|
47
|
+
if (/^\s*\|/.test(raw)) {
|
|
48
|
+
flush();
|
|
49
|
+
if (!/^\s*\|[\s:|-]+\|\s*$/.test(raw))
|
|
50
|
+
for (const cell of raw.split('|').slice(1, -1))
|
|
51
|
+
blocks.push({ line, lastLine: line, text: cell, label: true });
|
|
52
|
+
continue;
|
|
53
|
+
}
|
|
54
|
+
if (/^\s*#{1,6}\s/.test(raw)) {
|
|
55
|
+
flush();
|
|
56
|
+
blocks.push({ line, lastLine: line, text: raw.replace(/^\s*#+\s*/, ''), label: true });
|
|
57
|
+
continue;
|
|
58
|
+
}
|
|
59
|
+
const item = raw.match(/^\s*(?:[-*+]|\d+[.)])\s+(.*)$/);
|
|
60
|
+
if (item) {
|
|
61
|
+
flush();
|
|
62
|
+
para = { line, lastLine: line, text: item[1], label: false };
|
|
63
|
+
continue;
|
|
64
|
+
}
|
|
65
|
+
if (para) {
|
|
66
|
+
para.text += ` ${raw.trim()}`;
|
|
67
|
+
para.lastLine = line;
|
|
68
|
+
}
|
|
69
|
+
else
|
|
70
|
+
para = { line, lastLine: line, text: raw.trim().replace(/^>\s*/, ''), label: false };
|
|
71
|
+
}
|
|
72
|
+
flush();
|
|
73
|
+
return blocks;
|
|
74
|
+
}
|
|
75
|
+
function featureBlocks(text) {
|
|
76
|
+
return text.split('\n').flatMap((raw, i) => {
|
|
77
|
+
const t = raw.trim();
|
|
78
|
+
if (!t || t.startsWith('#') || t.startsWith('@'))
|
|
79
|
+
return [];
|
|
80
|
+
const label = t.startsWith('|') || /^[A-Za-z ]+:/.test(t);
|
|
81
|
+
return [{ line: i + 1, lastLine: i + 1, text: t.replace(/^[A-Za-z ]+:\s*/, ''), label }];
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
const words = (sentence) => sentence.split(/\s+/).filter((w) => /[\p{L}\p{N}]/u.test(w)).length;
|
|
85
|
+
/** Problems with the writing rules in one file. With `lines`, only blocks that touch those lines. */
|
|
86
|
+
export function checkProse(file, text, lines) {
|
|
87
|
+
const blocks = file.endsWith('.feature') ? featureBlocks(text) : markdownBlocks(text);
|
|
88
|
+
const issues = [];
|
|
89
|
+
for (const block of blocks) {
|
|
90
|
+
if (lines) {
|
|
91
|
+
let touched = false;
|
|
92
|
+
for (let l = block.line; l <= block.lastLine && !touched; l++)
|
|
93
|
+
touched = lines.has(l);
|
|
94
|
+
if (!touched)
|
|
95
|
+
continue;
|
|
96
|
+
}
|
|
97
|
+
const prose = stripInline(block.text);
|
|
98
|
+
const at = `${file}:${block.line}`;
|
|
99
|
+
if (prose.includes(';'))
|
|
100
|
+
issues.push(`${at}: no semicolons. Write two sentences`);
|
|
101
|
+
if (prose.includes('—'))
|
|
102
|
+
issues.push(`${at}: no em dashes. Write two sentences or use a colon`);
|
|
103
|
+
if (block.label)
|
|
104
|
+
continue;
|
|
105
|
+
for (const sentence of prose.split(/(?<=[.!?:][*_)"'`]*)\s+(?=[\p{Lu}`*"([])/u)) {
|
|
106
|
+
const n = words(sentence);
|
|
107
|
+
if (n > MAX_WORDS)
|
|
108
|
+
issues.push(`${at}: a sentence has ${n} words. Use ${MAX_WORDS} at most`);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
return issues;
|
|
112
|
+
}
|
|
113
|
+
//# sourceMappingURL=prose.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"prose.js","sourceRoot":"","sources":["../../src/workflow/prose.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { spawnSync } from 'node:child_process';
|
|
2
|
+
import { existsSync, mkdtempSync, rmSync } from 'node:fs';
|
|
3
|
+
import { tmpdir } from 'node:os';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
import { git } from './git.js';
|
|
6
|
+
const run = (cwd, cmd, args) => spawnSync(cmd, args, { cwd, stdio: 'inherit', env: { ...process.env, CI: 'true' } }).status ?? 1;
|
|
7
|
+
/** The nearest directory up from `file` with a package.json, below `root`. */
|
|
8
|
+
function packageOf(root, file) {
|
|
9
|
+
let dir = path.dirname(path.join(root, file));
|
|
10
|
+
while (dir !== root && !existsSync(path.join(dir, 'package.json')))
|
|
11
|
+
dir = path.dirname(dir);
|
|
12
|
+
return dir;
|
|
13
|
+
}
|
|
14
|
+
/** Runs the feature files at `sha` in a temporary worktree. True when at least one package fails. */
|
|
15
|
+
export function featuresFailAt(root, sha, features) {
|
|
16
|
+
const dir = mkdtempSync(path.join(tmpdir(), 'synoikia-red-'));
|
|
17
|
+
try {
|
|
18
|
+
git(root, 'worktree', 'add', '--detach', dir, sha);
|
|
19
|
+
if (run(dir, 'pnpm', ['install', '--frozen-lockfile', '--prefer-offline']) !== 0)
|
|
20
|
+
throw new Error(`pnpm install failed at ${sha.slice(0, 7)}`);
|
|
21
|
+
const byPackage = new Map();
|
|
22
|
+
for (const f of features.filter((f) => existsSync(path.join(dir, f)))) {
|
|
23
|
+
const pkg = packageOf(dir, f);
|
|
24
|
+
byPackage.set(pkg, [...(byPackage.get(pkg) ?? []), path.relative(pkg, path.join(dir, f))]);
|
|
25
|
+
}
|
|
26
|
+
let failed = false;
|
|
27
|
+
for (const [pkg, files] of byPackage)
|
|
28
|
+
failed = run(pkg, 'pnpm', ['exec', 'vitest', 'run', ...files]) !== 0 || failed;
|
|
29
|
+
return failed;
|
|
30
|
+
}
|
|
31
|
+
finally {
|
|
32
|
+
spawnSync('git', ['worktree', 'remove', '--force', dir], { cwd: root });
|
|
33
|
+
rmSync(dir, { recursive: true, force: true });
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
//# sourceMappingURL=red.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"red.js","sourceRoot":"","sources":["../../src/workflow/red.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,oBAAoB,CAAC;AAC/C,OAAO,EAAE,UAAU,EAAE,WAAW,EAAE,MAAM,EAAE,MAAM,SAAS,CAAC;AAC1D,OAAO,EAAE,MAAM,EAAE,MAAM,SAAS,CAAC;AACjC,OAAO,IAAI,MAAM,WAAW,CAAC;AAC7B,OAAO,EAAE,GAAG,EAAE,MAAM,UAAU,CAAC;AAE/B,MAAM,GAAG,GAAG,CAAC,GAAW,EAAE,GAAW,EAAE,IAAc,EAAE,EAAE,CACvD,SAAS,CAAC,GAAG,EAAE,IAAI,EAAE,EAAE,GAAG,EAAE,KAAK,EAAE,SAAS,EAAE,GAAG,EAAE,EAAE,GAAG,OAAO,CAAC,GAAG,EAAE,EAAE,EAAE,MAAM,EAAE,EAAE,CAAC,CAAC,MAAM,IAAI,CAAC,CAAC;AAEnG,8EAA8E;AAC9E,SAAS,SAAS,CAAC,IAAY,EAAE,IAAY;IAC3C,IAAI,GAAG,GAAG,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,EAAE,IAAI,CAAC,CAAC,CAAC;IAC9C,OAAO,GAAG,KAAK,IAAI,IAAI,CAAC,UAAU,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,EAAE,cAAc,CAAC,CAAC;QAAE,GAAG,GAAG,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC;IAC5F,OAAO,GAAG,CAAC;AACb,CAAC;AAED,qGAAqG;AACrG,MAAM,UAAU,cAAc,CAAC,IAAY,EAAE,GAAW,EAAE,QAA2B;IACnF,MAAM,GAAG,GAAG,WAAW,CAAC,IAAI,CAAC,IAAI,CAAC,MAAM,EAAE,EAAE,eAAe,CAAC,CAAC,CAAC;IAC9D,IAAI,CAAC;QACH,GAAG,CAAC,IAAI,EAAE,UAAU,EAAE,KAAK,EAAE,UAAU,EAAE,GAAG,EAAE,GAAG,CAAC,CAAC;QACnD,IAAI,GAAG,CAAC,GAAG,EAAE,MAAM,EAAE,CAAC,SAAS,EAAE,mBAAmB,EAAE,kBAAkB,CAAC,CAAC,KAAK,CAAC;YAC9E,MAAM,IAAI,KAAK,CAAC,0BAA0B,GAAG,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,EAAE,CAAC,CAAC;QAC/D,MAAM,SAAS,GAAG,IAAI,GAAG,EAAoB,CAAC;QAC9C,KAAK,MAAM,CAAC,IAAI,QAAQ,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,UAAU,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,EAAE,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC;YACtE,MAAM,GAAG,GAAG,SAAS,CAAC,GAAG,EAAE,CAAC,CAAC,CAAC;YAC9B,SAAS,CAAC,GAAG,CAAC,GAAG,EAAE,CAAC,GAAG,CAAC,SAAS,CAAC,GAAG,CAAC,GAAG,CAAC,IAAI,EAAE,CAAC,EAAE,IAAI,CAAC,QAAQ,CAAC,GAAG,EAAE,IAAI,CAAC,IAAI,CAAC,GAAG,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;QAC7F,CAAC;QACD,IAAI,MAAM,GAAG,KAAK,CAAC;QACnB,KAAK,MAAM,CAAC,GAAG,EAAE,KAAK,CAAC,IAAI,SAAS;YAClC,MAAM,GAAG,GAAG,CAAC,GAAG,EAAE,MAAM,EAAE,CAAC,MAAM,EAAE,QAAQ,EAAE,KAAK,EAAE,GAAG,KAAK,CAAC,CAAC,KAAK,CAAC,IAAI,MAAM,CAAC;QACjF,OAAO,MAAM,CAAC;IAChB,CAAC;YAAS,CAAC;QACT,SAAS,CAAC,KAAK,EAAE,CAAC,UAAU,EAAE,QAAQ,EAAE,SAAS,EAAE,GAAG,CAAC,EAAE,EAAE,GAAG,EAAE,IAAI,EAAE,CAAC,CAAC;QACxE,MAAM,CAAC,GAAG,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC,CAAC;IAChD,CAAC;AACH,CAAC"}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
const EXAMPLE = /(?:\.|^|\/)(?:example|sample|template)(?:\.[^/]+)?$/;
|
|
2
|
+
const CREDENTIAL = /(?:^|\/)(?:secrets?(?:\.[^/]+)*\.ya?ml|\.env(?:\..+)?|\.dev\.vars(?:\..+)?|[^/]+\.secret(?:\..+)?|[^/]+\.(?:pem|key|p12|pfx)|id_(?:rsa|dsa|ecdsa|ed25519)|\.npmrc|\.netrc)$/;
|
|
3
|
+
/** Staged paths that look like credential files, by name rather than content. */
|
|
4
|
+
export const credentialPaths = (files) => files.filter((f) => !EXAMPLE.test(f) && CREDENTIAL.test(f));
|
|
5
|
+
//# sourceMappingURL=secrets.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"secrets.js","sourceRoot":"","sources":["../../src/workflow/secrets.ts"],"names":[],"mappings":"AAAA,MAAM,OAAO,GAAG,qDAAqD,CAAC;AACtE,MAAM,UAAU,GACd,6KAA6K,CAAC;AAEhL,iFAAiF;AACjF,MAAM,CAAC,MAAM,eAAe,GAAG,CAAC,KAAwB,EAAE,EAAE,CAC1D,KAAK,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,UAAU,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC"}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export declare const SECURITY_LINE: RegExp;
|
|
2
|
+
/** A ready PR that changes a security path states its audit on a line of its own. */
|
|
3
|
+
export declare function checkSecurityLine(pr: {
|
|
4
|
+
draft: boolean;
|
|
5
|
+
body: string | null;
|
|
6
|
+
}, changed: readonly string[], paths: readonly string[]): string[];
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { matchesAny } from './glob.js';
|
|
2
|
+
export const SECURITY_LINE = /^Security audit: \S/m;
|
|
3
|
+
/** A ready PR that changes a security path states its audit on a line of its own. */
|
|
4
|
+
export function checkSecurityLine(pr, changed, paths) {
|
|
5
|
+
if (pr.draft)
|
|
6
|
+
return [];
|
|
7
|
+
const touched = changed.filter((f) => matchesAny(f, paths));
|
|
8
|
+
if (!touched.length || SECURITY_LINE.test(pr.body ?? ''))
|
|
9
|
+
return [];
|
|
10
|
+
return [
|
|
11
|
+
`this PR changes security paths (${touched.slice(0, 5).join(', ')}${touched.length > 5 ? ', …' : ''}). Add a line "Security audit: <boundary and findings>" or "Security audit: none. <why>" to its description`,
|
|
12
|
+
];
|
|
13
|
+
}
|
|
14
|
+
//# sourceMappingURL=security-line.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"security-line.js","sourceRoot":"","sources":["../../src/workflow/security-line.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,WAAW,CAAC;AAEvC,MAAM,CAAC,MAAM,aAAa,GAAG,sBAAsB,CAAC;AAEpD,qFAAqF;AACrF,MAAM,UAAU,iBAAiB,CAC/B,EAA2C,EAC3C,OAA0B,EAC1B,KAAwB;IAExB,IAAI,EAAE,CAAC,KAAK;QAAE,OAAO,EAAE,CAAC;IACxB,MAAM,OAAO,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,UAAU,CAAC,CAAC,EAAE,KAAK,CAAC,CAAC,CAAC;IAC5D,IAAI,CAAC,OAAO,CAAC,MAAM,IAAI,aAAa,CAAC,IAAI,CAAC,EAAE,CAAC,IAAI,IAAI,EAAE,CAAC;QAAE,OAAO,EAAE,CAAC;IACpE,OAAO;QACL,mCAAmC,OAAO,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,OAAO,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,6GAA6G;KACjN,CAAC;AACJ,CAAC"}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
/** The shared workflow files, as repo-relative path → template path. Claude files only for repos that use Claude. */
|
|
2
|
+
export declare function workflowFiles(root: string): Map<string, string>;
|
|
3
|
+
/** Writes the shared files, or with `check` lists the ones that differ. Returns the paths written or out of date. */
|
|
4
|
+
export declare function syncWorkflow(root: string, check?: boolean): string[];
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { chmodSync, existsSync, lstatSync, mkdirSync, readdirSync, readFileSync, realpathSync, statSync, writeFileSync, } from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { TEMPLATES_DIR } from '../repo-root.js';
|
|
4
|
+
const WORKFLOW_DIR = path.join(TEMPLATES_DIR, 'workflow');
|
|
5
|
+
const outName = (name) => (/^_[a-zA-Z]/.test(name) ? `.${name.slice(1)}` : name);
|
|
6
|
+
/** The shared workflow files, as repo-relative path → template path. Claude files only for repos that use Claude. */
|
|
7
|
+
export function workflowFiles(root) {
|
|
8
|
+
const files = new Map();
|
|
9
|
+
const walk = (from, to) => {
|
|
10
|
+
for (const entry of readdirSync(from).sort()) {
|
|
11
|
+
const src = path.join(from, entry);
|
|
12
|
+
const dest = path.posix.join(to, outName(entry));
|
|
13
|
+
if (statSync(src).isDirectory())
|
|
14
|
+
walk(src, dest);
|
|
15
|
+
else
|
|
16
|
+
files.set(dest, src);
|
|
17
|
+
}
|
|
18
|
+
};
|
|
19
|
+
walk(path.join(WORKFLOW_DIR, 'base'), '');
|
|
20
|
+
if (existsSync(path.join(root, '.claude')))
|
|
21
|
+
walk(path.join(WORKFLOW_DIR, 'claude'), '');
|
|
22
|
+
return files;
|
|
23
|
+
}
|
|
24
|
+
/** Writes the shared files, or with `check` lists the ones that differ. Returns the paths written or out of date. */
|
|
25
|
+
export function syncWorkflow(root, check = false) {
|
|
26
|
+
const changed = [];
|
|
27
|
+
for (const [rel, src] of workflowFiles(root)) {
|
|
28
|
+
const dest = path.join(root, rel);
|
|
29
|
+
if (lstatSync(dest, { throwIfNoEntry: false })?.isSymbolicLink())
|
|
30
|
+
throw new Error(`${rel} is a symlink. Remove it before the sync`);
|
|
31
|
+
const text = readFileSync(src, 'utf8');
|
|
32
|
+
if (existsSync(dest) && readFileSync(dest, 'utf8') === text)
|
|
33
|
+
continue;
|
|
34
|
+
changed.push(rel);
|
|
35
|
+
if (check)
|
|
36
|
+
continue;
|
|
37
|
+
mkdirSync(path.dirname(dest), { recursive: true });
|
|
38
|
+
const real = realpathSync(path.dirname(dest));
|
|
39
|
+
if (path.relative(realpathSync(root), real).startsWith('..'))
|
|
40
|
+
throw new Error(`${rel} resolves outside the repository`);
|
|
41
|
+
writeFileSync(dest, text);
|
|
42
|
+
if (rel.startsWith('.githooks/'))
|
|
43
|
+
chmodSync(dest, 0o755);
|
|
44
|
+
}
|
|
45
|
+
return changed;
|
|
46
|
+
}
|
|
47
|
+
//# sourceMappingURL=sync.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sync.js","sourceRoot":"","sources":["../../src/workflow/sync.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { matchesAny } from './glob.js';
|
|
2
|
+
/** New test files must be pure-function unit tests. Behavior is specified in feature files. */
|
|
3
|
+
export const unlistedTests = (added, unitTests) => added
|
|
4
|
+
.filter((f) => /\.(?:test|spec)\.[cm]?[jt]sx?$/.test(f) && !matchesAny(f, unitTests))
|
|
5
|
+
.map((f) => `${f}: a new test file must be a pure-function unit test (${unitTests.join(', ')}). Specify behavior in a feature file`);
|
|
6
|
+
//# sourceMappingURL=tests.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"tests.js","sourceRoot":"","sources":["../../src/workflow/tests.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,WAAW,CAAC;AAEvC,+FAA+F;AAC/F,MAAM,CAAC,MAAM,aAAa,GAAG,CAAC,KAAwB,EAAE,SAA4B,EAAE,EAAE,CACtF,KAAK;KACF,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,gCAAgC,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,UAAU,CAAC,CAAC,EAAE,SAAS,CAAC,CAAC;KACpF,GAAG,CACF,CAAC,CAAC,EAAE,EAAE,CACJ,GAAG,CAAC,wDAAwD,SAAS,CAAC,IAAI,CAAC,IAAI,CAAC,uCAAuC,CAC1H,CAAC"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@synoikia/create-plugin",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"description": "Create Synoikia plugin repositories and plugins, then build, check and publish them",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -31,12 +31,14 @@
|
|
|
31
31
|
],
|
|
32
32
|
"dependencies": {
|
|
33
33
|
"@clack/prompts": "^1.8.1",
|
|
34
|
+
"@cucumber/gherkin": "^42.0.1",
|
|
35
|
+
"@cucumber/messages": "^34.2.1",
|
|
34
36
|
"esbuild": "^0.28.2",
|
|
35
37
|
"yaml": "^2.9.1",
|
|
36
38
|
"@synoikia/plugin-sdk": "^0.5.0"
|
|
37
39
|
},
|
|
38
40
|
"peerDependencies": {
|
|
39
|
-
"@synoikia/core": "^0.
|
|
41
|
+
"@synoikia/core": "^0.15.0"
|
|
40
42
|
},
|
|
41
43
|
"peerDependenciesMeta": {
|
|
42
44
|
"@synoikia/core": {
|
|
@@ -46,7 +48,7 @@
|
|
|
46
48
|
"devDependencies": {
|
|
47
49
|
"@types/ws": "^8.18.1",
|
|
48
50
|
"ws": "^8.22.0",
|
|
49
|
-
"@synoikia/core": "^0.
|
|
51
|
+
"@synoikia/core": "^0.15.0"
|
|
50
52
|
},
|
|
51
53
|
"scripts": {
|
|
52
54
|
"build": "tsc -p tsconfig.build.json",
|
|
@@ -14,11 +14,22 @@
|
|
|
14
14
|
"test": "pnpm -r --filter './plugins/*' test",
|
|
15
15
|
"lint": "eslint .",
|
|
16
16
|
"format": "prettier --write .",
|
|
17
|
-
"format:check": "prettier --check ."
|
|
17
|
+
"format:check": "prettier --check .",
|
|
18
|
+
"workflow": "synoikia-plugin workflow",
|
|
19
|
+
"hooks:install": "synoikia-plugin workflow hooks install"
|
|
18
20
|
},
|
|
19
21
|
"synoikia": {
|
|
20
22
|
"repository": "{{repository}}",
|
|
21
|
-
"name": "{{repoName}}"
|
|
23
|
+
"name": "{{repoName}}",
|
|
24
|
+
"workflow": {
|
|
25
|
+
"securityPaths": [
|
|
26
|
+
"plugins/*/manifest.json",
|
|
27
|
+
"plugins/*/plugin.yaml",
|
|
28
|
+
"plugins/*/src/**",
|
|
29
|
+
"**/package.json",
|
|
30
|
+
".github/workflows/**"
|
|
31
|
+
]
|
|
32
|
+
}
|
|
22
33
|
},
|
|
23
34
|
"devDependencies": {
|
|
24
35
|
"@eslint/js": "^10.0.1",
|