@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/config.mjs
ADDED
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Central configuration for the Kiroku AI Standards CLI.
|
|
3
|
+
*
|
|
4
|
+
* Everything points at the Single Source of Truth (SSOT) repository. Each value
|
|
5
|
+
* can be overridden through an environment variable so the same binary works
|
|
6
|
+
* for forks, staging mirrors, or local testing without code changes.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
export const ORG = process.env.KIROKU_AI_ORG ?? 'Kiroku-Solutions';
|
|
10
|
+
export const REPO = process.env.KIROKU_AI_REPO ?? 'kiroku-ai-standards';
|
|
11
|
+
|
|
12
|
+
/** Git ref (branch, tag or commit SHA) the skills are pulled from. */
|
|
13
|
+
export const REF = process.env.KIROKU_AI_REF ?? 'main';
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Local source override (for testing without GitHub). When set to a path to a
|
|
17
|
+
* checkout of the SSOT repo, the CLI reads skills/agents straight from disk
|
|
18
|
+
* instead of calling the GitHub API.
|
|
19
|
+
*/
|
|
20
|
+
export const SOURCE = process.env.KIROKU_AI_SOURCE ?? null;
|
|
21
|
+
|
|
22
|
+
/** Path prefix inside the SSOT repo where the curated skills live. */
|
|
23
|
+
export const SKILLS_ROOT = 'skills';
|
|
24
|
+
|
|
25
|
+
/** Local folder created inside the consumer project. */
|
|
26
|
+
export const LOCAL_DIR = '.ai-skills';
|
|
27
|
+
|
|
28
|
+
/** Workflow file injected into the consumer project. */
|
|
29
|
+
export const WORKFLOW_PATH = '.github/workflows/kiroku-ai-sync.yml';
|
|
30
|
+
|
|
31
|
+
import fs from 'node:fs';
|
|
32
|
+
import path from 'node:path';
|
|
33
|
+
import os from 'node:os';
|
|
34
|
+
|
|
35
|
+
function resolveToken() {
|
|
36
|
+
if (process.env.KIROKU_AI_TOKEN) return process.env.KIROKU_AI_TOKEN;
|
|
37
|
+
if (process.env.GH_TOKEN) return process.env.GH_TOKEN;
|
|
38
|
+
|
|
39
|
+
// Magia de DX: Lee el token automáticamente del archivo .npmrc (local o del usuario)
|
|
40
|
+
try {
|
|
41
|
+
const npmrcPaths = [
|
|
42
|
+
path.join(process.cwd(), '.npmrc'),
|
|
43
|
+
path.join(os.homedir(), '.npmrc')
|
|
44
|
+
];
|
|
45
|
+
|
|
46
|
+
for (const rcPath of npmrcPaths) {
|
|
47
|
+
if (fs.existsSync(rcPath)) {
|
|
48
|
+
const content = fs.readFileSync(rcPath, 'utf-8');
|
|
49
|
+
const match = content.match(/\/\/npm\.pkg\.github\.com\/:_authToken=([^\s]+)/);
|
|
50
|
+
if (match && match[1]) {
|
|
51
|
+
return match[1];
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
} catch (e) {
|
|
56
|
+
// Ignorar errores de lectura
|
|
57
|
+
}
|
|
58
|
+
return null;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Optional GitHub token used for the API/raw downloads. Automatically extracted from .npmrc
|
|
63
|
+
*/
|
|
64
|
+
export let TOKEN = resolveToken();
|
|
65
|
+
|
|
66
|
+
export function setToken(newToken) {
|
|
67
|
+
TOKEN = newToken;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export function saveTokenToNpmrc(newToken) {
|
|
71
|
+
try {
|
|
72
|
+
const npmrcPath = path.join(os.homedir(), '.npmrc');
|
|
73
|
+
const line = `\n//npm.pkg.github.com/:_authToken=${newToken}\n@kiroku-solutions:registry=https://npm.pkg.github.com/\n`;
|
|
74
|
+
fs.appendFileSync(npmrcPath, line, 'utf-8');
|
|
75
|
+
} catch (e) {
|
|
76
|
+
// Fail silently if we can't write to ~/.npmrc
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/** Team that reviews proposed skills in the SSOT repository. */
|
|
81
|
+
export const REVIEW_TEAM = `${ORG}/architecture-leads`;
|
|
82
|
+
|
|
83
|
+
export const LOCKFILE_VERSION = 1;
|
package/src/github.mjs
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Thin GitHub client built on top of the global `fetch` (Node >= 18).
|
|
3
|
+
*
|
|
4
|
+
* Two responsibilities:
|
|
5
|
+
* 1. List the SSOT repository tree once (recursive) so we can resolve which
|
|
6
|
+
* blobs match the user's stack without N round-trips.
|
|
7
|
+
* 2. Download individual blobs as raw text (works for private repos too when a
|
|
8
|
+
* token is provided, via the Contents API `raw` media type).
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { ORG, REPO, TOKEN } from './config.mjs';
|
|
12
|
+
|
|
13
|
+
const API = 'https://api.github.com';
|
|
14
|
+
|
|
15
|
+
function baseHeaders(accept) {
|
|
16
|
+
const headers = {
|
|
17
|
+
Accept: accept,
|
|
18
|
+
'User-Agent': 'kiroku-ai-cli',
|
|
19
|
+
'X-GitHub-Api-Version': '2022-11-28',
|
|
20
|
+
};
|
|
21
|
+
if (TOKEN) headers.Authorization = `Bearer ${TOKEN}`;
|
|
22
|
+
return headers;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
async function fail(res, context) {
|
|
26
|
+
let detail = '';
|
|
27
|
+
try {
|
|
28
|
+
const body = await res.json();
|
|
29
|
+
detail = body?.message ? ` — ${body.message}` : '';
|
|
30
|
+
} catch {
|
|
31
|
+
/* ignore non-JSON bodies */
|
|
32
|
+
}
|
|
33
|
+
const hint =
|
|
34
|
+
res.status === 404
|
|
35
|
+
? ' (repo/ref not found, or private repo without a token — set KIROKU_AI_TOKEN)'
|
|
36
|
+
: res.status === 401 || res.status === 403
|
|
37
|
+
? ' (authentication failed or rate-limited — check KIROKU_AI_TOKEN)'
|
|
38
|
+
: '';
|
|
39
|
+
throw new Error(`GitHub ${context} failed: ${res.status} ${res.statusText}${detail}${hint}`);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Fetch the full recursive tree for a given ref.
|
|
44
|
+
* @param {string} ref branch, tag or commit SHA
|
|
45
|
+
* @returns {Promise<Array<{ path: string, type: 'blob' | 'tree' }>>}
|
|
46
|
+
*/
|
|
47
|
+
export async function fetchTree(ref) {
|
|
48
|
+
const url = `${API}/repos/${ORG}/${REPO}/git/trees/${encodeURIComponent(ref)}?recursive=1`;
|
|
49
|
+
const res = await fetch(url, { headers: baseHeaders('application/vnd.github+json') });
|
|
50
|
+
if (!res.ok) await fail(res, 'tree listing');
|
|
51
|
+
|
|
52
|
+
const data = await res.json();
|
|
53
|
+
if (data.truncated) {
|
|
54
|
+
throw new Error(
|
|
55
|
+
'GitHub tree response was truncated (repository too large). Aborting to avoid a partial install.',
|
|
56
|
+
);
|
|
57
|
+
}
|
|
58
|
+
return (data.tree ?? []).map((node) => ({ path: node.path, type: node.type }));
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Download a single file as raw text.
|
|
63
|
+
* @param {string} path repo-relative path
|
|
64
|
+
* @param {string} ref branch, tag or commit SHA
|
|
65
|
+
* @returns {Promise<string>}
|
|
66
|
+
*/
|
|
67
|
+
export async function downloadFile(path, ref) {
|
|
68
|
+
const url = `${API}/repos/${ORG}/${REPO}/contents/${path
|
|
69
|
+
.split('/')
|
|
70
|
+
.map(encodeURIComponent)
|
|
71
|
+
.join('/')}?ref=${encodeURIComponent(ref)}`;
|
|
72
|
+
const res = await fetch(url, { headers: baseHeaders('application/vnd.github.raw+json') });
|
|
73
|
+
if (!res.ok) await fail(res, `download of ${path}`);
|
|
74
|
+
return res.text();
|
|
75
|
+
}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AI environment integrations.
|
|
3
|
+
*
|
|
4
|
+
* Each supported AI tool discovers project context from a different file. This
|
|
5
|
+
* module maps an environment key to its canonical "pointer" file and renders a
|
|
6
|
+
* managed file that tells the tool to treat `.ai-skills/` as authoritative.
|
|
7
|
+
*
|
|
8
|
+
* Pointer files we generate carry a marker so we only ever delete/overwrite our
|
|
9
|
+
* own files — never a hand-written CLAUDE.md / AGENTS.md the user already had.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { mkdir, writeFile, readFile, unlink } from 'node:fs/promises';
|
|
13
|
+
import { dirname, join } from 'node:path';
|
|
14
|
+
import { LOCAL_DIR } from './config.mjs';
|
|
15
|
+
|
|
16
|
+
const MARKER = '<!-- kiroku-ai:managed -->';
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Supported environments, in display order. `mdc` toggles Cursor's frontmatter.
|
|
20
|
+
*/
|
|
21
|
+
export const ENVIRONMENTS = {
|
|
22
|
+
'claude-code': { label: 'Claude Code', file: 'CLAUDE.md' },
|
|
23
|
+
cursor: { label: 'Cursor', file: '.cursor/rules/kiroku-ai.mdc', mdc: true },
|
|
24
|
+
copilot: { label: 'GitHub Copilot', file: '.github/copilot-instructions.md' },
|
|
25
|
+
opencode: { label: 'OpenCode (AGENTS.md)', file: 'AGENTS.md' },
|
|
26
|
+
antigravity: { label: 'Antigravity', file: 'AGENTS.md' },
|
|
27
|
+
windsurf: { label: 'Windsurf', file: '.windsurf/rules/kiroku-ai.md' },
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
export function isValidEnvironment(key) {
|
|
31
|
+
return Object.prototype.hasOwnProperty.call(ENVIRONMENTS, key);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function environmentChoices() {
|
|
35
|
+
return Object.entries(ENVIRONMENTS).map(([value, { label }]) => ({ value, label }));
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function renderBody(envKey) {
|
|
39
|
+
const env = ENVIRONMENTS[envKey];
|
|
40
|
+
const header = env.mdc
|
|
41
|
+
? `---\ndescription: Kiroku AI Standards — project context\nalwaysApply: true\n---\n`
|
|
42
|
+
: '';
|
|
43
|
+
|
|
44
|
+
return `${header}# Kiroku AI Standards — Project Context
|
|
45
|
+
${MARKER}
|
|
46
|
+
|
|
47
|
+
This project follows Kiroku's shared AI standards. Treat the files inside
|
|
48
|
+
\`${LOCAL_DIR}/\` as authoritative context and conventions for this codebase.
|
|
49
|
+
|
|
50
|
+
- \`${LOCAL_DIR}/global/\` — organization-wide rules (always apply)
|
|
51
|
+
- \`${LOCAL_DIR}/frontend/\`, \`${LOCAL_DIR}/backend/\`, \`${LOCAL_DIR}/infra-and-db/\` — stack-specific rules
|
|
52
|
+
- \`${LOCAL_DIR}/local/\` — project-specific rules
|
|
53
|
+
|
|
54
|
+
Before writing or refactoring code, load and respect the relevant skill files in
|
|
55
|
+
\`${LOCAL_DIR}/\`. Do not edit the downloaded skill folders directly — propose
|
|
56
|
+
changes by adding files to \`${LOCAL_DIR}/proposals/\`.
|
|
57
|
+
|
|
58
|
+
> Managed by the \`kiroku-ai\` CLI. Run \`kiroku-ai env\` to switch AI tools.
|
|
59
|
+
`;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Write the pointer file for an environment.
|
|
64
|
+
* @returns {Promise<{ file: string, overwritten: boolean, skipped: boolean }>}
|
|
65
|
+
*/
|
|
66
|
+
export async function writePointer(targetDir, envKey) {
|
|
67
|
+
const env = ENVIRONMENTS[envKey];
|
|
68
|
+
const dest = join(targetDir, env.file);
|
|
69
|
+
|
|
70
|
+
let existing = null;
|
|
71
|
+
try {
|
|
72
|
+
existing = await readFile(dest, 'utf8');
|
|
73
|
+
} catch {
|
|
74
|
+
/* no existing file */
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// Never clobber a user-authored file that we didn't generate.
|
|
78
|
+
if (existing !== null && !existing.includes(MARKER)) {
|
|
79
|
+
return { file: env.file, overwritten: false, skipped: true };
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
await mkdir(dirname(dest), { recursive: true });
|
|
83
|
+
await writeFile(dest, renderBody(envKey), 'utf8');
|
|
84
|
+
return { file: env.file, overwritten: existing !== null, skipped: false };
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Remove a previously generated pointer file (only if it carries our marker).
|
|
89
|
+
* @returns {Promise<boolean>} true if a managed file was removed
|
|
90
|
+
*/
|
|
91
|
+
export async function removeManagedPointer(targetDir, envKey) {
|
|
92
|
+
const env = ENVIRONMENTS[envKey];
|
|
93
|
+
const dest = join(targetDir, env.file);
|
|
94
|
+
try {
|
|
95
|
+
const content = await readFile(dest, 'utf8');
|
|
96
|
+
if (content.includes(MARKER)) {
|
|
97
|
+
await unlink(dest);
|
|
98
|
+
return true;
|
|
99
|
+
}
|
|
100
|
+
} catch {
|
|
101
|
+
/* nothing to remove */
|
|
102
|
+
}
|
|
103
|
+
return false;
|
|
104
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Local filesystem source — mirrors the github.mjs interface so the CLI can be
|
|
3
|
+
* tested offline against a checkout of the SSOT repo (set KIROKU_AI_SOURCE).
|
|
4
|
+
*
|
|
5
|
+
* The `ref` argument is accepted for API parity but ignored: the local working
|
|
6
|
+
* tree is whatever is on disk.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { readFile, readdir } from 'node:fs/promises';
|
|
10
|
+
import { join, relative, sep } from 'node:path';
|
|
11
|
+
import { SOURCE } from './config.mjs';
|
|
12
|
+
|
|
13
|
+
function toPosix(p) {
|
|
14
|
+
return p.split(sep).join('/');
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
async function walk(dir, root, out) {
|
|
18
|
+
let entries;
|
|
19
|
+
try {
|
|
20
|
+
entries = await readdir(dir, { withFileTypes: true });
|
|
21
|
+
} catch {
|
|
22
|
+
return out;
|
|
23
|
+
}
|
|
24
|
+
for (const entry of entries) {
|
|
25
|
+
if (entry.name === '.git' || entry.name === 'node_modules') continue;
|
|
26
|
+
const abs = join(dir, entry.name);
|
|
27
|
+
const rel = toPosix(relative(root, abs));
|
|
28
|
+
if (entry.isDirectory()) {
|
|
29
|
+
out.push({ path: rel, type: 'tree' });
|
|
30
|
+
await walk(abs, root, out);
|
|
31
|
+
} else if (entry.isFile()) {
|
|
32
|
+
out.push({ path: rel, type: 'blob' });
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
return out;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** @returns {Promise<Array<{ path: string, type: 'blob' | 'tree' }>>} */
|
|
39
|
+
export async function fetchTree(_ref) {
|
|
40
|
+
return walk(SOURCE, SOURCE, []);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** @returns {Promise<string>} */
|
|
44
|
+
export async function downloadFile(path, _ref) {
|
|
45
|
+
return readFile(join(SOURCE, path), 'utf8');
|
|
46
|
+
}
|
package/src/lockfile.mjs
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Lockfile management.
|
|
3
|
+
*
|
|
4
|
+
* `.ai-skills/lockfile.json` records what was installed and from which ref. Its
|
|
5
|
+
* presence is the guard that prevents a second `init` from duplicating data
|
|
6
|
+
* (unless `--force` is passed).
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { readFile, writeFile } from 'node:fs/promises';
|
|
10
|
+
import { existsSync } from 'node:fs';
|
|
11
|
+
import { join } from 'node:path';
|
|
12
|
+
import { LOCAL_DIR, ORG, REPO, LOCKFILE_VERSION } from './config.mjs';
|
|
13
|
+
|
|
14
|
+
function lockfilePath(targetDir) {
|
|
15
|
+
return join(targetDir, LOCAL_DIR, 'lockfile.json');
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function lockfileExists(targetDir) {
|
|
19
|
+
return existsSync(lockfilePath(targetDir));
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export async function readLockfile(targetDir) {
|
|
23
|
+
try {
|
|
24
|
+
return JSON.parse(await readFile(lockfilePath(targetDir), 'utf8'));
|
|
25
|
+
} catch {
|
|
26
|
+
return null;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* @param {string} targetDir
|
|
32
|
+
* @param {object} args
|
|
33
|
+
* @param {string} args.ref
|
|
34
|
+
* @param {import('./resolver.mjs').Stack} args.stack
|
|
35
|
+
* @param {string[]} args.files repo-relative blob paths that were installed
|
|
36
|
+
*/
|
|
37
|
+
export async function writeLockfile(targetDir, { ref, stack, files }) {
|
|
38
|
+
const payload = {
|
|
39
|
+
lockfileVersion: LOCKFILE_VERSION,
|
|
40
|
+
generatedAt: new Date().toISOString(),
|
|
41
|
+
source: { org: ORG, repo: REPO, ref },
|
|
42
|
+
stack,
|
|
43
|
+
files: files.map((p) => p.replace(/^skills\//, '')).sort(),
|
|
44
|
+
};
|
|
45
|
+
await writeFile(lockfilePath(targetDir), JSON.stringify(payload, null, 2) + '\n', 'utf8');
|
|
46
|
+
return payload;
|
|
47
|
+
}
|
package/src/prompts.mjs
ADDED
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Interactive stack builder powered by @clack/prompts.
|
|
3
|
+
*
|
|
4
|
+
* The flow mirrors the Astro CLI feel: guided, conditional, and it enforces the
|
|
5
|
+
* compatibility matrix so users cannot assemble architecturally nonsensical
|
|
6
|
+
* stacks. Returns a `Stack` object consumed by `resolver.mjs`.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import {
|
|
10
|
+
intro,
|
|
11
|
+
outro,
|
|
12
|
+
group,
|
|
13
|
+
select,
|
|
14
|
+
multiselect,
|
|
15
|
+
confirm,
|
|
16
|
+
note,
|
|
17
|
+
isCancel,
|
|
18
|
+
cancel,
|
|
19
|
+
text,
|
|
20
|
+
log,
|
|
21
|
+
} from '@clack/prompts';
|
|
22
|
+
import pc from 'picocolors';
|
|
23
|
+
import { environmentChoices } from './integrations.mjs';
|
|
24
|
+
|
|
25
|
+
function ensure(value) {
|
|
26
|
+
if (isCancel(value)) {
|
|
27
|
+
cancel('Setup cancelled. No files were written.');
|
|
28
|
+
process.exit(0);
|
|
29
|
+
}
|
|
30
|
+
return value;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function showIntro() {
|
|
34
|
+
intro(pc.bgMagenta(pc.black(' kiroku-ai ')) + ' ' + pc.dim('AI Context as Code'));
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function showOutro(message) {
|
|
38
|
+
outro(message);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Runs the full guided flow.
|
|
43
|
+
* @returns {Promise<import('./resolver.mjs').Stack>}
|
|
44
|
+
*/
|
|
45
|
+
export async function collectStack() {
|
|
46
|
+
// --- Prompt 1: Project scope -------------------------------------------
|
|
47
|
+
const scope = ensure(
|
|
48
|
+
await select({
|
|
49
|
+
message: 'What is the scope of this project?',
|
|
50
|
+
options: [
|
|
51
|
+
{ value: 'fullstack', label: 'Fullstack', hint: 'frontend + backend' },
|
|
52
|
+
{ value: 'frontend', label: 'Frontend only' },
|
|
53
|
+
{ value: 'backend', label: 'Backend only' },
|
|
54
|
+
{ value: 'infra', label: 'Infra / Workers', hint: 'no app frameworks' },
|
|
55
|
+
],
|
|
56
|
+
}),
|
|
57
|
+
);
|
|
58
|
+
|
|
59
|
+
const wantsFrontend = scope === 'fullstack' || scope === 'frontend';
|
|
60
|
+
const wantsBackend = scope === 'fullstack' || scope === 'backend';
|
|
61
|
+
|
|
62
|
+
/** @type {import('./resolver.mjs').Stack} */
|
|
63
|
+
const stack = { scope };
|
|
64
|
+
|
|
65
|
+
// --- Prompt 2: Frontend framework --------------------------------------
|
|
66
|
+
if (wantsFrontend) {
|
|
67
|
+
stack.frontend = ensure(
|
|
68
|
+
await select({
|
|
69
|
+
message: 'Choose your frontend framework',
|
|
70
|
+
options: [
|
|
71
|
+
{ value: 'nextjs', label: 'Next.js', hint: 'App Router' },
|
|
72
|
+
{ value: 'astro', label: 'Astro', hint: 'islands / content' },
|
|
73
|
+
{ value: 'sveltekit', label: 'SvelteKit' },
|
|
74
|
+
],
|
|
75
|
+
}),
|
|
76
|
+
);
|
|
77
|
+
|
|
78
|
+
// Astro sub-prompt: optional UI integrations.
|
|
79
|
+
if (stack.frontend === 'astro') {
|
|
80
|
+
const addIntegrations = ensure(
|
|
81
|
+
await confirm({
|
|
82
|
+
message: 'Add Astro UI integrations?',
|
|
83
|
+
initialValue: true,
|
|
84
|
+
}),
|
|
85
|
+
);
|
|
86
|
+
if (addIntegrations) {
|
|
87
|
+
stack.astroIntegrations = ensure(
|
|
88
|
+
await multiselect({
|
|
89
|
+
message: 'Select integrations (Space to toggle, Enter to confirm)',
|
|
90
|
+
required: false,
|
|
91
|
+
options: [
|
|
92
|
+
{ value: 'react', label: 'React' },
|
|
93
|
+
{ value: 'svelte', label: 'Svelte' },
|
|
94
|
+
{ value: 'tailwind', label: 'Tailwind CSS' },
|
|
95
|
+
],
|
|
96
|
+
}),
|
|
97
|
+
);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// --- Prompt 3: Backend technology --------------------------------------
|
|
103
|
+
if (wantsBackend) {
|
|
104
|
+
stack.backend = ensure(
|
|
105
|
+
await select({
|
|
106
|
+
message: 'Choose your backend technology',
|
|
107
|
+
options: [
|
|
108
|
+
{ value: 'python-fastapi', label: 'Python (FastAPI)' },
|
|
109
|
+
{ value: 'dotnet', label: '.NET' },
|
|
110
|
+
{ value: 'java', label: 'Java' },
|
|
111
|
+
{ value: 'none', label: 'None', hint: 'Supabase BaaS' },
|
|
112
|
+
],
|
|
113
|
+
}),
|
|
114
|
+
);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// --- Prompt 4: Databases & state (multi-select) ------------------------
|
|
118
|
+
if (scope !== 'infra') {
|
|
119
|
+
stack.databases = ensure(
|
|
120
|
+
await multiselect({
|
|
121
|
+
message: 'Databases & state (Space to toggle)',
|
|
122
|
+
required: false,
|
|
123
|
+
options: [
|
|
124
|
+
{ value: 'supabase', label: 'Supabase (Postgres)' },
|
|
125
|
+
{ value: 'postgres', label: 'Custom Postgres' },
|
|
126
|
+
{ value: 'mongodb', label: 'MongoDB' },
|
|
127
|
+
{ value: 'redis', label: 'Redis' },
|
|
128
|
+
],
|
|
129
|
+
}),
|
|
130
|
+
);
|
|
131
|
+
} else {
|
|
132
|
+
stack.databases = [];
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// --- Prompt 5: Deployment target ---------------------------------------
|
|
136
|
+
// Compatibility constraint: Next.js defaults to Vercel.
|
|
137
|
+
const nextjsSelected = stack.frontend === 'nextjs';
|
|
138
|
+
stack.deploy = ensure(
|
|
139
|
+
await select({
|
|
140
|
+
message: nextjsSelected
|
|
141
|
+
? 'Deployment target ' + pc.dim('(Vercel recommended for Next.js)')
|
|
142
|
+
: 'Deployment target',
|
|
143
|
+
initialValue: nextjsSelected ? 'vercel' : undefined,
|
|
144
|
+
options: [
|
|
145
|
+
{
|
|
146
|
+
value: 'vercel',
|
|
147
|
+
label: 'Vercel',
|
|
148
|
+
hint: nextjsSelected ? 'recommended' : undefined,
|
|
149
|
+
},
|
|
150
|
+
{ value: 'cloudflare', label: 'Cloudflare', hint: 'Workers / Pages' },
|
|
151
|
+
{ value: 'docker', label: 'Docker / Self-Hosted' },
|
|
152
|
+
],
|
|
153
|
+
}),
|
|
154
|
+
);
|
|
155
|
+
|
|
156
|
+
// --- Prompt 6: DevOps & queues (multi-select) --------------------------
|
|
157
|
+
stack.devops = ensure(
|
|
158
|
+
await multiselect({
|
|
159
|
+
message: 'DevOps & queues (Space to toggle)',
|
|
160
|
+
required: false,
|
|
161
|
+
options: [
|
|
162
|
+
{ value: 'github-actions', label: 'GitHub Actions' },
|
|
163
|
+
{ value: 'n8n', label: 'n8n' },
|
|
164
|
+
{ value: 'rabbitmq', label: 'RabbitMQ' },
|
|
165
|
+
],
|
|
166
|
+
}),
|
|
167
|
+
);
|
|
168
|
+
|
|
169
|
+
// --- Prompt 7: Agent personas (optional) -------------------------------
|
|
170
|
+
stack.agents = ensure(
|
|
171
|
+
await confirm({
|
|
172
|
+
message: 'Include Kiroku agent personas? (e.g. Security Auditor)',
|
|
173
|
+
initialValue: true,
|
|
174
|
+
}),
|
|
175
|
+
);
|
|
176
|
+
|
|
177
|
+
// --- Prompt 8: AI environment (single select) --------------------------
|
|
178
|
+
// Determines which "pointer" file is generated so the chosen AI tool loads
|
|
179
|
+
// .ai-skills/ automatically. Switchable later via `kiroku-ai env`.
|
|
180
|
+
stack.environment = ensure(
|
|
181
|
+
await select({
|
|
182
|
+
message: 'Which AI tool will this project use?',
|
|
183
|
+
options: environmentChoices(),
|
|
184
|
+
}),
|
|
185
|
+
);
|
|
186
|
+
|
|
187
|
+
return stack;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/** Pretty-print the resolved stack as a summary note. */
|
|
191
|
+
export function summarizeStack(stack, fileCount) {
|
|
192
|
+
const lines = [];
|
|
193
|
+
lines.push(`${pc.bold('Scope')} ${stack.scope}`);
|
|
194
|
+
if (stack.frontend) {
|
|
195
|
+
const integ =
|
|
196
|
+
stack.astroIntegrations?.length ? ` (+ ${stack.astroIntegrations.join(', ')})` : '';
|
|
197
|
+
lines.push(`${pc.bold('Frontend')} ${stack.frontend}${integ}`);
|
|
198
|
+
}
|
|
199
|
+
if (stack.backend) lines.push(`${pc.bold('Backend')} ${stack.backend}`);
|
|
200
|
+
if (stack.databases?.length) lines.push(`${pc.bold('Database')} ${stack.databases.join(', ')}`);
|
|
201
|
+
if (stack.deploy) lines.push(`${pc.bold('Deploy')} ${stack.deploy}`);
|
|
202
|
+
if (stack.devops?.length) lines.push(`${pc.bold('DevOps')} ${stack.devops.join(', ')}`);
|
|
203
|
+
if (stack.agents) lines.push(`${pc.bold('Agents')} included`);
|
|
204
|
+
if (stack.environment) lines.push(`${pc.bold('AI tool')} ${stack.environment}`);
|
|
205
|
+
lines.push('');
|
|
206
|
+
lines.push(pc.green(`${fileCount} skill file(s) will be installed.`));
|
|
207
|
+
note(lines.join('\n'), 'Resolved stack');
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
export async function confirmInstall() {
|
|
211
|
+
return ensure(
|
|
212
|
+
await confirm({
|
|
213
|
+
message: 'Install these skills into ./.ai-skills ?',
|
|
214
|
+
initialValue: true,
|
|
215
|
+
}),
|
|
216
|
+
);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
export async function promptForToken() {
|
|
220
|
+
log.warn(pc.yellow('Authentication required to access private skills.'));
|
|
221
|
+
|
|
222
|
+
const token = ensure(
|
|
223
|
+
await text({
|
|
224
|
+
message: 'Enter your GitHub PAT (with read:packages permission):',
|
|
225
|
+
placeholder: 'ghp_...',
|
|
226
|
+
validate(value) {
|
|
227
|
+
if (!value.trim()) return 'Token is required to download private skills.';
|
|
228
|
+
},
|
|
229
|
+
}),
|
|
230
|
+
);
|
|
231
|
+
return token.trim();
|
|
232
|
+
}
|