@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/README.md
ADDED
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
# kiroku-ai CLI
|
|
2
|
+
|
|
3
|
+
Interactive CLI that bootstraps **Kiroku AI Standards** ("AI Context as Code")
|
|
4
|
+
into any project. It asks a few questions about your stack, downloads only the
|
|
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.
|
|
8
|
+
|
|
9
|
+
## Usage
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
# Pre-requisite: ensure you have configured your .npmrc with your GitHub token.
|
|
13
|
+
# (See .npmrc.example in the root directory for instructions).
|
|
14
|
+
|
|
15
|
+
# From the root of the project you want to set up:
|
|
16
|
+
pnpm dlx @kiroku-solutions/kiroku-ai init
|
|
17
|
+
|
|
18
|
+
# Re-sync skills later (picks up new files, removes stale ones):
|
|
19
|
+
pnpm dlx @kiroku-solutions/kiroku-ai update
|
|
20
|
+
|
|
21
|
+
# Switch which AI tool this project targets:
|
|
22
|
+
pnpm dlx @kiroku-solutions/kiroku-ai env # interactive picker
|
|
23
|
+
pnpm dlx @kiroku-solutions/kiroku-ai env cursor # non-interactive
|
|
24
|
+
|
|
25
|
+
# or, inside this monorepo for local development:
|
|
26
|
+
pnpm --dir CLI start init
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
### Commands
|
|
30
|
+
|
|
31
|
+
| Command | Description |
|
|
32
|
+
| ---------------- | --------------------------------------------------------------------------- |
|
|
33
|
+
| `init` | Guided stack builder; downloads skills and scaffolds `.ai-skills/`. |
|
|
34
|
+
| `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. |
|
|
36
|
+
|
|
37
|
+
### AI environments
|
|
38
|
+
|
|
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/`:
|
|
41
|
+
|
|
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` |
|
|
50
|
+
|
|
51
|
+
Pointer files carry a `kiroku-ai:managed` marker — the CLI never overwrites or
|
|
52
|
+
deletes a hand-written file you already had. Switch tools anytime with
|
|
53
|
+
`kiroku-ai env`.
|
|
54
|
+
|
|
55
|
+
### Options
|
|
56
|
+
|
|
57
|
+
| Option | Description |
|
|
58
|
+
| -------------- | ------------------------------------------------------------------ |
|
|
59
|
+
| `--dir <path>` | Target project directory (default: current directory). |
|
|
60
|
+
| `--ref <ref>` | Git ref of the SSOT to pull from (default: `main`). |
|
|
61
|
+
| `--force` | Re-run even if a lockfile exists (re-installs / overwrites skills). |
|
|
62
|
+
| `--dry-run` | Resolve the stack and list files without writing anything. |
|
|
63
|
+
| `-h, --help` | Show help. |
|
|
64
|
+
| `-v, --version`| Show version. |
|
|
65
|
+
|
|
66
|
+
### Environment variables
|
|
67
|
+
|
|
68
|
+
| Variable | Purpose |
|
|
69
|
+
| ------------------ | ----------------------------------------------------------- |
|
|
70
|
+
| `KIROKU_AI_ORG` | Override the GitHub org (default: `Kiroku-Solutions`). |
|
|
71
|
+
| `KIROKU_AI_REPO` | Override the repo (default: `kiroku-ai-standards`). |
|
|
72
|
+
| `KIROKU_AI_REF` | Override the default git ref. |
|
|
73
|
+
| `KIROKU_AI_TOKEN` | Read token — **required if the SSOT repo is private**. |
|
|
74
|
+
| `KIROKU_AI_SOURCE` | Local path to an SSOT checkout — reads from disk instead of GitHub (for testing). |
|
|
75
|
+
|
|
76
|
+
## What it generates
|
|
77
|
+
|
|
78
|
+
```text
|
|
79
|
+
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
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
## How the stack maps to skills
|
|
90
|
+
|
|
91
|
+
The compatibility matrix lives in [`src/resolver.mjs`](./src/resolver.mjs). In short:
|
|
92
|
+
|
|
93
|
+
- `global/` is **always** installed.
|
|
94
|
+
- A frontend scope pulls `frontend/shared/` plus the chosen framework folder.
|
|
95
|
+
Astro additionally pulls UI-integration folders (React → `frontend/react`).
|
|
96
|
+
- A backend scope pulls `backend/shared/` plus the chosen tech folder. Choosing
|
|
97
|
+
**None (Supabase BaaS)** pulls `infra-and-db/supabase.md` instead.
|
|
98
|
+
- Databases, deployment target and DevOps tools pull their matching `infra-and-db/`
|
|
99
|
+
/ `backend/shared/` documents.
|
|
100
|
+
|
|
101
|
+
## Architecture
|
|
102
|
+
|
|
103
|
+
```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)
|
|
108
|
+
prompts.mjs # @clack/prompts guided flow & compatibility constraints
|
|
109
|
+
resolver.mjs # stack -> skill selectors -> repo blob paths
|
|
110
|
+
source.mjs # dispatch: GitHub or local filesystem (KIROKU_AI_SOURCE)
|
|
111
|
+
github.mjs # tree listing + raw file download (public/private)
|
|
112
|
+
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
|
|
116
|
+
templates/
|
|
117
|
+
kiroku-ai-sync.yml # workflow injected into consumer projects
|
|
118
|
+
test/ # node:test suite (run: pnpm test)
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
## Development
|
|
122
|
+
|
|
123
|
+
```bash
|
|
124
|
+
pnpm install
|
|
125
|
+
pnpm test
|
|
126
|
+
node bin/kiroku-ai.mjs --help
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
## Testing from another repo (offline, no GitHub)
|
|
130
|
+
|
|
131
|
+
You can verify the CLI downloads the right files **without pushing** the SSOT to
|
|
132
|
+
GitHub by pointing it at this repo on disk with `KIROKU_AI_SOURCE`:
|
|
133
|
+
|
|
134
|
+
```bash
|
|
135
|
+
# 1. Create a throwaway target project somewhere else
|
|
136
|
+
mkdir /tmp/demo-app && cd /tmp/demo-app
|
|
137
|
+
|
|
138
|
+
# 2. Run init against the local SSOT checkout (PowerShell shown below)
|
|
139
|
+
# bash:
|
|
140
|
+
KIROKU_AI_SOURCE=/path/to/kiroku-ai-standards \
|
|
141
|
+
node /path/to/kiroku-ai-standards/CLI/bin/kiroku-ai.mjs init
|
|
142
|
+
|
|
143
|
+
# 3. Inspect what came down
|
|
144
|
+
ls -R .ai-skills
|
|
145
|
+
cat .ai-skills/lockfile.json
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
PowerShell:
|
|
149
|
+
|
|
150
|
+
```powershell
|
|
151
|
+
$env:KIROKU_AI_SOURCE = "T:\Kiroku\kiroku-ai-standards"
|
|
152
|
+
node "T:\Kiroku\kiroku-ai-standards\CLI\bin\kiroku-ai.mjs" init
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
When `KIROKU_AI_SOURCE` is set, the resolver reads from disk, so each folder's
|
|
156
|
+
`README.md` (and the `infra-and-db/*.md` topic files) is what gets installed —
|
|
157
|
+
confirming the stack → files mapping without any network access.
|
package/package.json
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@kiroku-solutions/kiroku-ai",
|
|
3
|
+
"version": "0.1.2",
|
|
4
|
+
"description": "Interactive CLI to bootstrap Kiroku AI Standards (AI Context as Code) into any project.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"kiroku-ai": "./bin/kiroku-ai.mjs"
|
|
8
|
+
},
|
|
9
|
+
"files": [
|
|
10
|
+
"bin",
|
|
11
|
+
"src",
|
|
12
|
+
"templates"
|
|
13
|
+
],
|
|
14
|
+
"scripts": {
|
|
15
|
+
"start": "node ./bin/kiroku-ai.mjs",
|
|
16
|
+
"init": "node ./bin/kiroku-ai.mjs init",
|
|
17
|
+
"test": "node --test"
|
|
18
|
+
},
|
|
19
|
+
"engines": {
|
|
20
|
+
"node": ">=18.17.0"
|
|
21
|
+
},
|
|
22
|
+
"keywords": [
|
|
23
|
+
"kiroku",
|
|
24
|
+
"ai",
|
|
25
|
+
"skills",
|
|
26
|
+
"prompts",
|
|
27
|
+
"cli",
|
|
28
|
+
"ai-context-as-code"
|
|
29
|
+
],
|
|
30
|
+
"author": "Kiroku Solutions",
|
|
31
|
+
"license": "UNLICENSED",
|
|
32
|
+
"repository": {
|
|
33
|
+
"type": "git",
|
|
34
|
+
"url": "git+https://github.com/Kiroku-Solutions/kiroku-ai-standards.git",
|
|
35
|
+
"directory": "CLI"
|
|
36
|
+
},
|
|
37
|
+
"publishConfig": {
|
|
38
|
+
"access": "public",
|
|
39
|
+
"registry": "https://registry.npmjs.org/"
|
|
40
|
+
},
|
|
41
|
+
"dependencies": {
|
|
42
|
+
"@clack/prompts": "^0.7.0",
|
|
43
|
+
"picocolors": "^1.0.1"
|
|
44
|
+
}
|
|
45
|
+
}
|
package/src/cli.mjs
ADDED
|
@@ -0,0 +1,420 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CLI entrypoint and command router for `kiroku-ai`.
|
|
3
|
+
*
|
|
4
|
+
* kiroku-ai init [--dir <path>] [--ref <git-ref>] [--force] [--dry-run]
|
|
5
|
+
* kiroku-ai update [--dir <path>] [--ref <git-ref>]
|
|
6
|
+
* kiroku-ai env [tool] [--dir <path>]
|
|
7
|
+
* kiroku-ai --help | --version
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { readFile } from 'node:fs/promises';
|
|
11
|
+
import { dirname, resolve, join } from 'node:path';
|
|
12
|
+
import { fileURLToPath } from 'node:url';
|
|
13
|
+
import { spinner, log, intro, outro, select, isCancel, cancel } from '@clack/prompts';
|
|
14
|
+
import pc from 'picocolors';
|
|
15
|
+
|
|
16
|
+
import { REF, SKILLS_ROOT, LOCAL_DIR, ORG, REPO, TOKEN, setToken, saveTokenToNpmrc, SOURCE } from './config.mjs';
|
|
17
|
+
import { fetchTree, downloadFile, sourceLabel } from './source.mjs';
|
|
18
|
+
import { resolveSkillSelectors, expandSelectors, collectAgentBlobs } from './resolver.mjs';
|
|
19
|
+
import {
|
|
20
|
+
showIntro,
|
|
21
|
+
showOutro,
|
|
22
|
+
collectStack,
|
|
23
|
+
summarizeStack,
|
|
24
|
+
confirmInstall,
|
|
25
|
+
promptForToken,
|
|
26
|
+
} from './prompts.mjs';
|
|
27
|
+
import { lockfileExists, readLockfile, writeLockfile } from './lockfile.mjs';
|
|
28
|
+
import {
|
|
29
|
+
writeSkills,
|
|
30
|
+
scaffoldGovernanceFolders,
|
|
31
|
+
injectSyncWorkflow,
|
|
32
|
+
pruneStaleSkills,
|
|
33
|
+
} from './scaffold.mjs';
|
|
34
|
+
import {
|
|
35
|
+
ENVIRONMENTS,
|
|
36
|
+
isValidEnvironment,
|
|
37
|
+
environmentChoices,
|
|
38
|
+
writePointer,
|
|
39
|
+
removeManagedPointer,
|
|
40
|
+
} from './integrations.mjs';
|
|
41
|
+
|
|
42
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
43
|
+
|
|
44
|
+
function parseArgs(argv) {
|
|
45
|
+
const args = { _: [], ref: REF, dir: process.cwd(), force: false, dryRun: false };
|
|
46
|
+
for (let i = 0; i < argv.length; i++) {
|
|
47
|
+
const a = argv[i];
|
|
48
|
+
switch (a) {
|
|
49
|
+
case '-h':
|
|
50
|
+
case '--help':
|
|
51
|
+
args.help = true;
|
|
52
|
+
break;
|
|
53
|
+
case '-v':
|
|
54
|
+
case '--version':
|
|
55
|
+
args.version = true;
|
|
56
|
+
break;
|
|
57
|
+
case '--force':
|
|
58
|
+
args.force = true;
|
|
59
|
+
break;
|
|
60
|
+
case '--dry-run':
|
|
61
|
+
args.dryRun = true;
|
|
62
|
+
break;
|
|
63
|
+
case '--dir':
|
|
64
|
+
args.dir = resolve(argv[++i] ?? '.');
|
|
65
|
+
break;
|
|
66
|
+
case '--ref':
|
|
67
|
+
args.ref = argv[++i] ?? REF;
|
|
68
|
+
args.refExplicit = true;
|
|
69
|
+
break;
|
|
70
|
+
default:
|
|
71
|
+
if (a.startsWith('--dir=')) args.dir = resolve(a.slice(6));
|
|
72
|
+
else if (a.startsWith('--ref=')) {
|
|
73
|
+
args.ref = a.slice(6);
|
|
74
|
+
args.refExplicit = true;
|
|
75
|
+
} else args._.push(a);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
return args;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
async function readVersion() {
|
|
82
|
+
try {
|
|
83
|
+
const pkg = JSON.parse(await readFile(join(__dirname, '..', 'package.json'), 'utf8'));
|
|
84
|
+
return pkg.version ?? '0.0.0';
|
|
85
|
+
} catch {
|
|
86
|
+
return '0.0.0';
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function printHelp() {
|
|
91
|
+
console.log(`
|
|
92
|
+
${pc.bold('kiroku-ai')} — Bootstrap Kiroku AI Standards into a project
|
|
93
|
+
|
|
94
|
+
${pc.bold('Usage')}
|
|
95
|
+
kiroku-ai init [options] Set up .ai-skills/ from a guided stack builder
|
|
96
|
+
kiroku-ai update [options] Re-sync skills for the recorded stack (prunes stale)
|
|
97
|
+
kiroku-ai env [tool] [options] Switch the AI tool / regenerate its pointer file
|
|
98
|
+
|
|
99
|
+
${pc.bold('Environments')} (for ${pc.cyan('env')})
|
|
100
|
+
${Object.entries(ENVIRONMENTS)
|
|
101
|
+
.map(([k, v]) => `${k} (${v.label})`)
|
|
102
|
+
.join(', ')}
|
|
103
|
+
|
|
104
|
+
${pc.bold('Options')}
|
|
105
|
+
--dir <path> Target project directory (default: current directory)
|
|
106
|
+
--ref <ref> Git ref of the SSOT to pull from (default: ${REF})
|
|
107
|
+
--force Re-run even if a lockfile already exists (overwrites skills)
|
|
108
|
+
--dry-run Resolve the stack and list files without writing anything
|
|
109
|
+
-h, --help Show this help
|
|
110
|
+
-v, --version Show version
|
|
111
|
+
|
|
112
|
+
${pc.bold('Source')}
|
|
113
|
+
${ORG}/${REPO} (override with KIROKU_AI_ORG / KIROKU_AI_REPO / KIROKU_AI_REF)
|
|
114
|
+
Private repos: set KIROKU_AI_TOKEN with a read token.
|
|
115
|
+
`);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
async function runInit(args) {
|
|
119
|
+
showIntro();
|
|
120
|
+
|
|
121
|
+
// Guard: refuse to clobber an existing install unless forced.
|
|
122
|
+
if (lockfileExists(args.dir) && !args.force && !args.dryRun) {
|
|
123
|
+
log.warn(
|
|
124
|
+
`A ${pc.cyan(`${LOCAL_DIR}/lockfile.json`)} already exists in this project.\n` +
|
|
125
|
+
` Run with ${pc.cyan('--force')} to re-install, or delete it manually.`,
|
|
126
|
+
);
|
|
127
|
+
showOutro(pc.yellow('Nothing to do.'));
|
|
128
|
+
return 0;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// 1. Collect the stack interactively.
|
|
132
|
+
const stack = await collectStack();
|
|
133
|
+
|
|
134
|
+
// 1.5 Ask for token if missing (and not using local source)
|
|
135
|
+
if (!TOKEN && !SOURCE) {
|
|
136
|
+
const newToken = await promptForToken();
|
|
137
|
+
setToken(newToken);
|
|
138
|
+
saveTokenToNpmrc(newToken);
|
|
139
|
+
log.success('GitHub token saved securely to ~/.npmrc');
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// 2. Resolve selectors and fetch the repository tree.
|
|
143
|
+
const selectors = resolveSkillSelectors(stack);
|
|
144
|
+
|
|
145
|
+
const s = spinner();
|
|
146
|
+
s.start(`Resolving skills from ${sourceLabel()} (${ORG}/${REPO}@${args.ref})`);
|
|
147
|
+
let blobPaths;
|
|
148
|
+
try {
|
|
149
|
+
const tree = await fetchTree(args.ref);
|
|
150
|
+
blobPaths = expandSelectors(selectors, tree, SKILLS_ROOT);
|
|
151
|
+
if (stack.agents) blobPaths = [...blobPaths, ...collectAgentBlobs(tree)];
|
|
152
|
+
s.stop(`Resolved ${blobPaths.length} file(s).`);
|
|
153
|
+
} catch (err) {
|
|
154
|
+
s.stop(pc.red('Failed to resolve skills.'));
|
|
155
|
+
log.error(err.message);
|
|
156
|
+
showOutro(pc.red('Aborted.'));
|
|
157
|
+
return 1;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
if (blobPaths.length === 0) {
|
|
161
|
+
log.warn('No skill files matched your stack on the remote yet.');
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
summarizeStack(stack, blobPaths.length);
|
|
165
|
+
|
|
166
|
+
// Dry-run stops here.
|
|
167
|
+
if (args.dryRun) {
|
|
168
|
+
log.info('Dry run — files that would be installed:');
|
|
169
|
+
for (const p of blobPaths) console.log(' ' + pc.dim(p.replace(/^skills\//, '')));
|
|
170
|
+
showOutro(pc.cyan('Dry run complete. Nothing was written.'));
|
|
171
|
+
return 0;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
if (!(await confirmInstall())) {
|
|
175
|
+
showOutro(pc.yellow('Cancelled. Nothing was written.'));
|
|
176
|
+
return 0;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// 3. Download the matched files.
|
|
180
|
+
const dl = spinner();
|
|
181
|
+
dl.start('Downloading skills');
|
|
182
|
+
const files = [];
|
|
183
|
+
try {
|
|
184
|
+
for (const path of blobPaths) {
|
|
185
|
+
const content = await downloadFile(path, args.ref);
|
|
186
|
+
files.push({ path, content });
|
|
187
|
+
}
|
|
188
|
+
dl.stop(`Downloaded ${files.length} file(s).`);
|
|
189
|
+
} catch (err) {
|
|
190
|
+
dl.stop(pc.red('Download failed.'));
|
|
191
|
+
log.error(err.message);
|
|
192
|
+
showOutro(pc.red('Aborted — no partial install was committed to the lockfile.'));
|
|
193
|
+
return 1;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// 4. Scaffold the local structure and inject the workflow.
|
|
197
|
+
const write = spinner();
|
|
198
|
+
write.start('Writing .ai-skills/ and injecting sync workflow');
|
|
199
|
+
try {
|
|
200
|
+
await writeSkills(args.dir, files);
|
|
201
|
+
await scaffoldGovernanceFolders(args.dir);
|
|
202
|
+
const wf = await injectSyncWorkflow(args.dir);
|
|
203
|
+
if (stack.environment) {
|
|
204
|
+
const pointer = await writePointer(args.dir, stack.environment);
|
|
205
|
+
if (pointer.skipped) {
|
|
206
|
+
log.warn(
|
|
207
|
+
`Kept your existing ${pc.cyan(pointer.file)} (not generated by kiroku-ai). ` +
|
|
208
|
+
`Add a reference to ${LOCAL_DIR}/ manually.`,
|
|
209
|
+
);
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
await writeLockfile(args.dir, { ref: args.ref, stack, files: blobPaths });
|
|
213
|
+
write.stop('Project scaffolded.');
|
|
214
|
+
if (wf.existed) {
|
|
215
|
+
log.warn(`Overwrote existing ${wf.path}.`);
|
|
216
|
+
}
|
|
217
|
+
} catch (err) {
|
|
218
|
+
write.stop(pc.red('Scaffolding failed.'));
|
|
219
|
+
log.error(err.message);
|
|
220
|
+
showOutro(pc.red('Aborted.'));
|
|
221
|
+
return 1;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// 5. Next steps.
|
|
225
|
+
log.success('Kiroku AI context installed.');
|
|
226
|
+
log.message(
|
|
227
|
+
[
|
|
228
|
+
`${pc.bold('Next steps')}`,
|
|
229
|
+
` 1. Reference ${pc.cyan(LOCAL_DIR + '/')} from your AI tool (Cursor / Copilot / Claude).`,
|
|
230
|
+
` 2. Propose skills by adding files to ${pc.cyan(LOCAL_DIR + '/proposals/')} and pushing to main.`,
|
|
231
|
+
` 3. Add the ${pc.cyan('KIROKU_AI_SYNC_TOKEN')} org secret so the sync workflow can open PRs.`,
|
|
232
|
+
].join('\n'),
|
|
233
|
+
);
|
|
234
|
+
showOutro(pc.green('Done.'));
|
|
235
|
+
return 0;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
async function runUpdate(args) {
|
|
239
|
+
intro(pc.bgMagenta(pc.black(' kiroku-ai update ')));
|
|
240
|
+
|
|
241
|
+
const lock = await readLockfile(args.dir);
|
|
242
|
+
if (!lock) {
|
|
243
|
+
log.warn(`No ${pc.cyan(`${LOCAL_DIR}/lockfile.json`)} found. Run ${pc.cyan('kiroku-ai init')} first.`);
|
|
244
|
+
outro(pc.yellow('Nothing to do.'));
|
|
245
|
+
return 0;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
const stack = lock.stack;
|
|
249
|
+
const ref = args.refExplicit ? args.ref : lock.source?.ref ?? args.ref;
|
|
250
|
+
const selectors = resolveSkillSelectors(stack);
|
|
251
|
+
|
|
252
|
+
if (!TOKEN && !SOURCE) {
|
|
253
|
+
const newToken = await promptForToken();
|
|
254
|
+
setToken(newToken);
|
|
255
|
+
saveTokenToNpmrc(newToken);
|
|
256
|
+
log.success('GitHub token saved securely to ~/.npmrc');
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
const s = spinner();
|
|
260
|
+
s.start(`Re-resolving skills from ${sourceLabel()} (${ORG}/${REPO}@${ref})`);
|
|
261
|
+
let blobPaths;
|
|
262
|
+
try {
|
|
263
|
+
const tree = await fetchTree(ref);
|
|
264
|
+
blobPaths = expandSelectors(selectors, tree, SKILLS_ROOT);
|
|
265
|
+
if (stack.agents) blobPaths = [...blobPaths, ...collectAgentBlobs(tree)];
|
|
266
|
+
s.stop(`Resolved ${blobPaths.length} file(s).`);
|
|
267
|
+
} catch (err) {
|
|
268
|
+
s.stop(pc.red('Failed to resolve skills.'));
|
|
269
|
+
log.error(err.message);
|
|
270
|
+
outro(pc.red('Aborted — existing install untouched.'));
|
|
271
|
+
return 1;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
// Download the fresh set.
|
|
275
|
+
const dl = spinner();
|
|
276
|
+
dl.start('Downloading latest skills');
|
|
277
|
+
const files = [];
|
|
278
|
+
try {
|
|
279
|
+
for (const path of blobPaths) {
|
|
280
|
+
files.push({ path, content: await downloadFile(path, ref) });
|
|
281
|
+
}
|
|
282
|
+
dl.stop(`Downloaded ${files.length} file(s).`);
|
|
283
|
+
} catch (err) {
|
|
284
|
+
dl.stop(pc.red('Download failed.'));
|
|
285
|
+
log.error(err.message);
|
|
286
|
+
outro(pc.red('Aborted — existing install untouched.'));
|
|
287
|
+
return 1;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
// Compute and prune files that left the resolved set.
|
|
291
|
+
const newLocal = new Set(blobPaths.map((p) => p.replace(/^skills\//, '')));
|
|
292
|
+
const stale = (lock.files ?? []).filter((p) => !newLocal.has(p));
|
|
293
|
+
|
|
294
|
+
const w = spinner();
|
|
295
|
+
w.start('Applying update');
|
|
296
|
+
try {
|
|
297
|
+
await writeSkills(args.dir, files);
|
|
298
|
+
const removed = await pruneStaleSkills(args.dir, stale);
|
|
299
|
+
// Refresh the AI pointer file for the recorded environment.
|
|
300
|
+
if (stack.environment && isValidEnvironment(stack.environment)) {
|
|
301
|
+
await writePointer(args.dir, stack.environment);
|
|
302
|
+
}
|
|
303
|
+
await writeLockfile(args.dir, { ref, stack, files: blobPaths });
|
|
304
|
+
w.stop(`Updated ${files.length} file(s)${removed ? `, removed ${removed} stale` : ''}.`);
|
|
305
|
+
} catch (err) {
|
|
306
|
+
w.stop(pc.red('Update failed.'));
|
|
307
|
+
log.error(err.message);
|
|
308
|
+
outro(pc.red('Aborted.'));
|
|
309
|
+
return 1;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
log.success('Skills are up to date.');
|
|
313
|
+
outro(pc.green('Done.'));
|
|
314
|
+
return 0;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
async function runEnv(args) {
|
|
318
|
+
intro(pc.bgMagenta(pc.black(' kiroku-ai env ')));
|
|
319
|
+
|
|
320
|
+
const lock = await readLockfile(args.dir);
|
|
321
|
+
if (!lock) {
|
|
322
|
+
log.warn(`No ${pc.cyan(`${LOCAL_DIR}/lockfile.json`)} found. Run ${pc.cyan('kiroku-ai init')} first.`);
|
|
323
|
+
outro(pc.yellow('Nothing to do.'));
|
|
324
|
+
return 0;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
const current = lock.stack?.environment ?? null;
|
|
328
|
+
|
|
329
|
+
// Allow a non-interactive form: `kiroku-ai env <tool>`.
|
|
330
|
+
let target = args._[1];
|
|
331
|
+
if (target && !isValidEnvironment(target)) {
|
|
332
|
+
log.error(`Unknown environment "${target}". Valid: ${Object.keys(ENVIRONMENTS).join(', ')}`);
|
|
333
|
+
outro(pc.red('Aborted.'));
|
|
334
|
+
return 1;
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
if (!target) {
|
|
338
|
+
const picked = await select({
|
|
339
|
+
message: 'Select the AI tool for this project',
|
|
340
|
+
initialValue: current ?? undefined,
|
|
341
|
+
options: environmentChoices().map((c) => ({
|
|
342
|
+
...c,
|
|
343
|
+
hint: c.value === current ? 'current' : undefined,
|
|
344
|
+
})),
|
|
345
|
+
});
|
|
346
|
+
if (isCancel(picked)) {
|
|
347
|
+
cancel('No changes made.');
|
|
348
|
+
return 0;
|
|
349
|
+
}
|
|
350
|
+
target = picked;
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
if (target === current) {
|
|
354
|
+
// Still (re)generate in case the pointer file was deleted.
|
|
355
|
+
await writePointer(args.dir, target);
|
|
356
|
+
log.info(`Already using ${pc.cyan(ENVIRONMENTS[target].label)}. Pointer file refreshed.`);
|
|
357
|
+
outro(pc.green('Done.'));
|
|
358
|
+
return 0;
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
// Swap: remove the previous managed pointer, write the new one.
|
|
362
|
+
if (current && isValidEnvironment(current)) {
|
|
363
|
+
const removed = await removeManagedPointer(args.dir, current);
|
|
364
|
+
if (removed) log.message(`Removed ${pc.dim(ENVIRONMENTS[current].file)}`);
|
|
365
|
+
}
|
|
366
|
+
const pointer = await writePointer(args.dir, target);
|
|
367
|
+
if (pointer.skipped) {
|
|
368
|
+
log.warn(`Kept your existing ${pc.cyan(pointer.file)} (not generated by kiroku-ai).`);
|
|
369
|
+
} else {
|
|
370
|
+
log.success(`Switched AI tool to ${pc.cyan(ENVIRONMENTS[target].label)} → ${pointer.file}`);
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
// Persist the new environment in the lockfile.
|
|
374
|
+
const stack = { ...lock.stack, environment: target };
|
|
375
|
+
await writeLockfile(args.dir, {
|
|
376
|
+
ref: lock.source?.ref ?? REF,
|
|
377
|
+
stack,
|
|
378
|
+
files: (lock.files ?? []).map((p) => `skills/${p}`),
|
|
379
|
+
});
|
|
380
|
+
|
|
381
|
+
outro(pc.green('Done.'));
|
|
382
|
+
return 0;
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
export async function run(argv) {
|
|
386
|
+
const args = parseArgs(argv);
|
|
387
|
+
|
|
388
|
+
if (args.version) {
|
|
389
|
+
console.log(await readVersion());
|
|
390
|
+
return 0;
|
|
391
|
+
}
|
|
392
|
+
if (args.help) {
|
|
393
|
+
printHelp();
|
|
394
|
+
return 0;
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
const command = args._[0] ?? 'init';
|
|
398
|
+
switch (command) {
|
|
399
|
+
case 'init': {
|
|
400
|
+
const code = await runInit(args);
|
|
401
|
+
process.exitCode = code;
|
|
402
|
+
return code;
|
|
403
|
+
}
|
|
404
|
+
case 'update': {
|
|
405
|
+
const code = await runUpdate(args);
|
|
406
|
+
process.exitCode = code;
|
|
407
|
+
return code;
|
|
408
|
+
}
|
|
409
|
+
case 'env': {
|
|
410
|
+
const code = await runEnv(args);
|
|
411
|
+
process.exitCode = code;
|
|
412
|
+
return code;
|
|
413
|
+
}
|
|
414
|
+
default:
|
|
415
|
+
console.error(pc.red(`Unknown command: ${command}`));
|
|
416
|
+
printHelp();
|
|
417
|
+
process.exitCode = 1;
|
|
418
|
+
return 1;
|
|
419
|
+
}
|
|
420
|
+
}
|