@synoikia/create-plugin 0.3.1 → 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.
Files changed (58) hide show
  1. package/README.md +15 -0
  2. package/dist/cli.js +4 -0
  3. package/dist/cli.js.map +1 -1
  4. package/dist/scaffold.js +6 -1
  5. package/dist/scaffold.js.map +1 -1
  6. package/dist/workflow/branch.d.ts +2 -0
  7. package/dist/workflow/branch.js +31 -0
  8. package/dist/workflow/branch.js.map +1 -0
  9. package/dist/workflow/config.d.ts +24 -0
  10. package/dist/workflow/config.js +25 -0
  11. package/dist/workflow/config.js.map +1 -0
  12. package/dist/workflow/features.d.ts +20 -0
  13. package/dist/workflow/features.js +165 -0
  14. package/dist/workflow/features.js.map +1 -0
  15. package/dist/workflow/git.d.ts +12 -0
  16. package/dist/workflow/git.js +42 -0
  17. package/dist/workflow/git.js.map +1 -0
  18. package/dist/workflow/glob.d.ts +3 -0
  19. package/dist/workflow/glob.js +21 -0
  20. package/dist/workflow/glob.js.map +1 -0
  21. package/dist/workflow/index.d.ts +2 -0
  22. package/dist/workflow/index.js +162 -0
  23. package/dist/workflow/index.js.map +1 -0
  24. package/dist/workflow/phases.d.ts +22 -0
  25. package/dist/workflow/phases.js +50 -0
  26. package/dist/workflow/phases.js.map +1 -0
  27. package/dist/workflow/prose.d.ts +2 -0
  28. package/dist/workflow/prose.js +113 -0
  29. package/dist/workflow/prose.js.map +1 -0
  30. package/dist/workflow/red.d.ts +2 -0
  31. package/dist/workflow/red.js +36 -0
  32. package/dist/workflow/red.js.map +1 -0
  33. package/dist/workflow/secrets.d.ts +2 -0
  34. package/dist/workflow/secrets.js +5 -0
  35. package/dist/workflow/secrets.js.map +1 -0
  36. package/dist/workflow/security-line.d.ts +6 -0
  37. package/dist/workflow/security-line.js +14 -0
  38. package/dist/workflow/security-line.js.map +1 -0
  39. package/dist/workflow/sync.d.ts +4 -0
  40. package/dist/workflow/sync.js +47 -0
  41. package/dist/workflow/sync.js.map +1 -0
  42. package/dist/workflow/tests.d.ts +2 -0
  43. package/dist/workflow/tests.js +6 -0
  44. package/dist/workflow/tests.js.map +1 -0
  45. package/package.json +3 -1
  46. package/templates/repo/base/package.json +13 -2
  47. package/templates/repo/claude/CLAUDE.md +7 -1
  48. package/templates/repo/claude/_claude/settings.json +4 -0
  49. package/templates/workflow/base/_githooks/pre-commit +3 -0
  50. package/templates/workflow/base/_githooks/pre-push +4 -0
  51. package/templates/workflow/base/_github/workflows/workflow.yml +45 -0
  52. package/templates/workflow/base/docs/workflow.md +100 -0
  53. package/templates/workflow/claude/_claude/hooks/guard-branch.mjs +70 -0
  54. package/templates/workflow/claude/_claude/skills/architect/SKILL.md +44 -0
  55. package/templates/workflow/claude/_claude/skills/change-workflow/SKILL.md +70 -0
  56. package/templates/workflow/claude/_claude/skills/developer/SKILL.md +57 -0
  57. package/templates/workflow/claude/_claude/skills/requirements-engineer/SKILL.md +59 -0
  58. 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,2 @@
1
+ /** Problems with the writing rules in one file. With `lines`, only blocks that touch those lines. */
2
+ export declare function checkProse(file: string, text: string, lines?: ReadonlySet<number>): string[];
@@ -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,2 @@
1
+ /** Runs the feature files at `sha` in a temporary worktree. True when at least one package fails. */
2
+ export declare function featuresFailAt(root: string, sha: string, features: readonly string[]): boolean;
@@ -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":"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"}
@@ -0,0 +1,2 @@
1
+ /** Staged paths that look like credential files, by name rather than content. */
2
+ export declare const credentialPaths: (files: readonly string[]) => string[];
@@ -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,2 @@
1
+ /** New test files must be pure-function unit tests. Behavior is specified in feature files. */
2
+ export declare const unlistedTests: (added: readonly string[], unitTests: readonly string[]) => string[];
@@ -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.1",
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,6 +31,8 @@
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"
@@ -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",
@@ -15,6 +15,10 @@ Plugins for [Synoikia](https://github.com/via-justa/synoikia-core). Authoring gu
15
15
 
16
16
  Policy data (locks, splits, sensitive params and results, confirm literals, guidance) goes in `plugin.yaml`; only decisions that need logic go in `src/`. `plugin.yaml` is validated and inlined at build time, so rebuild before e2e tests.
17
17
 
18
+ ## Workflow
19
+
20
+ Every change is built spec first and test first, one slice at a time. The rules are in `docs/workflow.md`. Start every task that changes the repo with the `change-workflow` skill. Write prose by the `technical-writing` skill.
21
+
18
22
  ## Commands
19
23
 
20
24
  Node 22.12+, pnpm 10.
@@ -24,6 +28,8 @@ pnpm new # add a plugin (see the add-plugin sk
24
28
  pnpm lint && pnpm format:check && pnpm typecheck && pnpm test # what CI runs
25
29
  pnpm --filter ./plugins/<id> test # one plugin: check, build, unit + e2e
26
30
  pnpm check # manifests, plugin.yaml and versions
31
+ pnpm hooks:install # once per clone: the workflow's git hooks
32
+ pnpm workflow pre-push # the workflow checks CI runs
27
33
  ```
28
34
 
29
35
  ## Rules that break releases or installs
@@ -35,4 +41,4 @@ pnpm check # manifests, plugin.yaml and versions
35
41
 
36
42
  ## Security review
37
43
 
38
- Before opening a PR that touches a plugin's `manifest.json`, `plugin.yaml`, `src/` or dependencies, or `.github/workflows/`, run the `security-reviewer` subagent on the diff and address its findings.
44
+ Run the `security-reviewer` subagent on the diff before a PR that touches these paths: a plugin's `manifest.json`, `plugin.yaml`, `src/` or dependencies, and `.github/workflows/`. Address its findings. Put its `Security audit:` line in the PR description.