@kiroku-solutions/kiroku-ai 0.1.4 → 0.2.1

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 CHANGED
@@ -3,8 +3,23 @@
3
3
  Interactive CLI that bootstraps **Kiroku AI Standards** ("AI Context as Code")
4
4
  into any project. It asks a few questions about your stack, downloads only the
5
5
  relevant skill files from the central [`Kiroku-Solutions/kiroku-ai-standards`](https://github.com/Kiroku-Solutions/kiroku-ai-standards)
6
- repository into a local `.ai-skills/` folder, and wires up the two-way sync
7
- GitHub Action.
6
+ repository into the folders your chosen AI tool expects, and wires up the
7
+ two-way sync GitHub Action.
8
+
9
+ ## Private-by-design: the npm package is just a loader
10
+
11
+ The package published to npm contains **only a bootstrap loader** (`bin/`), not
12
+ the CLI source. On first run the loader:
13
+
14
+ 1. Asks for a GitHub token **first** (env `KIROKU_AI_TOKEN`/`GH_TOKEN`, or
15
+ `~/.npmrc`, or an interactive prompt — saved to `~/.npmrc`).
16
+ 2. Downloads the real CLI (`CLI/src/**`, `CLI/templates/**`, `CLI/package.json`)
17
+ from the private SSOT repo via the GitHub API.
18
+ 3. Caches it under `~/.kiroku-ai/runtime/<ref>/` and runs `npm install` once.
19
+ 4. Runs the CLI. Subsequent runs reuse the cache (no token needed) — pass
20
+ `KIROKU_AI_REFRESH=1` to force a re-download.
21
+
22
+ The CLI code and skills therefore never live on npm.
8
23
 
9
24
  ## Usage
10
25
 
@@ -30,27 +45,66 @@ pnpm --dir CLI start init
30
45
 
31
46
  | Command | Description |
32
47
  | ---------------- | --------------------------------------------------------------------------- |
33
- | `init` | Guided stack builder; downloads skills and scaffolds `.ai-skills/`. |
48
+ | `init` | Pick a setup mode, then download skills into the engine's folders. |
34
49
  | `update` | Re-resolves the recorded stack, re-downloads, and prunes removed skills. |
35
- | `env [tool]` | Switches the AI tool and (re)generates its pointer file. Updates lockfile. |
50
+ | `env [tool]` | Switches the AI tool: **migrates the folders** and regenerates its pointer. |
51
+
52
+ ### Setup modes
53
+
54
+ `init` starts by asking how much control you want:
55
+
56
+ | Mode | What it asks |
57
+ | ------------- | --------------------------------------------------------------------------------- |
58
+ | **Quickstart**| Pick one predefined stack (see presets below), then just the AI tool. |
59
+ | **Guided** | The classic stack questionnaire (scope → frontend → backend → db → deploy → devops).|
60
+ | **Modular** | Guided **plus** Architecture, Methodology and Development practices, and per-agent selection. |
61
+
62
+ #### Quickstart presets
63
+
64
+ | Preset id | Stack |
65
+ | ------------------- | --------------------------------------- |
66
+ | `nextjs-supabase` | Next.js + Supabase (fullstack, Vercel) |
67
+ | `astro-fastapi` | Astro + FastAPI (fullstack, Postgres) |
68
+ | `fastapi-supabase` | FastAPI + Supabase (backend API) |
69
+ | `sveltekit-postgres`| SvelteKit + Postgres (fullstack, Vercel)|
70
+ | `dotnet-postgres` | .NET API + Postgres (backend) |
71
+
72
+ Presets live in [`src/presets.mjs`](./src/presets.mjs) — add or edit entries there.
73
+
74
+ #### Engineering approach (Modular mode)
75
+
76
+ The Modular mode also captures, records (in the lockfile) and writes into the
77
+ pointer file an **Engineering approach** the AI must follow:
78
+
79
+ - **Architecture** (single): `layered`, `modular-monolith`, `hexagonal`, `clean`,
80
+ `microservices`, `event-driven`, `cqrs`, `serverless`, `mvc` (or none).
81
+ - **Methodology** (single): `scrum`, `kanban`, `scrumban`, `waterfall`, `xp`, `lean` (or none).
82
+ - **Development practices** (multi): `tdd`, `bdd`, `atdd`, `sdd`, `ddd`.
83
+
84
+ The matching docs come from `skills/architecture/`, `skills/methodology/` and
85
+ `skills/practices/` in the SSOT and install under your tool's skills folder.
36
86
 
37
87
  ### AI environments
38
88
 
39
- During `init` you pick the AI tool the project targets. The CLI generates the
40
- matching "pointer" file so the tool auto-loads `.ai-skills/`:
89
+ During `init` you pick the AI tool the project targets. The CLI installs into
90
+ **that tool's native folder convention** and generates the matching "pointer"
91
+ file so the tool auto-loads its skills. Tools without their own convention
92
+ (Cursor, Copilot, Windsurf) use the generic `.agents/` + `skills/` fallback.
41
93
 
42
- | Tool | Generated file |
43
- | ------------- | ---------------------------------- |
44
- | Claude Code | `CLAUDE.md` |
45
- | Cursor | `.cursor/rules/kiroku-ai.mdc` |
46
- | GitHub Copilot| `.github/copilot-instructions.md` |
47
- | OpenCode | `AGENTS.md` |
48
- | Antigravity | `AGENTS.md` |
49
- | Windsurf | `.windsurf/rules/kiroku-ai.md` |
94
+ | Tool | Pointer file | Agents folder | Skills folder |
95
+ | ------------- | ---------------------------------- | ------------------ | ------------------- |
96
+ | Claude Code | `CLAUDE.md` | `.claude/agents/` | `.claude/skills/` |
97
+ | Cursor | `.cursor/rules/kiroku-ai.mdc` | `.agents/` | `skills/` |
98
+ | GitHub Copilot| `.github/copilot-instructions.md` | `.agents/` | `skills/` |
99
+ | OpenCode | `AGENTS.md` | `.opencode/agents/`| `.opencode/skills/` |
100
+ | Antigravity | `GEMINI.md` | `.agent/agents/` | `.agent/skills/` |
101
+ | Windsurf | `.windsurf/rules/kiroku-ai.md` | `.agents/` | `skills/` |
50
102
 
51
103
  Pointer files carry a `kiroku-ai:managed` marker — the CLI never overwrites or
52
104
  deletes a hand-written file you already had. Switch tools anytime with
53
- `kiroku-ai env`.
105
+ `kiroku-ai env <tool>`; the installed skill/agent folders are moved to the new
106
+ tool's convention automatically. The lockfile always lives in the engine-stable
107
+ `.kiroku-ai/` folder.
54
108
 
55
109
  ### Options
56
110
 
@@ -70,20 +124,27 @@ deletes a hand-written file you already had. Switch tools anytime with
70
124
  | `KIROKU_AI_ORG` | Override the GitHub org (default: `Kiroku-Solutions`). |
71
125
  | `KIROKU_AI_REPO` | Override the repo (default: `kiroku-ai-standards`). |
72
126
  | `KIROKU_AI_REF` | Override the default git ref. |
73
- | `KIROKU_AI_TOKEN` | Read token — **required if the SSOT repo is private**. |
127
+ | `KIROKU_AI_TOKEN` | Read token — **required to fetch the private CLI + skills**. |
128
+ | `KIROKU_AI_REFRESH`| Force the loader to re-download the cached CLI runtime. |
74
129
  | `KIROKU_AI_SOURCE` | Local path to an SSOT checkout — reads from disk instead of GitHub (for testing). |
75
130
 
76
131
  ## What it generates
77
132
 
133
+ Folder names depend on the chosen AI tool (see the table above). Example for
134
+ **Claude Code**:
135
+
78
136
  ```text
79
137
  your-project/
80
- ├── .ai-skills/
81
- │ ├── global/ frontend/ backend/ infra-and-db/ # downloaded skills (do not edit)
82
- │ ├── proposals/ # propose skills company-wide
83
- │ ├── local/ # project-only AI rules (never synced)
84
- │ ├── README.md
85
- │ └── lockfile.json # prevents duplicate installs
86
- └── .github/workflows/kiroku-ai-sync.yml # two-way sync action
138
+ ├── .claude/
139
+ │ ├── skills/
140
+ ├── global/ frontend/ backend/ infra-and-db/ # downloaded skills (do not edit)
141
+ ├── proposals/ # propose skills company-wide
142
+ ├── local/ # project-only AI rules (never synced)
143
+ └── README.md
144
+ └── agents/ # agent personas (Security Auditor, …)
145
+ ├── CLAUDE.md # pointer file (auto-loaded by the tool)
146
+ ├── .kiroku-ai/lockfile.json # engine-stable; prevents duplicate installs
147
+ └── .github/workflows/kiroku-ai-sync.yml # two-way sync action
87
148
  ```
88
149
 
89
150
  ## How the stack maps to skills
@@ -101,18 +162,18 @@ The compatibility matrix lives in [`src/resolver.mjs`](./src/resolver.mjs). In s
101
162
  ## Architecture
102
163
 
103
164
  ```text
104
- bin/kiroku-ai.mjs # executable shim
105
- src/
106
- cli.mjs # arg parsing + init orchestration
107
- config.mjs # org/repo/ref constants (+ env overrides)
165
+ bin/kiroku-ai.mjs # PUBLISHED loader: token-first, fetches src/ from private GitHub, caches, runs
166
+ src/ # NOT published to npm — fetched at runtime from the SSOT repo
167
+ cli.mjs # arg parsing + init/update/env orchestration
168
+ config.mjs # org/repo/ref constants, CONFIG_DIR, token resolution (+ env overrides)
108
169
  prompts.mjs # @clack/prompts guided flow & compatibility constraints
109
170
  resolver.mjs # stack -> skill selectors -> repo blob paths
110
171
  source.mjs # dispatch: GitHub or local filesystem (KIROKU_AI_SOURCE)
111
172
  github.mjs # tree listing + raw file download (public/private)
112
173
  local-source.mjs # read skills/agents from a local SSOT checkout (testing)
113
- scaffold.mjs # write .ai-skills/, governance folders, inject workflow, prune
114
- integrations.mjs # AI environment pointer files (CLAUDE.md, AGENTS.md, ...)
115
- lockfile.mjs # read/write/guard lockfile.json
174
+ scaffold.mjs # write engine folders, governance, inject workflow, prune, migrateLayout
175
+ integrations.mjs # per-engine pointer + agentsDir/skillsDir (CLAUDE.md, AGENTS.md, GEMINI.md, ...)
176
+ lockfile.mjs # read/write/guard .kiroku-ai/lockfile.json
116
177
  templates/
117
178
  kiroku-ai-sync.yml # workflow injected into consumer projects
118
179
  test/ # node:test suite (run: pnpm test)
@@ -140,9 +201,9 @@ mkdir /tmp/demo-app && cd /tmp/demo-app
140
201
  KIROKU_AI_SOURCE=/path/to/kiroku-ai-standards \
141
202
  node /path/to/kiroku-ai-standards/CLI/bin/kiroku-ai.mjs init
142
203
 
143
- # 3. Inspect what came down
144
- ls -R .ai-skills
145
- cat .ai-skills/lockfile.json
204
+ # 3. Inspect what came down (folder depends on the AI tool you picked)
205
+ ls -R .claude .opencode .agent .agents skills 2>/dev/null
206
+ cat .kiroku-ai/lockfile.json
146
207
  ```
147
208
 
148
209
  PowerShell:
package/bin/kiroku-ai.mjs CHANGED
@@ -1,8 +1,236 @@
1
1
  #!/usr/bin/env node
2
- import { run } from '../src/cli.mjs';
2
+ /**
3
+ * kiroku-ai — bootstrap loader.
4
+ *
5
+ * The published npm package contains ONLY this file (plus package.json). The
6
+ * real CLI implementation is never shipped to npm; instead this loader:
7
+ *
8
+ * 1. Asks for a GitHub token FIRST (env → ~/.npmrc → interactive prompt),
9
+ * saving it to ~/.npmrc for next time.
10
+ * 2. Downloads the CLI source (`CLI/src/**`, `CLI/templates/**`,
11
+ * `CLI/package.json`) from the private SSOT repo via the GitHub API.
12
+ * 3. Caches it under ~/.kiroku-ai/runtime/<ref>/ and runs `npm install` once
13
+ * so its dependencies (@clack/prompts, picocolors) resolve.
14
+ * 4. Dynamically imports the cached `src/cli.mjs` and runs it.
15
+ *
16
+ * When run from a full checkout of this repo (dev/CI), the sibling `../src`
17
+ * exists and is used directly — no token or network required.
18
+ */
3
19
 
4
- run(process.argv.slice(2)).catch((err) => {
5
- // Defensive top-level guard. Normal control flow handles its own errors.
20
+ import { spawn } from 'node:child_process';
21
+ import { existsSync } from 'node:fs';
22
+ import { mkdir, readFile, writeFile, appendFile } from 'node:fs/promises';
23
+ import { createInterface } from 'node:readline';
24
+ import os from 'node:os';
25
+ import { dirname, join, resolve } from 'node:path';
26
+ import { fileURLToPath, pathToFileURL } from 'node:url';
27
+
28
+ const __dirname = dirname(fileURLToPath(import.meta.url));
29
+
30
+ const ORG = process.env.KIROKU_AI_ORG ?? 'Kiroku-Solutions';
31
+ const REPO = process.env.KIROKU_AI_REPO ?? 'kiroku-ai-standards';
32
+ const REF = process.env.KIROKU_AI_REF ?? 'main';
33
+ const REFRESH = !!process.env.KIROKU_AI_REFRESH;
34
+
35
+ const API = 'https://api.github.com';
36
+ const CACHE_ROOT = join(os.homedir(), '.kiroku-ai', 'runtime');
37
+ // Subtree of the SSOT repo that holds the CLI, mapped onto the cache root.
38
+ const SUBTREE = 'CLI/';
39
+
40
+ /** Run the inner CLI from a given src directory. */
41
+ async function runFrom(srcDir, argv) {
42
+ const mod = await import(pathToFileURL(join(srcDir, 'cli.mjs')).href);
43
+ return mod.run(argv);
44
+ }
45
+
46
+ // ---------------------------------------------------------------------------
47
+ // Token handling (token first)
48
+ // ---------------------------------------------------------------------------
49
+
50
+ function tokenFromNpmrc(content) {
51
+ const match = content.match(
52
+ /\/\/(?:npm\.pkg\.github\.com|api\.github\.com)\/:_authToken=([^\s]+)/,
53
+ );
54
+ return match?.[1] ?? null;
55
+ }
56
+
57
+ async function resolveToken() {
58
+ if (process.env.KIROKU_AI_TOKEN) return process.env.KIROKU_AI_TOKEN;
59
+ if (process.env.GH_TOKEN) return process.env.GH_TOKEN;
60
+ for (const rc of [join(process.cwd(), '.npmrc'), join(os.homedir(), '.npmrc')]) {
61
+ try {
62
+ if (existsSync(rc)) {
63
+ const t = tokenFromNpmrc(await readFile(rc, 'utf8'));
64
+ if (t) return t;
65
+ }
66
+ } catch {
67
+ /* ignore unreadable .npmrc */
68
+ }
69
+ }
70
+ return null;
71
+ }
72
+
73
+ /** Prompt for a secret without echoing it to the terminal. */
74
+ function promptHidden(question) {
75
+ return new Promise((res) => {
76
+ const rl = createInterface({ input: process.stdin, output: process.stdout, terminal: true });
77
+ const onData = (char) => {
78
+ // Re-clear the line so typed characters never render.
79
+ const s = char.toString('utf8');
80
+ if (s === '\n' || s === '\r' || s === '') return;
81
+ process.stdout.clearLine?.(0);
82
+ process.stdout.cursorTo?.(0);
83
+ process.stdout.write(question);
84
+ };
85
+ process.stdin.on('data', onData);
86
+ rl.question(question, (answer) => {
87
+ process.stdin.off('data', onData);
88
+ process.stdout.write('\n');
89
+ rl.close();
90
+ res(answer.trim());
91
+ });
92
+ });
93
+ }
94
+
95
+ async function saveTokenToNpmrc(token) {
96
+ try {
97
+ await appendFile(join(os.homedir(), '.npmrc'), `\n//api.github.com/:_authToken=${token}\n`, 'utf8');
98
+ } catch {
99
+ /* non-fatal: token still used for this run via env */
100
+ }
101
+ }
102
+
103
+ async function ensureToken() {
104
+ let token = await resolveToken();
105
+ if (token) return token;
106
+
107
+ process.stdout.write(
108
+ '\nKiroku AI Standards — authentication required to fetch the private CLI.\n' +
109
+ 'Enter a GitHub token with read access to the SSOT repo (input hidden):\n',
110
+ );
111
+ token = await promptHidden('Token: ');
112
+ if (!token) {
113
+ console.error('No token provided. Aborting.');
114
+ process.exit(1);
115
+ }
116
+ await saveTokenToNpmrc(token);
117
+ process.stdout.write('Token saved to ~/.npmrc.\n');
118
+ return token;
119
+ }
120
+
121
+ // ---------------------------------------------------------------------------
122
+ // GitHub download
123
+ // ---------------------------------------------------------------------------
124
+
125
+ function ghHeaders(token, accept) {
126
+ return {
127
+ Accept: accept,
128
+ 'User-Agent': 'kiroku-ai-loader',
129
+ 'X-GitHub-Api-Version': '2022-11-28',
130
+ Authorization: `Bearer ${token}`,
131
+ };
132
+ }
133
+
134
+ async function ghJson(url, token, accept = 'application/vnd.github+json') {
135
+ const res = await fetch(url, { headers: ghHeaders(token, accept) });
136
+ if (!res.ok) {
137
+ const hint =
138
+ res.status === 401 || res.status === 403
139
+ ? ' (bad/expired token or rate-limited)'
140
+ : res.status === 404
141
+ ? ' (repo/ref not found or token lacks access)'
142
+ : '';
143
+ throw new Error(`GitHub ${res.status} ${res.statusText}${hint} — ${url}`);
144
+ }
145
+ return res;
146
+ }
147
+
148
+ async function fetchTree(token) {
149
+ const url = `${API}/repos/${ORG}/${REPO}/git/trees/${encodeURIComponent(REF)}?recursive=1`;
150
+ const data = await (await ghJson(url, token)).json();
151
+ if (data.truncated) throw new Error('GitHub tree truncated; cannot bootstrap CLI reliably.');
152
+ return data;
153
+ }
154
+
155
+ async function downloadBlob(repoPath, token) {
156
+ const url = `${API}/repos/${ORG}/${REPO}/contents/${repoPath
157
+ .split('/')
158
+ .map(encodeURIComponent)
159
+ .join('/')}?ref=${encodeURIComponent(REF)}`;
160
+ const res = await ghJson(url, token, 'application/vnd.github.raw+json');
161
+ return res.text();
162
+ }
163
+
164
+ /** Map a repo path under CLI/ to its path inside the runtime cache dir. */
165
+ function cacheRelPath(repoPath) {
166
+ return repoPath.slice(SUBTREE.length);
167
+ }
168
+
169
+ async function downloadCli(runtimeDir, token) {
170
+ const tree = await fetchTree(token);
171
+ const wanted = (tree.tree ?? []).filter(
172
+ (n) =>
173
+ n.type === 'blob' &&
174
+ (n.path.startsWith(`${SUBTREE}src/`) ||
175
+ n.path.startsWith(`${SUBTREE}templates/`) ||
176
+ n.path === `${SUBTREE}package.json`),
177
+ );
178
+ if (wanted.length === 0) {
179
+ throw new Error(`No CLI files found under ${SUBTREE} at ${ORG}/${REPO}@${REF}.`);
180
+ }
181
+ for (const node of wanted) {
182
+ const content = await downloadBlob(node.path, token);
183
+ const dest = join(runtimeDir, cacheRelPath(node.path));
184
+ await mkdir(dirname(dest), { recursive: true });
185
+ await writeFile(dest, content, 'utf8');
186
+ }
187
+ await writeFile(join(runtimeDir, '.kiroku-meta.json'), JSON.stringify({ sha: tree.sha, ref: REF }) + '\n', 'utf8');
188
+ }
189
+
190
+ function npmInstall(cwd) {
191
+ return new Promise((res, rej) => {
192
+ const npm = process.platform === 'win32' ? 'npm.cmd' : 'npm';
193
+ const child = spawn(
194
+ npm,
195
+ ['install', '--omit=dev', '--no-audit', '--no-fund', '--loglevel=error'],
196
+ { cwd, stdio: 'inherit', shell: process.platform === 'win32' },
197
+ );
198
+ child.on('error', rej);
199
+ child.on('close', (code) => (code === 0 ? res() : rej(new Error(`npm install exited ${code}`))));
200
+ });
201
+ }
202
+
203
+ // ---------------------------------------------------------------------------
204
+ // Main
205
+ // ---------------------------------------------------------------------------
206
+
207
+ async function main(argv) {
208
+ // Dev / full-checkout fast path: run the real source directly, no network.
209
+ const localSrc = resolve(__dirname, '..', 'src');
210
+ if (existsSync(join(localSrc, 'cli.mjs'))) {
211
+ return runFrom(localSrc, argv);
212
+ }
213
+
214
+ const runtimeDir = join(CACHE_ROOT, REF.replace(/[^a-zA-Z0-9._-]/g, '_'));
215
+ const cachedCli = join(runtimeDir, 'src', 'cli.mjs');
216
+ const cacheValid = existsSync(cachedCli) && existsSync(join(runtimeDir, 'node_modules')) && !REFRESH;
217
+
218
+ if (!cacheValid) {
219
+ const token = await ensureToken();
220
+ process.env.KIROKU_AI_TOKEN = token; // hand off to the inner CLI
221
+
222
+ process.stdout.write(`Fetching kiroku-ai CLI from ${ORG}/${REPO}@${REF}…\n`);
223
+ await mkdir(runtimeDir, { recursive: true });
224
+ await downloadCli(runtimeDir, token);
225
+
226
+ process.stdout.write('Installing CLI dependencies (first run only)…\n');
227
+ await npmInstall(runtimeDir);
228
+ }
229
+
230
+ return runFrom(join(runtimeDir, 'src'), argv);
231
+ }
232
+
233
+ main(process.argv.slice(2)).catch((err) => {
6
234
  console.error(err?.stack || String(err));
7
235
  process.exit(1);
8
236
  });
package/package.json CHANGED
@@ -1,15 +1,14 @@
1
1
  {
2
2
  "name": "@kiroku-solutions/kiroku-ai",
3
- "version": "0.1.4",
3
+ "version": "0.2.1",
4
4
  "description": "Interactive CLI to bootstrap Kiroku AI Standards (AI Context as Code) into any project.",
5
5
  "type": "module",
6
6
  "bin": {
7
7
  "kiroku-ai": "./bin/kiroku-ai.mjs"
8
8
  },
9
+ "comment:files": "Only the bootstrap loader is published to npm. The real CLI (src/, templates/) is fetched from the private SSOT repo at runtime; deps below are needed by that fetched runtime.",
9
10
  "files": [
10
- "bin",
11
- "src",
12
- "templates"
11
+ "bin"
13
12
  ],
14
13
  "scripts": {
15
14
  "start": "node ./bin/kiroku-ai.mjs",