kodu 2.1.2 → 2.2.0
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/AGENTS.md +23 -1
- package/__tests__/core/registry/registry.service.test.ts +82 -0
- package/__tests__/shared/runbook/runbook.service.test.ts +104 -0
- package/dist/package.json +1 -1
- package/dist/src/app.module.js +6 -0
- package/dist/src/app.module.js.map +1 -1
- package/dist/src/commands/init/init.command.d.ts +1 -0
- package/dist/src/commands/init/init.command.js +34 -1
- package/dist/src/commands/init/init.command.js.map +1 -1
- package/dist/src/commands/ops/ops-add.command.d.ts +18 -0
- package/dist/src/commands/ops/ops-add.command.js +102 -0
- package/dist/src/commands/ops/ops-add.command.js.map +1 -0
- package/dist/src/commands/ops/ops-init.command.d.ts +22 -0
- package/dist/src/commands/ops/ops-init.command.js +130 -0
- package/dist/src/commands/ops/ops-init.command.js.map +1 -0
- package/dist/src/commands/ops/ops-list.command.d.ts +12 -0
- package/dist/src/commands/ops/ops-list.command.js +73 -0
- package/dist/src/commands/ops/ops-list.command.js.map +1 -0
- package/dist/src/commands/ops/ops-path.command.d.ts +9 -0
- package/dist/src/commands/ops/ops-path.command.js +52 -0
- package/dist/src/commands/ops/ops-path.command.js.map +1 -0
- package/dist/src/commands/ops/ops-runbook.command.d.ts +12 -0
- package/dist/src/commands/ops/ops-runbook.command.js +81 -0
- package/dist/src/commands/ops/ops-runbook.command.js.map +1 -0
- package/dist/src/commands/ops/ops-status.command.d.ts +11 -0
- package/dist/src/commands/ops/ops-status.command.js +62 -0
- package/dist/src/commands/ops/ops-status.command.js.map +1 -0
- package/dist/src/commands/ops/ops-use.command.d.ts +12 -0
- package/dist/src/commands/ops/ops-use.command.js +76 -0
- package/dist/src/commands/ops/ops-use.command.js.map +1 -0
- package/dist/src/commands/ops/ops.command.d.ts +7 -0
- package/dist/src/commands/ops/ops.command.js +56 -0
- package/dist/src/commands/ops/ops.command.js.map +1 -0
- package/dist/src/commands/ops/ops.helpers.d.ts +2 -0
- package/dist/src/commands/ops/ops.helpers.js +11 -0
- package/dist/src/commands/ops/ops.helpers.js.map +1 -0
- package/dist/src/commands/ops/ops.module.d.ts +2 -0
- package/dist/src/commands/ops/ops.module.js +36 -0
- package/dist/src/commands/ops/ops.module.js.map +1 -0
- package/dist/src/core/registry/registry.module.d.ts +2 -0
- package/dist/src/core/registry/registry.module.js +22 -0
- package/dist/src/core/registry/registry.module.js.map +1 -0
- package/dist/src/core/registry/registry.schema.d.ts +24 -0
- package/dist/src/core/registry/registry.schema.js +21 -0
- package/dist/src/core/registry/registry.schema.js.map +1 -0
- package/dist/src/core/registry/registry.service.d.ts +16 -0
- package/dist/src/core/registry/registry.service.js +91 -0
- package/dist/src/core/registry/registry.service.js.map +1 -0
- package/dist/src/shared/runbook/runbook.module.d.ts +2 -0
- package/dist/src/shared/runbook/runbook.module.js +22 -0
- package/dist/src/shared/runbook/runbook.module.js.map +1 -0
- package/dist/src/shared/runbook/runbook.service.d.ts +20 -0
- package/dist/src/shared/runbook/runbook.service.js +118 -0
- package/dist/src/shared/runbook/runbook.service.js.map +1 -0
- package/dist/src/shared/runbook/runbook.templates.d.ts +6 -0
- package/dist/src/shared/runbook/runbook.templates.js +49 -0
- package/dist/src/shared/runbook/runbook.templates.js.map +1 -0
- package/dist/tsconfig.build.tsbuildinfo +1 -1
- package/package.json +1 -1
- package/registry.schema.json +39 -0
- package/scripts/generate-json-schema.ts +14 -5
- package/skills/ac/SKILL.md +239 -0
- package/skills/al/SKILL.md +98 -0
- package/skills/audit/SKILL.md +205 -0
- package/skills/audit/audit-baseline-template.yml +188 -0
- package/skills/audit/runtime-detect.md +64 -0
- package/skills/audit/stacks/_generic.md +41 -0
- package/skills/audit/stacks/_registry.md +47 -0
- package/skills/audit/stacks/go.md +66 -0
- package/skills/audit/stacks/java.md +44 -0
- package/skills/audit/stacks/node.md +57 -0
- package/skills/audit/stacks/python.md +45 -0
- package/skills/audit/stacks/rust.md +44 -0
- package/skills/audit-api-contracts/SKILL.md +201 -0
- package/skills/audit-architecture/SKILL.md +200 -0
- package/skills/audit-bugs/SKILL.md +226 -0
- package/skills/audit-concurrency/SKILL.md +197 -0
- package/skills/audit-deployment/SKILL.md +218 -0
- package/skills/audit-docs/SKILL.md +209 -0
- package/skills/audit-errors/SKILL.md +216 -0
- package/skills/audit-logging/SKILL.md +197 -0
- package/skills/audit-matrix/SKILL.md +245 -0
- package/skills/audit-meta/SKILL.md +120 -0
- package/skills/audit-naming/SKILL.md +200 -0
- package/skills/audit-owasp/SKILL.md +223 -0
- package/skills/audit-performance/SKILL.md +199 -0
- package/skills/audit-reinvention/SKILL.md +214 -0
- package/skills/audit-secrets/SKILL.md +198 -0
- package/skills/audit-tests/SKILL.md +210 -0
- package/skills/audit-validation/SKILL.md +206 -0
- package/skills/audit-verify/SKILL.md +139 -0
- package/skills/audit-yagni/SKILL.md +188 -0
- package/skills/doc-gen/SKILL.md +490 -0
- package/skills/doc-gen/scripts/doc_gen.py +911 -0
- package/skills/generate-project-docs/SKILL.md +380 -0
- package/skills/implement-project/SKILL.md +409 -0
- package/skills/litefront-prototype/SKILL.md +484 -0
- package/skills/ops/SKILL.md +94 -0
- package/skills/post-call-task-builder/SKILL.md +419 -0
- package/skills/skills-best-practices/SKILL.md +415 -0
- package/skills/start/SKILL.md +319 -0
- package/skills/tech-blueprint/SKILL.md +890 -0
- package/skills/tech-blueprint/scripts/blueprint_validator.py +417 -0
- package/src/app.module.ts +6 -0
- package/src/commands/init/init.command.ts +43 -1
- package/src/commands/ops/ops-add.command.ts +83 -0
- package/src/commands/ops/ops-init.command.ts +125 -0
- package/src/commands/ops/ops-list.command.ts +57 -0
- package/src/commands/ops/ops-path.command.ts +38 -0
- package/src/commands/ops/ops-runbook.command.ts +74 -0
- package/src/commands/ops/ops-status.command.ts +47 -0
- package/src/commands/ops/ops-use.command.ts +76 -0
- package/src/commands/ops/ops.command.ts +42 -0
- package/src/commands/ops/ops.helpers.ts +20 -0
- package/src/commands/ops/ops.module.ts +23 -0
- package/src/core/registry/registry.module.ts +9 -0
- package/src/core/registry/registry.schema.ts +46 -0
- package/src/core/registry/registry.service.ts +128 -0
- package/src/shared/runbook/runbook.module.ts +9 -0
- package/src/shared/runbook/runbook.service.ts +164 -0
- package/src/shared/runbook/runbook.templates.ts +66 -0
- package/dist/tsconfig.tsbuildinfo +0 -1
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import { promises as fs } from 'node:fs';
|
|
2
|
+
import os from 'node:os';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { Injectable } from '@nestjs/common';
|
|
5
|
+
import {
|
|
6
|
+
type ProjectEntry,
|
|
7
|
+
type Registry,
|
|
8
|
+
registrySchema,
|
|
9
|
+
} from './registry.schema';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Читает и пишет глобальный реестр проектов `~/.config/kodu/registry.json`
|
|
13
|
+
* (учитывает `$XDG_CONFIG_HOME`). Файл создаётся при первой записи — пока
|
|
14
|
+
* проектов нет, ничего в системе не создаётся.
|
|
15
|
+
*/
|
|
16
|
+
@Injectable()
|
|
17
|
+
export class RegistryService {
|
|
18
|
+
private readonly dir = path.join(
|
|
19
|
+
process.env.XDG_CONFIG_HOME?.trim() || path.join(os.homedir(), '.config'),
|
|
20
|
+
'kodu',
|
|
21
|
+
);
|
|
22
|
+
private readonly file = path.join(this.dir, 'registry.json');
|
|
23
|
+
|
|
24
|
+
/** Путь к файлу реестра (для подсказок пользователю). */
|
|
25
|
+
getFilePath(): string {
|
|
26
|
+
return this.file;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** Загрузить реестр. Если файла ещё нет — вернуть пустой реестр. */
|
|
30
|
+
async load(): Promise<Registry> {
|
|
31
|
+
let raw: unknown;
|
|
32
|
+
|
|
33
|
+
try {
|
|
34
|
+
const content = await fs.readFile(this.file, 'utf8');
|
|
35
|
+
raw = JSON.parse(content);
|
|
36
|
+
} catch (error) {
|
|
37
|
+
if ((error as NodeJS.ErrnoException).code === 'ENOENT') {
|
|
38
|
+
return registrySchema.parse({});
|
|
39
|
+
}
|
|
40
|
+
throw new Error(
|
|
41
|
+
`Не удалось прочитать реестр ${this.file}: ${(error as Error).message}`,
|
|
42
|
+
);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const parsed = registrySchema.safeParse(raw);
|
|
46
|
+
|
|
47
|
+
if (!parsed.success) {
|
|
48
|
+
const issues = parsed.error.issues
|
|
49
|
+
.map(
|
|
50
|
+
(issue) => `- ${issue.path.join('.') || '(root)'}: ${issue.message}`,
|
|
51
|
+
)
|
|
52
|
+
.join('\n');
|
|
53
|
+
throw new Error(`Реестр ${this.file} невалиден:\n${issues}`);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
return parsed.data;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** Сохранить реестр на диск (создаёт директорию при необходимости). */
|
|
60
|
+
async save(registry: Registry): Promise<void> {
|
|
61
|
+
const validated = registrySchema.parse(registry);
|
|
62
|
+
await fs.mkdir(this.dir, { recursive: true });
|
|
63
|
+
await fs.writeFile(
|
|
64
|
+
this.file,
|
|
65
|
+
`${JSON.stringify(validated, null, 2)}\n`,
|
|
66
|
+
'utf8',
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
async list(): Promise<Registry['projects']> {
|
|
71
|
+
return (await this.load()).projects;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
async get(name: string): Promise<ProjectEntry | undefined> {
|
|
75
|
+
return (await this.load()).projects[name];
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
async has(name: string): Promise<boolean> {
|
|
79
|
+
return Boolean(await this.get(name));
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/** Добавить проект. По умолчанию запрещает перезапись существующего имени. */
|
|
83
|
+
async add(
|
|
84
|
+
name: string,
|
|
85
|
+
entry: ProjectEntry,
|
|
86
|
+
options: { overwrite?: boolean } = {},
|
|
87
|
+
): Promise<void> {
|
|
88
|
+
const registry = await this.load();
|
|
89
|
+
|
|
90
|
+
if (registry.projects[name] && !options.overwrite) {
|
|
91
|
+
throw new Error(
|
|
92
|
+
`Проект с именем "${name}" уже есть в реестре. Выбери другое имя или обнови существующий проект.`,
|
|
93
|
+
);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
registry.projects[name] = entry;
|
|
97
|
+
await this.save(registry);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/** Обновить поля существующего проекта. */
|
|
101
|
+
async update(
|
|
102
|
+
name: string,
|
|
103
|
+
patch: Partial<ProjectEntry>,
|
|
104
|
+
): Promise<ProjectEntry> {
|
|
105
|
+
const registry = await this.load();
|
|
106
|
+
const existing = registry.projects[name];
|
|
107
|
+
|
|
108
|
+
if (!existing) {
|
|
109
|
+
throw new Error(`Проект "${name}" не найден в реестре.`);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const next: ProjectEntry = { ...existing, ...patch };
|
|
113
|
+
registry.projects[name] = next;
|
|
114
|
+
await this.save(registry);
|
|
115
|
+
return next;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
async remove(name: string): Promise<void> {
|
|
119
|
+
const registry = await this.load();
|
|
120
|
+
|
|
121
|
+
if (!registry.projects[name]) {
|
|
122
|
+
throw new Error(`Проект "${name}" не найден в реестре.`);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
delete registry.projects[name];
|
|
126
|
+
await this.save(registry);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
import { promises as fs } from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { Injectable } from '@nestjs/common';
|
|
4
|
+
import {
|
|
5
|
+
type ProjectConfig,
|
|
6
|
+
projectConfigSchema,
|
|
7
|
+
} from '../../core/registry/registry.schema';
|
|
8
|
+
import { type DetectedStack, renderRunbook } from './runbook.templates';
|
|
9
|
+
|
|
10
|
+
const RUNBOOK_DIR = '.runbook';
|
|
11
|
+
const GITIGNORE_ENTRY = '/.runbook/';
|
|
12
|
+
/** Эквивалентные записи `.runbook/` в `.gitignore` — считаем дублем. */
|
|
13
|
+
const GITIGNORE_EQUIVALENTS = new Set([
|
|
14
|
+
'/.runbook/',
|
|
15
|
+
'/.runbook',
|
|
16
|
+
'.runbook/',
|
|
17
|
+
'.runbook',
|
|
18
|
+
]);
|
|
19
|
+
|
|
20
|
+
/** Результат настройки `.gitignore`. */
|
|
21
|
+
export type GitignoreResult = 'created' | 'added' | 'present' | 'no-git';
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Работа с per-project директорией `.runbook/`: скаффолд, чтение/запись
|
|
25
|
+
* `config.json`, чтение `runbook.md` и идемпотентная настройка `.gitignore`.
|
|
26
|
+
*/
|
|
27
|
+
@Injectable()
|
|
28
|
+
export class RunbookService {
|
|
29
|
+
dirPath(root: string = process.cwd()): string {
|
|
30
|
+
return path.join(root, RUNBOOK_DIR);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
configPath(root: string = process.cwd()): string {
|
|
34
|
+
return path.join(root, RUNBOOK_DIR, 'config.json');
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
runbookPath(root: string = process.cwd()): string {
|
|
38
|
+
return path.join(root, RUNBOOK_DIR, 'runbook.md');
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** Инициализирован ли проект (есть `.runbook/config.json`). */
|
|
42
|
+
async exists(root: string = process.cwd()): Promise<boolean> {
|
|
43
|
+
return this.pathExists(this.configPath(root));
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
async readConfig(root: string = process.cwd()): Promise<ProjectConfig> {
|
|
47
|
+
const content = await fs.readFile(this.configPath(root), 'utf8');
|
|
48
|
+
return projectConfigSchema.parse(JSON.parse(content));
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
async writeConfig(
|
|
52
|
+
config: ProjectConfig,
|
|
53
|
+
root: string = process.cwd(),
|
|
54
|
+
): Promise<void> {
|
|
55
|
+
const validated = projectConfigSchema.parse(config);
|
|
56
|
+
await fs.mkdir(this.dirPath(root), { recursive: true });
|
|
57
|
+
await fs.writeFile(
|
|
58
|
+
this.configPath(root),
|
|
59
|
+
`${JSON.stringify(validated, null, 2)}\n`,
|
|
60
|
+
'utf8',
|
|
61
|
+
);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
async readRunbook(root: string = process.cwd()): Promise<string> {
|
|
65
|
+
return fs.readFile(this.runbookPath(root), 'utf8');
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Создаёт `.runbook/` с config.json и (если ещё нет) runbook.md.
|
|
70
|
+
* Существующий runbook.md не перезаписывается — ручные правки сохраняются.
|
|
71
|
+
*/
|
|
72
|
+
async scaffold(
|
|
73
|
+
options: { project: string; stands: string[]; activeStand: string },
|
|
74
|
+
root: string = process.cwd(),
|
|
75
|
+
): Promise<void> {
|
|
76
|
+
await fs.mkdir(this.dirPath(root), { recursive: true });
|
|
77
|
+
|
|
78
|
+
await this.writeConfig(
|
|
79
|
+
{
|
|
80
|
+
project: options.project,
|
|
81
|
+
activeStand: options.activeStand,
|
|
82
|
+
stands: options.stands,
|
|
83
|
+
},
|
|
84
|
+
root,
|
|
85
|
+
);
|
|
86
|
+
|
|
87
|
+
if (!(await this.pathExists(this.runbookPath(root)))) {
|
|
88
|
+
const detected = await this.detectStack(root);
|
|
89
|
+
const markdown = renderRunbook(options.project, options.stands, detected);
|
|
90
|
+
await fs.writeFile(this.runbookPath(root), markdown, 'utf8');
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/** Определяет наличие docker-compose / Dockerfile / .env.example. */
|
|
95
|
+
async detectStack(root: string = process.cwd()): Promise<DetectedStack> {
|
|
96
|
+
const [composeYml, composeYaml, dockerfile, envExample] = await Promise.all(
|
|
97
|
+
[
|
|
98
|
+
this.pathExists(path.join(root, 'docker-compose.yml')),
|
|
99
|
+
this.pathExists(path.join(root, 'docker-compose.yaml')),
|
|
100
|
+
this.pathExists(path.join(root, 'Dockerfile')),
|
|
101
|
+
this.pathExists(path.join(root, '.env.example')),
|
|
102
|
+
],
|
|
103
|
+
);
|
|
104
|
+
|
|
105
|
+
return {
|
|
106
|
+
compose: composeYml || composeYaml,
|
|
107
|
+
dockerfile,
|
|
108
|
+
envExample,
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Гарантирует, что `.runbook/` игнорируется git — автоматически и идемпотентно.
|
|
114
|
+
* - не git-репозиторий → ничего не пишем, возвращаем 'no-git';
|
|
115
|
+
* - запись уже есть → 'present';
|
|
116
|
+
* - `.gitignore` отсутствует → создаём с записью → 'created';
|
|
117
|
+
* - иначе дописываем в конец → 'added'.
|
|
118
|
+
*/
|
|
119
|
+
async ensureGitignore(
|
|
120
|
+
root: string = process.cwd(),
|
|
121
|
+
): Promise<GitignoreResult> {
|
|
122
|
+
if (!(await this.pathExists(path.join(root, '.git')))) {
|
|
123
|
+
return 'no-git';
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const gitignorePath = path.join(root, '.gitignore');
|
|
127
|
+
let content = '';
|
|
128
|
+
let fileExists = true;
|
|
129
|
+
|
|
130
|
+
try {
|
|
131
|
+
content = await fs.readFile(gitignorePath, 'utf8');
|
|
132
|
+
} catch {
|
|
133
|
+
fileExists = false;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const alreadyIgnored = content
|
|
137
|
+
.split(/\r?\n/)
|
|
138
|
+
.map((line) => line.trim())
|
|
139
|
+
.some((line) => GITIGNORE_EQUIVALENTS.has(line));
|
|
140
|
+
|
|
141
|
+
if (alreadyIgnored) {
|
|
142
|
+
return 'present';
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
const trimmed = content.trimEnd();
|
|
146
|
+
const next =
|
|
147
|
+
trimmed.length > 0
|
|
148
|
+
? `${trimmed}\n${GITIGNORE_ENTRY}\n`
|
|
149
|
+
: `${GITIGNORE_ENTRY}\n`;
|
|
150
|
+
|
|
151
|
+
await fs.writeFile(gitignorePath, next, 'utf8');
|
|
152
|
+
|
|
153
|
+
return fileExists ? 'added' : 'created';
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
private async pathExists(target: string): Promise<boolean> {
|
|
157
|
+
try {
|
|
158
|
+
await fs.access(target);
|
|
159
|
+
return true;
|
|
160
|
+
} catch {
|
|
161
|
+
return false;
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/** Что удалось определить про проект (для подсказок в runbook). */
|
|
2
|
+
export type DetectedStack = {
|
|
3
|
+
compose: boolean;
|
|
4
|
+
dockerfile: boolean;
|
|
5
|
+
envExample: boolean;
|
|
6
|
+
};
|
|
7
|
+
|
|
8
|
+
const STAND_TITLES: Record<string, string> = {
|
|
9
|
+
local: 'локальная разработка',
|
|
10
|
+
dev: 'dev-стенд',
|
|
11
|
+
stage: 'stage-стенд',
|
|
12
|
+
prod: 'production (осторожно!)',
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
function renderStandSection(stand: string, detected: DetectedStack): string {
|
|
16
|
+
const title = STAND_TITLES[stand] ?? stand;
|
|
17
|
+
|
|
18
|
+
const startCmd = detected.compose
|
|
19
|
+
? 'docker compose up -d'
|
|
20
|
+
: '<команда запуска, напр. docker compose up -d>';
|
|
21
|
+
const logsCmd = detected.compose
|
|
22
|
+
? 'docker compose logs -f'
|
|
23
|
+
: '<команда логов>';
|
|
24
|
+
const envNote = detected.envExample
|
|
25
|
+
? 'Скопируй `.env.example` → `.env` и заполни значения.'
|
|
26
|
+
: '<откуда взять переменные окружения / секреты>';
|
|
27
|
+
|
|
28
|
+
return [
|
|
29
|
+
`## Стенд: ${stand} (${title})`,
|
|
30
|
+
'',
|
|
31
|
+
'- **Где живёт / доступ**: <ssh user@host или localhost>',
|
|
32
|
+
'- **Рабочая директория**: <путь на сервере или локально>',
|
|
33
|
+
'- **Получить код**: `git clone <repo>` (первый раз) / `git pull` (обновить)',
|
|
34
|
+
`- **Запуск**: \`${startCmd}\``,
|
|
35
|
+
`- **Логи**: \`${logsCmd}\``,
|
|
36
|
+
'- **Деплой**: <пошаговые команды деплоя на этот стенд>',
|
|
37
|
+
'- **Откат**: <как откатиться, если что-то пошло не так>',
|
|
38
|
+
`- **Переменные окружения / секреты**: ${envNote}`,
|
|
39
|
+
'',
|
|
40
|
+
].join('\n');
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Стартовый runbook для проекта. Заполняется человеком/агентом — конкретные
|
|
45
|
+
* команды и пути подставляются под реальную инфраструктуру.
|
|
46
|
+
*/
|
|
47
|
+
export function renderRunbook(
|
|
48
|
+
project: string,
|
|
49
|
+
stands: string[],
|
|
50
|
+
detected: DetectedStack,
|
|
51
|
+
): string {
|
|
52
|
+
const header = [
|
|
53
|
+
`# Runbook: ${project}`,
|
|
54
|
+
'',
|
|
55
|
+
'> Этот файл описывает, как работать со стендами проекта.',
|
|
56
|
+
'> Он лежит в `.gitignore` и не коммитится — здесь могут быть хосты и пути.',
|
|
57
|
+
'> Заполни плейсхолдеры `<...>` под свою инфраструктуру.',
|
|
58
|
+
'',
|
|
59
|
+
].join('\n');
|
|
60
|
+
|
|
61
|
+
const sections = stands
|
|
62
|
+
.map((stand) => renderStandSection(stand, detected))
|
|
63
|
+
.join('\n');
|
|
64
|
+
|
|
65
|
+
return `${header}\n${sections}`;
|
|
66
|
+
}
|