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,125 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import { CommandRunner, Option, SubCommand } from 'nest-commander';
|
|
3
|
+
import { DEFAULT_STANDS } from '../../core/registry/registry.schema';
|
|
4
|
+
import { RegistryService } from '../../core/registry/registry.service';
|
|
5
|
+
import { UiService } from '../../core/ui/ui.service';
|
|
6
|
+
import { RunbookService } from '../../shared/runbook/runbook.service';
|
|
7
|
+
|
|
8
|
+
type InitOptions = {
|
|
9
|
+
name?: string;
|
|
10
|
+
// Свойства называются по длинным флагам `--active` / `--stand`.
|
|
11
|
+
active?: string;
|
|
12
|
+
stand?: string[];
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
@SubCommand({
|
|
16
|
+
name: 'init',
|
|
17
|
+
description:
|
|
18
|
+
'Настроить стенды в текущем проекте: создать .runbook/, .gitignore и зарегистрировать проект',
|
|
19
|
+
})
|
|
20
|
+
export class OpsInitCommand extends CommandRunner {
|
|
21
|
+
constructor(
|
|
22
|
+
private readonly ui: UiService,
|
|
23
|
+
private readonly registry: RegistryService,
|
|
24
|
+
private readonly runbook: RunbookService,
|
|
25
|
+
) {
|
|
26
|
+
super();
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
@Option({
|
|
30
|
+
flags: '-n, --name <name>',
|
|
31
|
+
description: 'Уникальное имя проекта (по умолчанию — имя папки)',
|
|
32
|
+
})
|
|
33
|
+
parseName(value: string): string {
|
|
34
|
+
return value;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
@Option({
|
|
38
|
+
flags: '-a, --active <stand>',
|
|
39
|
+
description: 'Активный стенд по умолчанию (по умолчанию local)',
|
|
40
|
+
})
|
|
41
|
+
parseActive(value: string): string {
|
|
42
|
+
return value;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
@Option({
|
|
46
|
+
flags: '-s, --stand <stand>',
|
|
47
|
+
description: 'Стенд проекта (можно повторять)',
|
|
48
|
+
})
|
|
49
|
+
parseStand(value: string, previous: string[] = []): string[] {
|
|
50
|
+
return [...previous, value];
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
async run(_inputs: string[], options: InitOptions): Promise<void> {
|
|
54
|
+
const root = process.cwd();
|
|
55
|
+
const name = options.name ?? path.basename(root);
|
|
56
|
+
const stands =
|
|
57
|
+
options.stand && options.stand.length > 0
|
|
58
|
+
? options.stand
|
|
59
|
+
: [...DEFAULT_STANDS];
|
|
60
|
+
const activeStand = options.active ?? stands[0] ?? 'local';
|
|
61
|
+
|
|
62
|
+
try {
|
|
63
|
+
// 1. Скаффолд .runbook/ (config.json + runbook.md).
|
|
64
|
+
await this.runbook.scaffold({ project: name, stands, activeStand }, root);
|
|
65
|
+
this.ui.log.success(`Создан .runbook/ для проекта "${name}".`);
|
|
66
|
+
this.ui.log.info(`Активный стенд: ${activeStand}`);
|
|
67
|
+
this.ui.log.info(
|
|
68
|
+
`Заполни шаги деплоя в ${this.runbook.runbookPath(root)}`,
|
|
69
|
+
);
|
|
70
|
+
|
|
71
|
+
// 2. Гарантируем игнор .runbook/ — автоматически.
|
|
72
|
+
const gitignore = await this.runbook.ensureGitignore(root);
|
|
73
|
+
this.reportGitignore(gitignore);
|
|
74
|
+
|
|
75
|
+
// 3. Регистрируем проект в глобальном реестре (имя уникально).
|
|
76
|
+
await this.registerProject(name, root, stands);
|
|
77
|
+
} catch (error) {
|
|
78
|
+
this.ui.log.error((error as Error).message);
|
|
79
|
+
process.exitCode = 1;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
private reportGitignore(result: string): void {
|
|
84
|
+
switch (result) {
|
|
85
|
+
case 'created':
|
|
86
|
+
this.ui.log.success('Создан .gitignore с записью /.runbook/');
|
|
87
|
+
break;
|
|
88
|
+
case 'added':
|
|
89
|
+
this.ui.log.success('Добавил /.runbook/ в .gitignore');
|
|
90
|
+
break;
|
|
91
|
+
case 'present':
|
|
92
|
+
this.ui.log.info('/.runbook/ уже в .gitignore');
|
|
93
|
+
break;
|
|
94
|
+
case 'no-git':
|
|
95
|
+
this.ui.log.warn(
|
|
96
|
+
'Это не git-репозиторий — .gitignore не настроен. Не коммить .runbook/ вручную.',
|
|
97
|
+
);
|
|
98
|
+
break;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
private async registerProject(
|
|
103
|
+
name: string,
|
|
104
|
+
root: string,
|
|
105
|
+
stands: string[],
|
|
106
|
+
): Promise<void> {
|
|
107
|
+
const existing = await this.registry.get(name);
|
|
108
|
+
|
|
109
|
+
if (!existing) {
|
|
110
|
+
await this.registry.add(name, { path: root, stands });
|
|
111
|
+
this.ui.log.success(`Проект "${name}" добавлен в реестр.`);
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
if (existing.path === root) {
|
|
116
|
+
this.ui.log.info(`Проект "${name}" уже в реестре.`);
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
this.ui.log.warn(
|
|
121
|
+
`Имя "${name}" уже занято другим путём (${existing.path}). ` +
|
|
122
|
+
'Запусти заново с другим именем: kodu ops init --name <другое-имя>',
|
|
123
|
+
);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { CommandRunner, SubCommand } from 'nest-commander';
|
|
2
|
+
import { RegistryService } from '../../core/registry/registry.service';
|
|
3
|
+
import { UiService } from '../../core/ui/ui.service';
|
|
4
|
+
import { RunbookService } from '../../shared/runbook/runbook.service';
|
|
5
|
+
|
|
6
|
+
@SubCommand({
|
|
7
|
+
name: 'list',
|
|
8
|
+
aliases: ['ls'],
|
|
9
|
+
description: 'Показать все проекты из реестра и их активные стенды',
|
|
10
|
+
})
|
|
11
|
+
export class OpsListCommand extends CommandRunner {
|
|
12
|
+
constructor(
|
|
13
|
+
private readonly ui: UiService,
|
|
14
|
+
private readonly registry: RegistryService,
|
|
15
|
+
private readonly runbook: RunbookService,
|
|
16
|
+
) {
|
|
17
|
+
super();
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
async run(): Promise<void> {
|
|
21
|
+
try {
|
|
22
|
+
const projects = await this.registry.list();
|
|
23
|
+
const names = Object.keys(projects).sort((a, b) => a.localeCompare(b));
|
|
24
|
+
|
|
25
|
+
if (names.length === 0) {
|
|
26
|
+
this.ui.log.info(
|
|
27
|
+
'Реестр пуст. Добавь проект: kodu ops add <name> --path <dir>',
|
|
28
|
+
);
|
|
29
|
+
this.ui.log.info(`Файл реестра: ${this.registry.getFilePath()}`);
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
for (const name of names) {
|
|
34
|
+
const entry = projects[name];
|
|
35
|
+
const active = await this.readActiveStand(entry.path);
|
|
36
|
+
const activeLabel = active ? ` [активный: ${active}]` : '';
|
|
37
|
+
this.ui.log.info(`${name}${activeLabel}`);
|
|
38
|
+
this.ui.log.info(` путь: ${entry.path}`);
|
|
39
|
+
this.ui.log.info(` стенды: ${entry.stands.join(', ')}`);
|
|
40
|
+
}
|
|
41
|
+
} catch (error) {
|
|
42
|
+
this.ui.log.error((error as Error).message);
|
|
43
|
+
process.exitCode = 1;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
private async readActiveStand(root: string): Promise<string | undefined> {
|
|
48
|
+
try {
|
|
49
|
+
if (!(await this.runbook.exists(root))) {
|
|
50
|
+
return undefined;
|
|
51
|
+
}
|
|
52
|
+
return (await this.runbook.readConfig(root)).activeStand;
|
|
53
|
+
} catch {
|
|
54
|
+
return undefined;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { CommandRunner, SubCommand } from 'nest-commander';
|
|
2
|
+
import { RegistryService } from '../../core/registry/registry.service';
|
|
3
|
+
import { UiService } from '../../core/ui/ui.service';
|
|
4
|
+
import { resolveProjectRoot } from './ops.helpers';
|
|
5
|
+
|
|
6
|
+
@SubCommand({
|
|
7
|
+
name: 'path',
|
|
8
|
+
description:
|
|
9
|
+
'Напечатать путь к репозиторию проекта (удобно для cd $(kodu ops path <name>))',
|
|
10
|
+
arguments: '<name>',
|
|
11
|
+
})
|
|
12
|
+
export class OpsPathCommand extends CommandRunner {
|
|
13
|
+
constructor(
|
|
14
|
+
private readonly ui: UiService,
|
|
15
|
+
private readonly registry: RegistryService,
|
|
16
|
+
) {
|
|
17
|
+
super();
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
async run(inputs: string[]): Promise<void> {
|
|
21
|
+
const name = inputs[0];
|
|
22
|
+
|
|
23
|
+
if (!name) {
|
|
24
|
+
this.ui.log.error('Укажи имя проекта: kodu ops path <name>');
|
|
25
|
+
process.exitCode = 1;
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
try {
|
|
30
|
+
const root = await resolveProjectRoot(this.registry, name);
|
|
31
|
+
// Чистый вывод пути в stdout — чтобы работало в подстановке команды.
|
|
32
|
+
process.stdout.write(`${root}\n`);
|
|
33
|
+
} catch (error) {
|
|
34
|
+
this.ui.log.error((error as Error).message);
|
|
35
|
+
process.exitCode = 1;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { CommandRunner, SubCommand } from 'nest-commander';
|
|
2
|
+
import { RegistryService } from '../../core/registry/registry.service';
|
|
3
|
+
import { UiService } from '../../core/ui/ui.service';
|
|
4
|
+
import { RunbookService } from '../../shared/runbook/runbook.service';
|
|
5
|
+
import { resolveProjectRoot } from './ops.helpers';
|
|
6
|
+
|
|
7
|
+
@SubCommand({
|
|
8
|
+
name: 'runbook',
|
|
9
|
+
description: 'Напечатать runbook проекта (или секцию конкретного стенда)',
|
|
10
|
+
arguments: '<name> [stand]',
|
|
11
|
+
})
|
|
12
|
+
export class OpsRunbookCommand extends CommandRunner {
|
|
13
|
+
constructor(
|
|
14
|
+
private readonly ui: UiService,
|
|
15
|
+
private readonly registry: RegistryService,
|
|
16
|
+
private readonly runbook: RunbookService,
|
|
17
|
+
) {
|
|
18
|
+
super();
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
async run(inputs: string[]): Promise<void> {
|
|
22
|
+
const name = inputs[0];
|
|
23
|
+
const stand = inputs[1];
|
|
24
|
+
|
|
25
|
+
if (!name) {
|
|
26
|
+
this.ui.log.error('Укажи имя проекта: kodu ops runbook <name> [stand]');
|
|
27
|
+
process.exitCode = 1;
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
try {
|
|
32
|
+
const root = await resolveProjectRoot(this.registry, name);
|
|
33
|
+
|
|
34
|
+
if (!(await this.runbook.exists(root))) {
|
|
35
|
+
this.ui.log.warn(
|
|
36
|
+
`В проекте "${name}" нет .runbook/. Запусти: kodu ops init`,
|
|
37
|
+
);
|
|
38
|
+
process.exitCode = 1;
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const markdown = await this.runbook.readRunbook(root);
|
|
43
|
+
const output = stand ? this.extractStand(markdown, stand) : markdown;
|
|
44
|
+
|
|
45
|
+
if (!output) {
|
|
46
|
+
this.ui.log.warn(`Секция для стенда "${stand}" не найдена в runbook.`);
|
|
47
|
+
process.exitCode = 1;
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
process.stdout.write(`${output.trimEnd()}\n`);
|
|
52
|
+
} catch (error) {
|
|
53
|
+
this.ui.log.error((error as Error).message);
|
|
54
|
+
process.exitCode = 1;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** Возвращает блок `## Стенд: <stand> ...` до следующего заголовка `## `. */
|
|
59
|
+
private extractStand(markdown: string, stand: string): string | undefined {
|
|
60
|
+
const lines = markdown.split(/\r?\n/);
|
|
61
|
+
const startPrefix = `## Стенд: ${stand}`;
|
|
62
|
+
const start = lines.findIndex((line) => line.startsWith(startPrefix));
|
|
63
|
+
|
|
64
|
+
if (start === -1) {
|
|
65
|
+
return undefined;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const rest = lines.slice(start + 1);
|
|
69
|
+
const nextHeading = rest.findIndex((line) => line.startsWith('## '));
|
|
70
|
+
const end = nextHeading === -1 ? lines.length : start + 1 + nextHeading;
|
|
71
|
+
|
|
72
|
+
return lines.slice(start, end).join('\n');
|
|
73
|
+
}
|
|
74
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { CommandRunner, SubCommand } from 'nest-commander';
|
|
2
|
+
import { RegistryService } from '../../core/registry/registry.service';
|
|
3
|
+
import { UiService } from '../../core/ui/ui.service';
|
|
4
|
+
import { RunbookService } from '../../shared/runbook/runbook.service';
|
|
5
|
+
import { resolveProjectRoot } from './ops.helpers';
|
|
6
|
+
|
|
7
|
+
@SubCommand({
|
|
8
|
+
name: 'status',
|
|
9
|
+
description:
|
|
10
|
+
'Показать активный стенд и стенды проекта (по имени или в текущей папке)',
|
|
11
|
+
arguments: '[name]',
|
|
12
|
+
})
|
|
13
|
+
export class OpsStatusCommand extends CommandRunner {
|
|
14
|
+
constructor(
|
|
15
|
+
private readonly ui: UiService,
|
|
16
|
+
private readonly registry: RegistryService,
|
|
17
|
+
private readonly runbook: RunbookService,
|
|
18
|
+
) {
|
|
19
|
+
super();
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
async run(inputs: string[]): Promise<void> {
|
|
23
|
+
try {
|
|
24
|
+
const name = inputs[0];
|
|
25
|
+
const root = name
|
|
26
|
+
? await resolveProjectRoot(this.registry, name)
|
|
27
|
+
: process.cwd();
|
|
28
|
+
|
|
29
|
+
if (!(await this.runbook.exists(root))) {
|
|
30
|
+
this.ui.log.warn(
|
|
31
|
+
`В ${root} нет .runbook/. Инициализируй проект: kodu ops init`,
|
|
32
|
+
);
|
|
33
|
+
process.exitCode = 1;
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const config = await this.runbook.readConfig(root);
|
|
38
|
+
this.ui.log.info(`Проект: ${config.project}`);
|
|
39
|
+
this.ui.log.info(`Активный стенд: ${config.activeStand}`);
|
|
40
|
+
this.ui.log.info(`Стенды: ${config.stands.join(', ')}`);
|
|
41
|
+
this.ui.log.info(`Путь: ${root}`);
|
|
42
|
+
} catch (error) {
|
|
43
|
+
this.ui.log.error((error as Error).message);
|
|
44
|
+
process.exitCode = 1;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { CommandRunner, SubCommand } from 'nest-commander';
|
|
2
|
+
import { RegistryService } from '../../core/registry/registry.service';
|
|
3
|
+
import { UiService } from '../../core/ui/ui.service';
|
|
4
|
+
import { RunbookService } from '../../shared/runbook/runbook.service';
|
|
5
|
+
import { resolveProjectRoot } from './ops.helpers';
|
|
6
|
+
|
|
7
|
+
@SubCommand({
|
|
8
|
+
name: 'use',
|
|
9
|
+
aliases: ['switch'],
|
|
10
|
+
description:
|
|
11
|
+
'Переключить активный стенд: "kodu ops use <stand>" в текущем проекте или "kodu ops use <name> <stand>"',
|
|
12
|
+
arguments: '<args...>',
|
|
13
|
+
})
|
|
14
|
+
export class OpsUseCommand extends CommandRunner {
|
|
15
|
+
constructor(
|
|
16
|
+
private readonly ui: UiService,
|
|
17
|
+
private readonly registry: RegistryService,
|
|
18
|
+
private readonly runbook: RunbookService,
|
|
19
|
+
) {
|
|
20
|
+
super();
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
async run(inputs: string[]): Promise<void> {
|
|
24
|
+
try {
|
|
25
|
+
const { root, stand } = await this.resolveTarget(inputs);
|
|
26
|
+
|
|
27
|
+
if (!stand) {
|
|
28
|
+
this.ui.log.error(
|
|
29
|
+
'Укажи стенд: kodu ops use <stand> или kodu ops use <name> <stand>',
|
|
30
|
+
);
|
|
31
|
+
process.exitCode = 1;
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
if (!(await this.runbook.exists(root))) {
|
|
36
|
+
this.ui.log.warn(
|
|
37
|
+
`В ${root} нет .runbook/. Инициализируй проект: kodu ops init`,
|
|
38
|
+
);
|
|
39
|
+
process.exitCode = 1;
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const config = await this.runbook.readConfig(root);
|
|
44
|
+
const stands = config.stands.includes(stand)
|
|
45
|
+
? config.stands
|
|
46
|
+
: [...config.stands, stand];
|
|
47
|
+
|
|
48
|
+
if (!config.stands.includes(stand)) {
|
|
49
|
+
this.ui.log.info(`Стенд "${stand}" добавлен в список стендов проекта.`);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
await this.runbook.writeConfig(
|
|
53
|
+
{ ...config, activeStand: stand, stands },
|
|
54
|
+
root,
|
|
55
|
+
);
|
|
56
|
+
this.ui.log.success(
|
|
57
|
+
`Активный стенд проекта "${config.project}" → ${stand}`,
|
|
58
|
+
);
|
|
59
|
+
} catch (error) {
|
|
60
|
+
this.ui.log.error((error as Error).message);
|
|
61
|
+
process.exitCode = 1;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** 1 аргумент → стенд в текущей папке; 2 аргумента → <name> <stand>. */
|
|
66
|
+
private async resolveTarget(
|
|
67
|
+
inputs: string[],
|
|
68
|
+
): Promise<{ root: string; stand?: string }> {
|
|
69
|
+
if (inputs.length >= 2) {
|
|
70
|
+
const root = await resolveProjectRoot(this.registry, inputs[0]);
|
|
71
|
+
return { root, stand: inputs[1] };
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
return { root: process.cwd(), stand: inputs[0] };
|
|
75
|
+
}
|
|
76
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { Command, CommandRunner } from 'nest-commander';
|
|
2
|
+
import { UiService } from '../../core/ui/ui.service';
|
|
3
|
+
import { OpsAddCommand } from './ops-add.command';
|
|
4
|
+
import { OpsInitCommand } from './ops-init.command';
|
|
5
|
+
import { OpsListCommand } from './ops-list.command';
|
|
6
|
+
import { OpsPathCommand } from './ops-path.command';
|
|
7
|
+
import { OpsRunbookCommand } from './ops-runbook.command';
|
|
8
|
+
import { OpsStatusCommand } from './ops-status.command';
|
|
9
|
+
import { OpsUseCommand } from './ops-use.command';
|
|
10
|
+
|
|
11
|
+
@Command({
|
|
12
|
+
name: 'ops',
|
|
13
|
+
description:
|
|
14
|
+
'Работа с проектами и стендами (local/dev/stage/prod) из любого места',
|
|
15
|
+
subCommands: [
|
|
16
|
+
OpsInitCommand,
|
|
17
|
+
OpsListCommand,
|
|
18
|
+
OpsAddCommand,
|
|
19
|
+
OpsStatusCommand,
|
|
20
|
+
OpsUseCommand,
|
|
21
|
+
OpsPathCommand,
|
|
22
|
+
OpsRunbookCommand,
|
|
23
|
+
],
|
|
24
|
+
})
|
|
25
|
+
export class OpsCommand extends CommandRunner {
|
|
26
|
+
constructor(private readonly ui: UiService) {
|
|
27
|
+
super();
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
async run(): Promise<void> {
|
|
31
|
+
this.ui.log.info('Использование: kodu ops <команда>');
|
|
32
|
+
this.ui.log.info(
|
|
33
|
+
' init — настроить стенды в текущем проекте',
|
|
34
|
+
);
|
|
35
|
+
this.ui.log.info(' list — список всех проектов');
|
|
36
|
+
this.ui.log.info(' add <name> --path <d> — зарегистрировать проект');
|
|
37
|
+
this.ui.log.info(' status [name] — активный стенд проекта');
|
|
38
|
+
this.ui.log.info(' use <stand> — переключить активный стенд');
|
|
39
|
+
this.ui.log.info(' path <name> — путь к репозиторию проекта');
|
|
40
|
+
this.ui.log.info(' runbook <name> [stand]— показать инструкции по стенду');
|
|
41
|
+
}
|
|
42
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { RegistryService } from '../../core/registry/registry.service';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Возвращает путь к репозиторию проекта по его имени из реестра.
|
|
5
|
+
* Бросает понятную ошибку, если проект не найден.
|
|
6
|
+
*/
|
|
7
|
+
export async function resolveProjectRoot(
|
|
8
|
+
registry: RegistryService,
|
|
9
|
+
name: string,
|
|
10
|
+
): Promise<string> {
|
|
11
|
+
const entry = await registry.get(name);
|
|
12
|
+
|
|
13
|
+
if (!entry) {
|
|
14
|
+
throw new Error(
|
|
15
|
+
`Проект "${name}" не найден в реестре. Список проектов: kodu ops list`,
|
|
16
|
+
);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
return entry.path;
|
|
20
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { Module } from '@nestjs/common';
|
|
2
|
+
import { OpsCommand } from './ops.command';
|
|
3
|
+
import { OpsAddCommand } from './ops-add.command';
|
|
4
|
+
import { OpsInitCommand } from './ops-init.command';
|
|
5
|
+
import { OpsListCommand } from './ops-list.command';
|
|
6
|
+
import { OpsPathCommand } from './ops-path.command';
|
|
7
|
+
import { OpsRunbookCommand } from './ops-runbook.command';
|
|
8
|
+
import { OpsStatusCommand } from './ops-status.command';
|
|
9
|
+
import { OpsUseCommand } from './ops-use.command';
|
|
10
|
+
|
|
11
|
+
@Module({
|
|
12
|
+
providers: [
|
|
13
|
+
OpsCommand,
|
|
14
|
+
OpsInitCommand,
|
|
15
|
+
OpsListCommand,
|
|
16
|
+
OpsAddCommand,
|
|
17
|
+
OpsStatusCommand,
|
|
18
|
+
OpsUseCommand,
|
|
19
|
+
OpsPathCommand,
|
|
20
|
+
OpsRunbookCommand,
|
|
21
|
+
],
|
|
22
|
+
})
|
|
23
|
+
export class OpsModule {}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Стандартный набор стендов. Можно использовать и любые другие имена —
|
|
5
|
+
* стенды хранятся как обычные строки, это лишь значение по умолчанию.
|
|
6
|
+
*/
|
|
7
|
+
export const DEFAULT_STANDS = ['local', 'dev', 'stage', 'prod'] as const;
|
|
8
|
+
|
|
9
|
+
/** Один проект в глобальном реестре `~/.config/kodu/registry.json`. */
|
|
10
|
+
export const projectEntrySchema = z.object({
|
|
11
|
+
/** Абсолютный путь к репозиторию проекта на этой машине. */
|
|
12
|
+
path: z.string().min(1),
|
|
13
|
+
/** URL репозитория (git clone), необязательно. */
|
|
14
|
+
repo: z.string().optional(),
|
|
15
|
+
/** Доступные стенды проекта. */
|
|
16
|
+
stands: z.array(z.string()).default([...DEFAULT_STANDS]),
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
export type ProjectEntry = z.infer<typeof projectEntrySchema>;
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Глобальный реестр всех проектов. Ключ объекта `projects` — уникальное имя
|
|
23
|
+
* проекта, по которому агент/CLI понимает, с каким репозиторием работать.
|
|
24
|
+
*/
|
|
25
|
+
export const registrySchema = z.object({
|
|
26
|
+
$schema: z.string().optional(),
|
|
27
|
+
projects: z.record(z.string(), projectEntrySchema).default({}),
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
export type Registry = z.infer<typeof registrySchema>;
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Per-project конфиг `.runbook/config.json` (лежит в `.gitignore`).
|
|
34
|
+
* Хранит текущий активный стенд конкретного разработчика.
|
|
35
|
+
*/
|
|
36
|
+
export const projectConfigSchema = z.object({
|
|
37
|
+
$schema: z.string().optional(),
|
|
38
|
+
/** Имя проекта — должно совпадать с ключом в глобальном реестре. */
|
|
39
|
+
project: z.string().min(1),
|
|
40
|
+
/** Текущий активный стенд. */
|
|
41
|
+
activeStand: z.string().default('local'),
|
|
42
|
+
/** Доступные стенды проекта. */
|
|
43
|
+
stands: z.array(z.string()).default([...DEFAULT_STANDS]),
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
export type ProjectConfig = z.infer<typeof projectConfigSchema>;
|