@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.
- package/README.md +157 -0
- package/bin/kiroku-ai.mjs +8 -0
- package/package.json +45 -0
- package/src/cli.mjs +420 -0
- package/src/config.mjs +83 -0
- package/src/github.mjs +75 -0
- package/src/integrations.mjs +104 -0
- package/src/local-source.mjs +46 -0
- package/src/lockfile.mjs +47 -0
- package/src/prompts.mjs +232 -0
- package/src/resolver.mjs +141 -0
- package/src/scaffold.mjs +130 -0
- package/src/source.mjs +19 -0
- package/templates/kiroku-ai-sync.yml +108 -0
package/src/resolver.mjs
ADDED
|
@@ -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
|
+
}
|
package/src/scaffold.mjs
ADDED
|
@@ -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}"
|