@kiroku-solutions/kiroku-ai 0.1.2

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.
@@ -0,0 +1,141 @@
1
+ /**
2
+ * Compatibility matrix resolver.
3
+ *
4
+ * Translates the answers collected by the interactive prompts into a concrete
5
+ * set of skill *selectors* (paths relative to `skills/` in the SSOT repo). A
6
+ * selector can be a directory ("frontend/astro") or a single file
7
+ * ("infra-and-db/supabase.md").
8
+ *
9
+ * The matching against the real repository tree happens later in `cli.mjs`;
10
+ * here we only express *intent*. This keeps the matrix declarative and testable.
11
+ */
12
+
13
+ /**
14
+ * @typedef {Object} Stack
15
+ * @property {'fullstack'|'frontend'|'backend'|'infra'} scope
16
+ * @property {('nextjs'|'astro'|'sveltekit')=} frontend
17
+ * @property {string[]=} astroIntegrations e.g. ['react','tailwind']
18
+ * @property {('python-fastapi'|'dotnet'|'java'|'none')=} backend
19
+ * @property {string[]=} databases e.g. ['supabase','redis']
20
+ * @property {('vercel'|'cloudflare'|'docker')=} deploy
21
+ * @property {string[]=} devops e.g. ['github-actions','rabbitmq']
22
+ */
23
+
24
+ /**
25
+ * @param {Stack} stack
26
+ * @returns {string[]} sorted, de-duplicated skill selectors relative to `skills/`
27
+ */
28
+ export function resolveSkillSelectors(stack) {
29
+ const selectors = new Set();
30
+
31
+ // Organization-wide rules are always included.
32
+ selectors.add('global');
33
+
34
+ const hasFrontend = stack.scope === 'fullstack' || stack.scope === 'frontend';
35
+ const hasBackend = stack.scope === 'fullstack' || stack.scope === 'backend';
36
+
37
+ // --- Frontend -----------------------------------------------------------
38
+ if (hasFrontend && stack.frontend) {
39
+ selectors.add('frontend/shared');
40
+ selectors.add(`frontend/${stack.frontend}`);
41
+
42
+ // Astro is a meta-framework: pull the UI integration rules it can host.
43
+ if (stack.frontend === 'astro') {
44
+ const integrations = stack.astroIntegrations ?? [];
45
+ if (integrations.includes('react')) selectors.add('frontend/react');
46
+ if (integrations.includes('svelte')) selectors.add('frontend/sveltekit');
47
+ // 'tailwind' is covered by frontend/shared/tailwind-rules.md (already in).
48
+ }
49
+ }
50
+
51
+ // --- Backend ------------------------------------------------------------
52
+ if (hasBackend && stack.backend) {
53
+ if (stack.backend === 'none') {
54
+ // "None (Supabase BaaS)" — no server framework, but Supabase rules apply.
55
+ selectors.add('infra-and-db/supabase.md');
56
+ } else {
57
+ selectors.add('backend/shared');
58
+ selectors.add(`backend/${stack.backend}`);
59
+ }
60
+ }
61
+
62
+ // --- Databases ----------------------------------------------------------
63
+ for (const db of stack.databases ?? []) {
64
+ selectors.add('infra-and-db/databases.md');
65
+ if (db === 'supabase') selectors.add('infra-and-db/supabase.md');
66
+ }
67
+
68
+ // --- Deployment ---------------------------------------------------------
69
+ switch (stack.deploy) {
70
+ case 'vercel':
71
+ selectors.add('infra-and-db/deploy-vercel.md');
72
+ break;
73
+ case 'cloudflare':
74
+ selectors.add('infra-and-db/deploy-cloudflare.md');
75
+ break;
76
+ case 'docker':
77
+ selectors.add('backend/shared/docker-containers.md');
78
+ break;
79
+ default:
80
+ break;
81
+ }
82
+
83
+ // --- DevOps & Queues ----------------------------------------------------
84
+ for (const tool of stack.devops ?? []) {
85
+ if (tool === 'rabbitmq') {
86
+ selectors.add('backend/shared/message-brokers.md');
87
+ } else {
88
+ // github-actions, n8n
89
+ selectors.add('infra-and-db/cicd-n8n-actions.md');
90
+ }
91
+ }
92
+
93
+ return [...selectors].sort();
94
+ }
95
+
96
+ /**
97
+ * Expand selectors against the real repository tree.
98
+ *
99
+ * @param {string[]} selectors paths relative to `skills/`
100
+ * @param {Array<{path:string,type:string}>} tree full recursive tree
101
+ * @param {string} skillsRoot e.g. "skills"
102
+ * @returns {string[]} repo-relative blob paths to download (e.g. "skills/global/01-..md")
103
+ */
104
+ export function expandSelectors(selectors, tree, skillsRoot) {
105
+ const blobs = tree.filter((n) => n.type === 'blob' && n.path.startsWith(`${skillsRoot}/`));
106
+ const wanted = new Set();
107
+
108
+ for (const sel of selectors) {
109
+ const full = `${skillsRoot}/${sel}`;
110
+ for (const blob of blobs) {
111
+ const isExactFile = blob.path === full;
112
+ const isInsideDir = blob.path.startsWith(`${full}/`);
113
+ if (isExactFile || isInsideDir) {
114
+ // Ignore placeholder files so empty skill folders don't pollute installs.
115
+ if (blob.path.endsWith('/.gitkeep')) continue;
116
+ wanted.add(blob.path);
117
+ }
118
+ }
119
+ }
120
+
121
+ return [...wanted].sort();
122
+ }
123
+
124
+ /**
125
+ * Collect agent persona blobs (the `agents/` folder). Agents are optional and
126
+ * orthogonal to the stack — included wholesale when the user opts in.
127
+ *
128
+ * @param {Array<{path:string,type:string}>} tree
129
+ * @returns {string[]} repo-relative blob paths under `agents/`
130
+ */
131
+ export function collectAgentBlobs(tree) {
132
+ return tree
133
+ .filter(
134
+ (n) =>
135
+ n.type === 'blob' &&
136
+ n.path.startsWith('agents/') &&
137
+ !n.path.endsWith('/.gitkeep'),
138
+ )
139
+ .map((n) => n.path)
140
+ .sort();
141
+ }
@@ -0,0 +1,130 @@
1
+ /**
2
+ * Filesystem scaffolding for the consumer project.
3
+ *
4
+ * Responsibilities:
5
+ * - Materialize downloaded skill files under `.ai-skills/`, stripping the
6
+ * remote `skills/` prefix so the local tree is clean.
7
+ * - Create the governance folders `proposals/` and `local/` with READMEs.
8
+ * - Inject the two-way sync GitHub Action.
9
+ */
10
+
11
+ import { mkdir, writeFile, readFile, unlink } from 'node:fs/promises';
12
+ import { dirname, join, resolve } from 'node:path';
13
+ import { fileURLToPath } from 'node:url';
14
+ import { LOCAL_DIR, WORKFLOW_PATH, ORG, REPO, REF, REVIEW_TEAM } from './config.mjs';
15
+
16
+ const __dirname = dirname(fileURLToPath(import.meta.url));
17
+ const TEMPLATES_DIR = resolve(__dirname, '..', 'templates');
18
+
19
+ /** Strip the remote `skills/` prefix to produce a local relative path. */
20
+ function toLocalPath(repoPath) {
21
+ return repoPath.replace(/^skills\//, '');
22
+ }
23
+
24
+ async function writeFileEnsured(filePath, content) {
25
+ await mkdir(dirname(filePath), { recursive: true });
26
+ await writeFile(filePath, content, 'utf8');
27
+ }
28
+
29
+ /**
30
+ * Write all downloaded skills into `.ai-skills/`.
31
+ * @param {string} targetDir
32
+ * @param {Array<{ path: string, content: string }>} files
33
+ */
34
+ export async function writeSkills(targetDir, files) {
35
+ const base = join(targetDir, LOCAL_DIR);
36
+ for (const file of files) {
37
+ await writeFileEnsured(join(base, toLocalPath(file.path)), file.content);
38
+ }
39
+ }
40
+
41
+ /**
42
+ * Delete skill files that are no longer part of the resolved set (used by
43
+ * `update`). Paths are local-relative (skills/ prefix already stripped). Only
44
+ * touches the managed skill tree — never `proposals/` or `local/`.
45
+ * @returns {Promise<number>} count of files removed
46
+ */
47
+ export async function pruneStaleSkills(targetDir, staleLocalPaths) {
48
+ const base = join(targetDir, LOCAL_DIR);
49
+ const protectedRoots = ['proposals/', 'local/'];
50
+ let removed = 0;
51
+ for (const rel of staleLocalPaths) {
52
+ if (protectedRoots.some((p) => rel.startsWith(p)) || rel === 'lockfile.json') continue;
53
+ try {
54
+ await unlink(join(base, rel));
55
+ removed += 1;
56
+ } catch {
57
+ /* already gone */
58
+ }
59
+ }
60
+ return removed;
61
+ }
62
+
63
+ /** Create the proposals/ and local/ governance folders with guidance READMEs. */
64
+ export async function scaffoldGovernanceFolders(targetDir) {
65
+ const base = join(targetDir, LOCAL_DIR);
66
+
67
+ const rootReadme = `# .ai-skills
68
+
69
+ This folder is managed by the **Kiroku AI Standards** CLI (\`kiroku-ai\`).
70
+ It carries the AI context (prompts, rules, skills) for this project.
71
+
72
+ ## Layout
73
+
74
+ - **Downloaded skill folders** (\`global/\`, \`frontend/\`, \`backend/\`, \`infra-and-db/\`)
75
+ are pulled from the Single Source of Truth and **must not be edited locally** —
76
+ changes here are overwritten on the next sync.
77
+ - **\`proposals/\`** — drop framework-agnostic skills you want to promote
78
+ company-wide. Pushing to \`main\` opens a PR in the SSOT repository automatically.
79
+ - **\`local/\`** — project-specific AI rules. Ignored by syncing, never uploaded.
80
+
81
+ > To propose a change to a downloaded skill, copy it into \`proposals/\` and edit
82
+ > there. Architecture leads review every proposal before org-wide adoption.
83
+ `;
84
+
85
+ const proposalsReadme = `# proposals/
86
+
87
+ Place **new, framework-agnostic** skills here to propose them to the whole
88
+ company. On push to \`main\`, the \`kiroku-ai-sync\` GitHub Action copies these
89
+ files into the central \`${ORG}/${REPO}\` repository and opens a Pull Request for
90
+ the ${REVIEW_TEAM} team to review.
91
+
92
+ Naming: use a descriptive kebab-case filename, e.g. \`nextjs-routing.md\`.
93
+ `;
94
+
95
+ const localReadme = `# local/
96
+
97
+ Project-specific AI rules (local database schemas, internal conventions, etc.).
98
+ These files are **never** synced to the central repository. Use them freely.
99
+ `;
100
+
101
+ await writeFileEnsured(join(base, 'README.md'), rootReadme);
102
+ await writeFileEnsured(join(base, 'proposals', 'README.md'), proposalsReadme);
103
+ await writeFileEnsured(join(base, 'proposals', '.gitkeep'), '');
104
+ await writeFileEnsured(join(base, 'local', 'README.md'), localReadme);
105
+ await writeFileEnsured(join(base, 'local', '.gitkeep'), '');
106
+ }
107
+
108
+ /**
109
+ * Inject the two-way sync workflow, substituting org/repo/team placeholders.
110
+ * @returns {Promise<{ path: string, existed: boolean }>}
111
+ */
112
+ export async function injectSyncWorkflow(targetDir) {
113
+ const template = await readFile(join(TEMPLATES_DIR, 'kiroku-ai-sync.yml'), 'utf8');
114
+ const rendered = template
115
+ .replaceAll('__ORG__', ORG)
116
+ .replaceAll('__REPO__', REPO)
117
+ .replaceAll('__REF__', REF)
118
+ .replaceAll('__REVIEW_TEAM__', REVIEW_TEAM);
119
+
120
+ const dest = join(targetDir, WORKFLOW_PATH);
121
+ let existed = false;
122
+ try {
123
+ await readFile(dest, 'utf8');
124
+ existed = true;
125
+ } catch {
126
+ /* file does not exist yet */
127
+ }
128
+ await writeFileEnsured(dest, rendered);
129
+ return { path: WORKFLOW_PATH, existed };
130
+ }
package/src/source.mjs ADDED
@@ -0,0 +1,19 @@
1
+ /**
2
+ * Source dispatcher. Resolves skills/agents from either GitHub (default) or the
3
+ * local filesystem when KIROKU_AI_SOURCE is set. Keeps a single import surface
4
+ * (`fetchTree`, `downloadFile`) for the rest of the CLI.
5
+ */
6
+
7
+ import { SOURCE } from './config.mjs';
8
+ import * as github from './github.mjs';
9
+ import * as local from './local-source.mjs';
10
+
11
+ const impl = SOURCE ? local : github;
12
+
13
+ export const fetchTree = impl.fetchTree;
14
+ export const downloadFile = impl.downloadFile;
15
+
16
+ /** Human-readable description of where files come from (for logs). */
17
+ export function sourceLabel() {
18
+ return SOURCE ? `local:${SOURCE}` : 'github';
19
+ }
@@ -0,0 +1,108 @@
1
+ # Injected by the Kiroku AI Standards CLI (`kiroku-ai init`).
2
+ #
3
+ # Two-way sync: when a developer pushes a new skill into `.ai-skills/proposals/`,
4
+ # this workflow forwards it to the central SSOT repository as a Pull Request so
5
+ # the architecture leads can review it for company-wide adoption.
6
+ #
7
+ # Requirements:
8
+ # - Organization secret `KIROKU_AI_SYNC_TOKEN`: a token (PAT or GitHub App)
9
+ # with `contents:write` and `pull_requests:write` on __ORG__/__REPO__.
10
+ name: Kiroku AI — Sync Proposals to SSOT
11
+
12
+ on:
13
+ push:
14
+ branches: [main]
15
+ paths:
16
+ - '.ai-skills/proposals/**'
17
+
18
+ # Avoid racing PRs when several proposals land close together.
19
+ concurrency:
20
+ group: kiroku-ai-sync
21
+ cancel-in-progress: false
22
+
23
+ jobs:
24
+ propose:
25
+ name: Open proposal PR in __ORG__/__REPO__
26
+ runs-on: ubuntu-latest
27
+ permissions:
28
+ contents: read
29
+ steps:
30
+ - name: Checkout this project
31
+ uses: actions/checkout@v4
32
+ with:
33
+ fetch-depth: 0
34
+
35
+ - name: Forward proposals as a Pull Request
36
+ env:
37
+ GH_TOKEN: ${{ secrets.KIROKU_AI_SYNC_TOKEN }}
38
+ CENTRAL_ORG: __ORG__
39
+ CENTRAL_REPO: __REPO__
40
+ REVIEW_TEAM: __REVIEW_TEAM__
41
+ SOURCE_REPO: ${{ github.repository }}
42
+ SOURCE_SHA: ${{ github.sha }}
43
+ RUN_ID: ${{ github.run_id }}
44
+ run: |
45
+ set -euo pipefail
46
+
47
+ if [ -z "${GH_TOKEN:-}" ]; then
48
+ echo "::error::Missing KIROKU_AI_SYNC_TOKEN organization secret."
49
+ exit 1
50
+ fi
51
+
52
+ SOURCE_NAME="${SOURCE_REPO##*/}"
53
+ WORKDIR="$(mktemp -d)"
54
+ CENTRAL_SLUG="${CENTRAL_ORG}/${CENTRAL_REPO}"
55
+ BRANCH="proposal/${SOURCE_NAME}-${RUN_ID}"
56
+ DEST_DIR="proposals/incoming/${SOURCE_NAME}"
57
+
58
+ echo "Cloning ${CENTRAL_SLUG}..."
59
+ git clone --depth 1 \
60
+ "https://x-access-token:${GH_TOKEN}@github.com/${CENTRAL_SLUG}.git" \
61
+ "${WORKDIR}/central"
62
+
63
+ cd "${WORKDIR}/central"
64
+ git config user.name "kiroku-ai-bot"
65
+ git config user.email "kiroku-ai-bot@users.noreply.github.com"
66
+ git checkout -b "${BRANCH}"
67
+
68
+ mkdir -p "${DEST_DIR}"
69
+ # Copy every proposed file except placeholders/readmes.
70
+ rsync -a \
71
+ --exclude='.gitkeep' \
72
+ --exclude='README.md' \
73
+ "${GITHUB_WORKSPACE}/.ai-skills/proposals/" "${DEST_DIR}/"
74
+
75
+ if git diff --quiet && git diff --cached --quiet; then
76
+ echo "No proposal changes to forward. Exiting cleanly."
77
+ exit 0
78
+ fi
79
+
80
+ git add "${DEST_DIR}"
81
+ git commit -m "proposal: incoming skills from ${SOURCE_REPO}@${SOURCE_SHA}"
82
+ git push origin "${BRANCH}"
83
+
84
+ PR_BODY="$(cat <<EOF
85
+ Automated proposal forwarded by the Kiroku AI Standards sync action.
86
+
87
+ - **Source repository:** ${SOURCE_REPO}
88
+ - **Source commit:** ${SOURCE_SHA}
89
+ - **Staged under:** \`${DEST_DIR}/\`
90
+
91
+ Please review and relocate the proposed skill(s) into the appropriate
92
+ curated folder (e.g. \`skills/frontend/...\`) before merging.
93
+ EOF
94
+ )"
95
+
96
+ gh pr create \
97
+ --repo "${CENTRAL_SLUG}" \
98
+ --base main \
99
+ --head "${BRANCH}" \
100
+ --title "Proposal: skills from ${SOURCE_NAME}" \
101
+ --body "${PR_BODY}" \
102
+ --reviewer "${REVIEW_TEAM}" || \
103
+ gh pr create \
104
+ --repo "${CENTRAL_SLUG}" \
105
+ --base main \
106
+ --head "${BRANCH}" \
107
+ --title "Proposal: skills from ${SOURCE_NAME}" \
108
+ --body "${PR_BODY}"