kodu 2.1.3 → 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.
Files changed (109) hide show
  1. package/__tests__/core/registry/registry.service.test.ts +82 -0
  2. package/__tests__/shared/runbook/runbook.service.test.ts +104 -0
  3. package/dist/package.json +1 -1
  4. package/dist/src/app.module.js +6 -0
  5. package/dist/src/app.module.js.map +1 -1
  6. package/dist/src/commands/ops/ops-add.command.d.ts +18 -0
  7. package/dist/src/commands/ops/ops-add.command.js +102 -0
  8. package/dist/src/commands/ops/ops-add.command.js.map +1 -0
  9. package/dist/src/commands/ops/ops-init.command.d.ts +22 -0
  10. package/dist/src/commands/ops/ops-init.command.js +130 -0
  11. package/dist/src/commands/ops/ops-init.command.js.map +1 -0
  12. package/dist/src/commands/ops/ops-list.command.d.ts +12 -0
  13. package/dist/src/commands/ops/ops-list.command.js +73 -0
  14. package/dist/src/commands/ops/ops-list.command.js.map +1 -0
  15. package/dist/src/commands/ops/ops-path.command.d.ts +9 -0
  16. package/dist/src/commands/ops/ops-path.command.js +52 -0
  17. package/dist/src/commands/ops/ops-path.command.js.map +1 -0
  18. package/dist/src/commands/ops/ops-runbook.command.d.ts +12 -0
  19. package/dist/src/commands/ops/ops-runbook.command.js +81 -0
  20. package/dist/src/commands/ops/ops-runbook.command.js.map +1 -0
  21. package/dist/src/commands/ops/ops-status.command.d.ts +11 -0
  22. package/dist/src/commands/ops/ops-status.command.js +62 -0
  23. package/dist/src/commands/ops/ops-status.command.js.map +1 -0
  24. package/dist/src/commands/ops/ops-use.command.d.ts +12 -0
  25. package/dist/src/commands/ops/ops-use.command.js +76 -0
  26. package/dist/src/commands/ops/ops-use.command.js.map +1 -0
  27. package/dist/src/commands/ops/ops.command.d.ts +7 -0
  28. package/dist/src/commands/ops/ops.command.js +56 -0
  29. package/dist/src/commands/ops/ops.command.js.map +1 -0
  30. package/dist/src/commands/ops/ops.helpers.d.ts +2 -0
  31. package/dist/src/commands/ops/ops.helpers.js +11 -0
  32. package/dist/src/commands/ops/ops.helpers.js.map +1 -0
  33. package/dist/src/commands/ops/ops.module.d.ts +2 -0
  34. package/dist/src/commands/ops/ops.module.js +36 -0
  35. package/dist/src/commands/ops/ops.module.js.map +1 -0
  36. package/dist/src/core/registry/registry.module.d.ts +2 -0
  37. package/dist/src/core/registry/registry.module.js +22 -0
  38. package/dist/src/core/registry/registry.module.js.map +1 -0
  39. package/dist/src/core/registry/registry.schema.d.ts +24 -0
  40. package/dist/src/core/registry/registry.schema.js +21 -0
  41. package/dist/src/core/registry/registry.schema.js.map +1 -0
  42. package/dist/src/core/registry/registry.service.d.ts +16 -0
  43. package/dist/src/core/registry/registry.service.js +91 -0
  44. package/dist/src/core/registry/registry.service.js.map +1 -0
  45. package/dist/src/shared/runbook/runbook.module.d.ts +2 -0
  46. package/dist/src/shared/runbook/runbook.module.js +22 -0
  47. package/dist/src/shared/runbook/runbook.module.js.map +1 -0
  48. package/dist/src/shared/runbook/runbook.service.d.ts +20 -0
  49. package/dist/src/shared/runbook/runbook.service.js +118 -0
  50. package/dist/src/shared/runbook/runbook.service.js.map +1 -0
  51. package/dist/src/shared/runbook/runbook.templates.d.ts +6 -0
  52. package/dist/src/shared/runbook/runbook.templates.js +49 -0
  53. package/dist/src/shared/runbook/runbook.templates.js.map +1 -0
  54. package/dist/tsconfig.build.tsbuildinfo +1 -1
  55. package/package.json +1 -1
  56. package/registry.schema.json +39 -0
  57. package/scripts/generate-json-schema.ts +14 -5
  58. package/skills/ac/SKILL.md +239 -0
  59. package/skills/al/SKILL.md +98 -0
  60. package/skills/audit/SKILL.md +205 -0
  61. package/skills/audit/audit-baseline-template.yml +188 -0
  62. package/skills/audit/runtime-detect.md +64 -0
  63. package/skills/audit/stacks/_generic.md +41 -0
  64. package/skills/audit/stacks/_registry.md +47 -0
  65. package/skills/audit/stacks/go.md +66 -0
  66. package/skills/audit/stacks/java.md +44 -0
  67. package/skills/audit/stacks/node.md +57 -0
  68. package/skills/audit/stacks/python.md +45 -0
  69. package/skills/audit/stacks/rust.md +44 -0
  70. package/skills/audit-api-contracts/SKILL.md +201 -0
  71. package/skills/audit-architecture/SKILL.md +200 -0
  72. package/skills/audit-bugs/SKILL.md +226 -0
  73. package/skills/audit-concurrency/SKILL.md +197 -0
  74. package/skills/audit-deployment/SKILL.md +218 -0
  75. package/skills/audit-docs/SKILL.md +209 -0
  76. package/skills/audit-errors/SKILL.md +216 -0
  77. package/skills/audit-logging/SKILL.md +197 -0
  78. package/skills/audit-matrix/SKILL.md +245 -0
  79. package/skills/audit-meta/SKILL.md +120 -0
  80. package/skills/audit-naming/SKILL.md +200 -0
  81. package/skills/audit-owasp/SKILL.md +223 -0
  82. package/skills/audit-performance/SKILL.md +199 -0
  83. package/skills/audit-reinvention/SKILL.md +214 -0
  84. package/skills/audit-secrets/SKILL.md +198 -0
  85. package/skills/audit-tests/SKILL.md +210 -0
  86. package/skills/audit-validation/SKILL.md +206 -0
  87. package/skills/audit-verify/SKILL.md +139 -0
  88. package/skills/audit-yagni/SKILL.md +188 -0
  89. package/skills/generate-project-docs/SKILL.md +380 -0
  90. package/skills/ops/SKILL.md +94 -0
  91. package/skills/post-call-task-builder/SKILL.md +419 -0
  92. package/skills/skills-best-practices/SKILL.md +415 -0
  93. package/src/app.module.ts +6 -0
  94. package/src/commands/ops/ops-add.command.ts +83 -0
  95. package/src/commands/ops/ops-init.command.ts +125 -0
  96. package/src/commands/ops/ops-list.command.ts +57 -0
  97. package/src/commands/ops/ops-path.command.ts +38 -0
  98. package/src/commands/ops/ops-runbook.command.ts +74 -0
  99. package/src/commands/ops/ops-status.command.ts +47 -0
  100. package/src/commands/ops/ops-use.command.ts +76 -0
  101. package/src/commands/ops/ops.command.ts +42 -0
  102. package/src/commands/ops/ops.helpers.ts +20 -0
  103. package/src/commands/ops/ops.module.ts +23 -0
  104. package/src/core/registry/registry.module.ts +9 -0
  105. package/src/core/registry/registry.schema.ts +46 -0
  106. package/src/core/registry/registry.service.ts +128 -0
  107. package/src/shared/runbook/runbook.module.ts +9 -0
  108. package/src/shared/runbook/runbook.service.ts +164 -0
  109. package/src/shared/runbook/runbook.templates.ts +66 -0
@@ -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,9 @@
1
+ import { Global, Module } from '@nestjs/common';
2
+ import { RegistryService } from './registry.service';
3
+
4
+ @Global()
5
+ @Module({
6
+ providers: [RegistryService],
7
+ exports: [RegistryService],
8
+ })
9
+ export class RegistryModule {}
@@ -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>;
@@ -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,9 @@
1
+ import { Global, Module } from '@nestjs/common';
2
+ import { RunbookService } from './runbook.service';
3
+
4
+ @Global()
5
+ @Module({
6
+ providers: [RunbookService],
7
+ exports: [RunbookService],
8
+ })
9
+ export class RunbookModule {}