lv24-opencode-consilium 0.0.0-stage → 1.4.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 (106) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +499 -3
  3. package/README.ru.md +500 -0
  4. package/README.zh.md +188 -0
  5. package/bin/cli.js +19 -0
  6. package/bin/mcp.js +24 -0
  7. package/consilium.config.example.json +84 -0
  8. package/dist/agents.d.ts +79 -0
  9. package/dist/agents.d.ts.map +1 -0
  10. package/dist/agents.js +1051 -0
  11. package/dist/agents.js.map +1 -0
  12. package/dist/artifacts.d.ts +75 -0
  13. package/dist/artifacts.d.ts.map +1 -0
  14. package/dist/artifacts.js +464 -0
  15. package/dist/artifacts.js.map +1 -0
  16. package/dist/cli.d.ts +12 -0
  17. package/dist/cli.d.ts.map +1 -0
  18. package/dist/cli.js +1392 -0
  19. package/dist/cli.js.map +1 -0
  20. package/dist/compat.d.ts +160 -0
  21. package/dist/compat.d.ts.map +1 -0
  22. package/dist/compat.js +419 -0
  23. package/dist/compat.js.map +1 -0
  24. package/dist/config.d.ts +112 -0
  25. package/dist/config.d.ts.map +1 -0
  26. package/dist/config.js +728 -0
  27. package/dist/config.js.map +1 -0
  28. package/dist/consensus.d.ts +73 -0
  29. package/dist/consensus.d.ts.map +1 -0
  30. package/dist/consensus.js +273 -0
  31. package/dist/consensus.js.map +1 -0
  32. package/dist/defaults.d.ts +34 -0
  33. package/dist/defaults.d.ts.map +1 -0
  34. package/dist/defaults.js +102 -0
  35. package/dist/defaults.js.map +1 -0
  36. package/dist/doctor.d.ts +27 -0
  37. package/dist/doctor.d.ts.map +1 -0
  38. package/dist/doctor.js +798 -0
  39. package/dist/doctor.js.map +1 -0
  40. package/dist/fallback.d.ts +50 -0
  41. package/dist/fallback.d.ts.map +1 -0
  42. package/dist/fallback.js +134 -0
  43. package/dist/fallback.js.map +1 -0
  44. package/dist/git.d.ts +28 -0
  45. package/dist/git.d.ts.map +1 -0
  46. package/dist/git.js +71 -0
  47. package/dist/git.js.map +1 -0
  48. package/dist/index.d.ts +100 -0
  49. package/dist/index.d.ts.map +1 -0
  50. package/dist/index.js +653 -0
  51. package/dist/index.js.map +1 -0
  52. package/dist/mcp.d.ts +20 -0
  53. package/dist/mcp.d.ts.map +1 -0
  54. package/dist/mcp.js +1215 -0
  55. package/dist/mcp.js.map +1 -0
  56. package/dist/models.d.ts +97 -0
  57. package/dist/models.d.ts.map +1 -0
  58. package/dist/models.js +873 -0
  59. package/dist/models.js.map +1 -0
  60. package/dist/plugin.d.ts +59 -0
  61. package/dist/plugin.d.ts.map +1 -0
  62. package/dist/plugin.js +223 -0
  63. package/dist/plugin.js.map +1 -0
  64. package/dist/presets.d.ts +27 -0
  65. package/dist/presets.d.ts.map +1 -0
  66. package/dist/presets.js +86 -0
  67. package/dist/presets.js.map +1 -0
  68. package/dist/replay.d.ts +37 -0
  69. package/dist/replay.d.ts.map +1 -0
  70. package/dist/replay.js +103 -0
  71. package/dist/replay.js.map +1 -0
  72. package/dist/report.d.ts +28 -0
  73. package/dist/report.d.ts.map +1 -0
  74. package/dist/report.js +300 -0
  75. package/dist/report.js.map +1 -0
  76. package/dist/smart-experts.d.ts +44 -0
  77. package/dist/smart-experts.d.ts.map +1 -0
  78. package/dist/smart-experts.js +242 -0
  79. package/dist/smart-experts.js.map +1 -0
  80. package/dist/stream.d.ts +50 -0
  81. package/dist/stream.d.ts.map +1 -0
  82. package/dist/stream.js +219 -0
  83. package/dist/stream.js.map +1 -0
  84. package/dist/tool-run.d.ts +49 -0
  85. package/dist/tool-run.d.ts.map +1 -0
  86. package/dist/tool-run.js +143 -0
  87. package/dist/tool-run.js.map +1 -0
  88. package/dist/types.d.ts +352 -0
  89. package/dist/types.d.ts.map +1 -0
  90. package/dist/types.js +6 -0
  91. package/dist/types.js.map +1 -0
  92. package/dist/variants.d.ts +30 -0
  93. package/dist/variants.d.ts.map +1 -0
  94. package/dist/variants.js +111 -0
  95. package/dist/variants.js.map +1 -0
  96. package/dist/verify.d.ts +33 -0
  97. package/dist/verify.d.ts.map +1 -0
  98. package/dist/verify.js +80 -0
  99. package/dist/verify.js.map +1 -0
  100. package/dist/watch.d.ts +73 -0
  101. package/dist/watch.d.ts.map +1 -0
  102. package/dist/watch.js +350 -0
  103. package/dist/watch.js.map +1 -0
  104. package/docs/session-and-plugin.md +642 -0
  105. package/package.json +90 -6
  106. package/schema/config.schema.json +100 -0
@@ -0,0 +1,111 @@
1
+ /**
2
+ * @fileoverview Варианты моделей OpenCode (`provider/model#variant`).
3
+ * @module lv24-opencode-consilium
4
+ *
5
+ * Ссылка на модель: `provider/model` с необязательным `#variant`.
6
+ * Провайдер заканчивается на первом `/`; в имени модели `/` допустим;
7
+ * вариант следует за первым `#`; в `provider/model` символа `#` нет.
8
+ */
9
+ import { splitModelRef } from './compat.js';
10
+ /** Допустимый вариант модели: латиница/цифры, далее латиница/цифры/`._-` */
11
+ export const VARIANT_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._-]*$/;
12
+ /** Проверяет вариант (с триммингом); пустой/некорректный — `false` */
13
+ export function isValidVariant(variant) {
14
+ const trimmed = variant.trim();
15
+ if (trimmed.length === 0)
16
+ return false;
17
+ return VARIANT_PATTERN.test(trimmed);
18
+ }
19
+ /** Возвращает часть после первого `#` (пустая — `undefined`); без `#` — `undefined` */
20
+ export function variantOf(ref) {
21
+ return splitModelRef(ref).variant;
22
+ }
23
+ /** Убирает `#...` с конца; остальной текст сохраняет как есть (без трима) */
24
+ export function stripVariant(ref) {
25
+ return splitModelRef(ref).model;
26
+ }
27
+ /** Проверяет базу `provider/model`: непустая и содержит `/` */
28
+ function assertValidBase(base, ref) {
29
+ if (base.length === 0 || !base.includes('/')) {
30
+ throw new Error(`Некорректная модель: ${ref}`);
31
+ }
32
+ }
33
+ /**
34
+ * Ставит вариант на ссылку: `undefined`/`''` — снять вариант,
35
+ * иначе заменить/добавить `#variant`. База обязана содержать `/`.
36
+ */
37
+ export function withVariant(ref, variant) {
38
+ if (variant === undefined || variant === '') {
39
+ const base = stripVariant(ref);
40
+ assertValidBase(base, ref);
41
+ return base;
42
+ }
43
+ const trimmed = variant.trim();
44
+ if (!isValidVariant(trimmed)) {
45
+ throw new Error(`Некорректный вариант модели: ${variant}`);
46
+ }
47
+ const base = stripVariant(ref);
48
+ assertValidBase(base, ref);
49
+ return `${base}#${trimmed}`;
50
+ }
51
+ /**
52
+ * Применяет вариант ко всем моделям: у кого вариант уже есть — оставить,
53
+ * корректным `provider/model` без варианта — добавить `#variant`,
54
+ * некорректные id — оставить без изменений. Новый объект, вход не мутирует.
55
+ */
56
+ export function applyVariant(models, variant) {
57
+ const trimmed = variant.trim();
58
+ if (!isValidVariant(trimmed)) {
59
+ throw new Error(`Некорректный вариант модели: ${variant}`);
60
+ }
61
+ const out = {};
62
+ for (const [key, value] of Object.entries(models)) {
63
+ if (typeof value !== 'string') {
64
+ out[key] = value;
65
+ continue;
66
+ }
67
+ if (value.trim().length === 0) {
68
+ out[key] = value;
69
+ continue;
70
+ }
71
+ if (variantOf(value) !== undefined) {
72
+ out[key] = value;
73
+ continue;
74
+ }
75
+ const base = stripVariant(value);
76
+ if (base.length === 0 || !base.includes('/')) {
77
+ out[key] = value;
78
+ continue;
79
+ }
80
+ out[key] = `${base}#${trimmed}`;
81
+ }
82
+ return out;
83
+ }
84
+ /** То же для массива; дедупликация с сохранением порядка */
85
+ export function applyVariantList(models, variant) {
86
+ const trimmed = variant.trim();
87
+ if (!isValidVariant(trimmed)) {
88
+ throw new Error(`Некорректный вариант модели: ${variant}`);
89
+ }
90
+ const seen = new Set();
91
+ const out = [];
92
+ for (const ref of models) {
93
+ let next;
94
+ if (typeof ref !== 'string' || ref.trim().length === 0) {
95
+ next = ref;
96
+ }
97
+ else if (variantOf(ref) !== undefined) {
98
+ next = ref;
99
+ }
100
+ else {
101
+ const base = stripVariant(ref);
102
+ next = base.length === 0 || !base.includes('/') ? ref : `${base}#${trimmed}`;
103
+ }
104
+ if (!seen.has(next)) {
105
+ seen.add(next);
106
+ out.push(next);
107
+ }
108
+ }
109
+ return out;
110
+ }
111
+ //# sourceMappingURL=variants.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"variants.js","sourceRoot":"","sources":["../src/variants.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,33 @@
1
+ /**
2
+ * @fileoverview Проверка доступности бесплатных моделей реальным вызовом
3
+ * @module lv24-opencode-consilium
4
+ */
5
+ import type { FreeModel } from './models.js';
6
+ /** Результат проверки одной модели */
7
+ export interface ModelVerification {
8
+ model: FreeModel;
9
+ ok: boolean;
10
+ ms: number;
11
+ error?: string;
12
+ }
13
+ export interface VerifyModelsOptions {
14
+ concurrency?: number;
15
+ timeoutMs?: number;
16
+ cwd?: string;
17
+ opencodePath?: string;
18
+ prompt?: string;
19
+ }
20
+ /**
21
+ * Проверяет модели коротким реальным запросом через OpenCode.
22
+ * Возвращает результаты в исходном порядке.
23
+ */
24
+ export declare function verifyModels(models: FreeModel[], options?: VerifyModelsOptions): Promise<ModelVerification[]>;
25
+ /**
26
+ * Проверяет кандидатов и возвращает только рабочие модели (в порядке рейтинга).
27
+ */
28
+ export declare function filterWorkingModels(models: FreeModel[], options?: VerifyModelsOptions): Promise<{
29
+ working: FreeModel[];
30
+ failed: ModelVerification[];
31
+ checked: ModelVerification[];
32
+ }>;
33
+ //# sourceMappingURL=verify.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"verify.d.ts","sourceRoot":"","sources":["../src/verify.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAKH,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AAE7C,sCAAsC;AACtC,MAAM,WAAW,iBAAiB;IAChC,KAAK,EAAE,SAAS,CAAC;IACjB,EAAE,EAAE,OAAO,CAAC;IACZ,EAAE,EAAE,MAAM,CAAC;IACX,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,WAAW,mBAAmB;IAClC,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAOD;;;GAGG;AACH,wBAAsB,YAAY,CAChC,MAAM,EAAE,SAAS,EAAE,EACnB,OAAO,GAAE,mBAAwB,GAChC,OAAO,CAAC,iBAAiB,EAAE,CAAC,CAsD9B;AAED;;GAEG;AACH,wBAAsB,mBAAmB,CACvC,MAAM,EAAE,SAAS,EAAE,EACnB,OAAO,GAAE,mBAAwB,GAChC,OAAO,CAAC;IAAE,OAAO,EAAE,SAAS,EAAE,CAAC;IAAC,MAAM,EAAE,iBAAiB,EAAE,CAAC;IAAC,OAAO,EAAE,iBAAiB,EAAE,CAAA;CAAE,CAAC,CAS9F"}
package/dist/verify.js ADDED
@@ -0,0 +1,80 @@
1
+ /**
2
+ * @fileoverview Проверка доступности бесплатных моделей реальным вызовом
3
+ * @module lv24-opencode-consilium
4
+ */
5
+ import { callAgent } from './agents.js';
6
+ import { mergeWithDefaults } from './config.js';
7
+ function clampConcurrency(value) {
8
+ if (value === undefined || !Number.isFinite(value))
9
+ return 4;
10
+ return Math.max(1, Math.min(8, Math.floor(value)));
11
+ }
12
+ /**
13
+ * Проверяет модели коротким реальным запросом через OpenCode.
14
+ * Возвращает результаты в исходном порядке.
15
+ */
16
+ export async function verifyModels(models, options = {}) {
17
+ if (models.length === 0)
18
+ return [];
19
+ const concurrency = clampConcurrency(options.concurrency);
20
+ const timeoutMs = options.timeoutMs !== undefined && Number.isFinite(options.timeoutMs) && options.timeoutMs > 0
21
+ ? options.timeoutMs
22
+ : 60000;
23
+ const prompt = options.prompt ?? 'Ответь ровно одним словом: ок. Не вызывай инструменты.';
24
+ const config = mergeWithDefaults({
25
+ cwd: options.cwd,
26
+ opencodePath: options.opencodePath,
27
+ standalone: true,
28
+ timeouts: { expert: timeoutMs, chair: timeoutMs, shutdown: 1000 },
29
+ retry: {
30
+ expertRetries: 0,
31
+ chairRetries: 0,
32
+ delay: 0,
33
+ exponentialBackoff: false,
34
+ maxConcurrency: concurrency
35
+ }
36
+ });
37
+ const results = new Array(models.length);
38
+ let nextIndex = 0;
39
+ const worker = async () => {
40
+ for (;;) {
41
+ const index = nextIndex;
42
+ nextIndex += 1;
43
+ if (index >= models.length)
44
+ return;
45
+ const model = models[index];
46
+ const result = await callAgent('', prompt, config, {
47
+ timeout: timeoutMs,
48
+ retries: 0,
49
+ model: model.id
50
+ });
51
+ results[index] = {
52
+ model,
53
+ ok: result.success,
54
+ ms: result.duration ?? 0,
55
+ error: result.success
56
+ ? undefined
57
+ : result.errorMessage ?? result.errorCode ?? 'нет ответа'
58
+ };
59
+ }
60
+ };
61
+ const workerCount = Math.min(concurrency, models.length);
62
+ await Promise.all(Array.from({ length: workerCount }, () => worker()));
63
+ return results;
64
+ }
65
+ /**
66
+ * Проверяет кандидатов и возвращает только рабочие модели (в порядке рейтинга).
67
+ */
68
+ export async function filterWorkingModels(models, options = {}) {
69
+ const checks = await verifyModels(models, options);
70
+ const working = [];
71
+ const failed = [];
72
+ for (const check of checks) {
73
+ if (check.ok)
74
+ working.push(check.model);
75
+ else
76
+ failed.push(check);
77
+ }
78
+ return { working, failed, checked: checks };
79
+ }
80
+ //# sourceMappingURL=verify.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"verify.js","sourceRoot":"","sources":["../src/verify.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AACxC,OAAO,EAAE,iBAAiB,EAAE,MAAM,aAAa,CAAC;AAoBhD,SAAS,gBAAgB,CAAC,KAAyB;IACjD,IAAI,KAAK,KAAK,SAAS,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,KAAK,CAAC;QAAE,OAAO,CAAC,CAAC;IAC7D,OAAO,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,IAAI,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC;AACrD,CAAC;AAED;;;GAGG;AACH,MAAM,CAAC,KAAK,UAAU,YAAY,CAChC,MAAmB,EACnB,UAA+B,EAAE;IAEjC,IAAI,MAAM,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,EAAE,CAAC;IAEnC,MAAM,WAAW,GAAG,gBAAgB,CAAC,OAAO,CAAC,WAAW,CAAC,CAAC;IAC1D,MAAM,SAAS,GACb,OAAO,CAAC,SAAS,KAAK,SAAS,IAAI,MAAM,CAAC,QAAQ,CAAC,OAAO,CAAC,SAAS,CAAC,IAAI,OAAO,CAAC,SAAS,GAAG,CAAC;QAC5F,CAAC,CAAC,OAAO,CAAC,SAAS;QACnB,CAAC,CAAC,KAAK,CAAC;IACZ,MAAM,MAAM,GAAG,OAAO,CAAC,MAAM,IAAI,wDAAwD,CAAC;IAE1F,MAAM,MAAM,GAAoB,iBAAiB,CAAC;QAChD,GAAG,EAAE,OAAO,CAAC,GAAG;QAChB,YAAY,EAAE,OAAO,CAAC,YAAY;QAClC,UAAU,EAAE,IAAI;QAChB,QAAQ,EAAE,EAAE,MAAM,EAAE,SAAS,EAAE,KAAK,EAAE,SAAS,EAAE,QAAQ,EAAE,IAAI,EAAE;QACjE,KAAK,EAAE;YACL,aAAa,EAAE,CAAC;YAChB,YAAY,EAAE,CAAC;YACf,KAAK,EAAE,CAAC;YACR,kBAAkB,EAAE,KAAK;YACzB,cAAc,EAAE,WAAW;SAC5B;KACF,CAAC,CAAC;IAEH,MAAM,OAAO,GAAwB,IAAI,KAAK,CAAoB,MAAM,CAAC,MAAM,CAAC,CAAC;IACjF,IAAI,SAAS,GAAG,CAAC,CAAC;IAElB,MAAM,MAAM,GAAG,KAAK,IAAmB,EAAE;QACvC,SAAS,CAAC;YACR,MAAM,KAAK,GAAG,SAAS,CAAC;YACxB,SAAS,IAAI,CAAC,CAAC;YACf,IAAI,KAAK,IAAI,MAAM,CAAC,MAAM;gBAAE,OAAO;YAEnC,MAAM,KAAK,GAAG,MAAM,CAAC,KAAK,CAAC,CAAC;YAC5B,MAAM,MAAM,GAAG,MAAM,SAAS,CAAC,EAAE,EAAE,MAAM,EAAE,MAAM,EAAE;gBACjD,OAAO,EAAE,SAAS;gBAClB,OAAO,EAAE,CAAC;gBACV,KAAK,EAAE,KAAK,CAAC,EAAE;aAChB,CAAC,CAAC;YAEH,OAAO,CAAC,KAAK,CAAC,GAAG;gBACf,KAAK;gBACL,EAAE,EAAE,MAAM,CAAC,OAAO;gBAClB,EAAE,EAAE,MAAM,CAAC,QAAQ,IAAI,CAAC;gBACxB,KAAK,EAAE,MAAM,CAAC,OAAO;oBACnB,CAAC,CAAC,SAAS;oBACX,CAAC,CAAC,MAAM,CAAC,YAAY,IAAI,MAAM,CAAC,SAAS,IAAI,YAAY;aAC5D,CAAC;QACJ,CAAC;IACH,CAAC,CAAC;IAEF,MAAM,WAAW,GAAG,IAAI,CAAC,GAAG,CAAC,WAAW,EAAE,MAAM,CAAC,MAAM,CAAC,CAAC;IACzD,MAAM,OAAO,CAAC,GAAG,CAAC,KAAK,CAAC,IAAI,CAAC,EAAE,MAAM,EAAE,WAAW,EAAE,EAAE,GAAG,EAAE,CAAC,MAAM,EAAE,CAAC,CAAC,CAAC;IACvE,OAAO,OAAO,CAAC;AACjB,CAAC;AAED;;GAEG;AACH,MAAM,CAAC,KAAK,UAAU,mBAAmB,CACvC,MAAmB,EACnB,UAA+B,EAAE;IAEjC,MAAM,MAAM,GAAG,MAAM,YAAY,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IACnD,MAAM,OAAO,GAAgB,EAAE,CAAC;IAChC,MAAM,MAAM,GAAwB,EAAE,CAAC;IACvC,KAAK,MAAM,KAAK,IAAI,MAAM,EAAE,CAAC;QAC3B,IAAI,KAAK,CAAC,EAAE;YAAE,OAAO,CAAC,IAAI,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC;;YACnC,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;IAC1B,CAAC;IACD,OAAO,EAAE,OAAO,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,CAAC;AAC9C,CAAC"}
@@ -0,0 +1,73 @@
1
+ /**
2
+ * @fileoverview Watch mode (debounced fs.watch) and git pre-commit hook helpers.
3
+ * @module lv24-opencode-consilium
4
+ */
5
+ /** Опции createProjectWatcher */
6
+ export interface WatchOptions {
7
+ cwd?: string;
8
+ debounceMs?: number;
9
+ ignore?: string[];
10
+ onTrigger: (paths: string[]) => void | Promise<void>;
11
+ }
12
+ /** Игнорируемые пути/глобы по умолчанию (всегда применяются вместе с пользовательскими) */
13
+ export declare const DEFAULT_IGNORE_PATTERNS: readonly string[];
14
+ /**
15
+ * Проверяет, попадает ли путь под ignore-паттерны.
16
+ * Плоские имена (`.git`, `dist`) матчатся как полный сегмент пути —
17
+ * поэтому работают и для абсолютных путей внутри игнорируемых директорий.
18
+ * Паттерны со `*` (`*.tmp`) матчатся против всего пути и basename.
19
+ * Паттерны со `/` матчатся как префикс/подпуть.
20
+ */
21
+ export declare function shouldIgnore(relativePath: string, ignore?: string[]): boolean;
22
+ /**
23
+ * Создаёт дебаунсер: собирает пути в Set, сбрасывает таймер на каждый push,
24
+ * вызывает fn один раз с отсортированным массивом. flush срабатывает сразу,
25
+ * cancel всё сбрасывает; повторный вызов без новых данных ничего не делает.
26
+ */
27
+ export declare function createDebouncer(ms: number, fn: (paths: string[]) => void): {
28
+ push(path: string): void;
29
+ cancel(): void;
30
+ flush(): void;
31
+ };
32
+ /**
33
+ * Создаёт вотчер проекта: `fs.watch(cwd, { recursive: true })`,
34
+ * при исключении — обычный (нерекурсивный) watch cwd.
35
+ * Игнорируемые пути отфильтровываются, срабатывания дебаунсятся
36
+ * (400 мс по умолчанию). Ошибки onTrigger логируются в console.error
37
+ * и никогда не роняют процесс. Возвращает `{ close }`.
38
+ */
39
+ export declare function createProjectWatcher(options: WatchOptions): {
40
+ close(): void;
41
+ };
42
+ /** Возвращает полный текст standalone pre-commit hook для команды */
43
+ export declare function buildPreCommitHook(command: string): string;
44
+ /**
45
+ * Возвращает каталог hooks с учётом `.git`-файла (worktree/submodule) и
46
+ * настройки `core.hooksPath`. Бросает понятную ошибку, если репозитория нет.
47
+ */
48
+ export declare function resolveHooksDir(cwd: string): string;
49
+ /**
50
+ * Вставляет/обновляет блок consilium в существующем hook-файле.
51
+ * Все существующие блоки заменяются одним (дублей нет).
52
+ * Блок вставляется перед первым `exit`/`exec`, чтобы не оказаться мёртвым.
53
+ */
54
+ export declare function upsertHookBlock(existing: string, command: string): string;
55
+ /** Удаляет блок(и) consilium, остальное содержимое оставляет нетронутым */
56
+ export declare function removeHookBlock(existing: string): string;
57
+ /**
58
+ * Устанавливает pre-commit hook в `<cwd>/.git/hooks/pre-commit`.
59
+ * Существующий hook сохраняется (блок добавляется/обновляется через upsert).
60
+ * Бросает понятную ошибку, если `.git` отсутствует.
61
+ * Возвращает `created=false`, если файл уже существовал.
62
+ */
63
+ export declare function installPreCommitHook(cwd: string, command: string): {
64
+ path: string;
65
+ created: boolean;
66
+ };
67
+ /**
68
+ * Удаляет блок consilium из pre-commit hook.
69
+ * Если файл стал фактически пустым (только shebang/whitespace/set) —
70
+ * удаляет его. Возвращает true, если блок был удалён, иначе false.
71
+ */
72
+ export declare function uninstallPreCommitHook(cwd: string): boolean;
73
+ //# sourceMappingURL=watch.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"watch.d.ts","sourceRoot":"","sources":["../src/watch.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAiBH,iCAAiC;AACjC,MAAM,WAAW,YAAY;IAC3B,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,MAAM,CAAC,EAAE,MAAM,EAAE,CAAC;IAClB,SAAS,EAAE,CAAC,KAAK,EAAE,MAAM,EAAE,KAAK,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CACtD;AAKD,2FAA2F;AAC3F,eAAO,MAAM,uBAAuB,EAAE,SAAS,MAAM,EASpD,CAAC;AAyBF;;;;;;GAMG;AACH,wBAAgB,YAAY,CAAC,YAAY,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,MAAM,EAAE,GAAG,OAAO,CA2C7E;AAED;;;;GAIG;AACH,wBAAgB,eAAe,CAC7B,EAAE,EAAE,MAAM,EACV,EAAE,EAAE,CAAC,KAAK,EAAE,MAAM,EAAE,KAAK,IAAI,GAC5B;IACD,IAAI,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,MAAM,IAAI,IAAI,CAAC;IACf,KAAK,IAAI,IAAI,CAAC;CACf,CAqCA;AAED;;;;;;GAMG;AACH,wBAAgB,oBAAoB,CAAC,OAAO,EAAE,YAAY,GAAG;IAAE,KAAK,IAAI,IAAI,CAAA;CAAE,CAwD7E;AAED,qEAAqE;AACrE,wBAAgB,kBAAkB,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAE1D;AAKD;;;GAGG;AACH,wBAAgB,eAAe,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAiCnD;AAED;;;;GAIG;AACH,wBAAgB,eAAe,CAAC,QAAQ,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,MAAM,CA6BzE;AAED,2EAA2E;AAC3E,wBAAgB,eAAe,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,CAMxD;AASD;;;;;GAKG;AACH,wBAAgB,oBAAoB,CAClC,GAAG,EAAE,MAAM,EACX,OAAO,EAAE,MAAM,GACd;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,OAAO,CAAA;CAAE,CA6BpC;AAYD;;;;GAIG;AACH,wBAAgB,sBAAsB,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CA6B3D"}
package/dist/watch.js ADDED
@@ -0,0 +1,350 @@
1
+ /**
2
+ * @fileoverview Watch mode (debounced fs.watch) and git pre-commit hook helpers.
3
+ * @module lv24-opencode-consilium
4
+ */
5
+ import { chmodSync, existsSync, mkdirSync, readFileSync, renameSync, statSync, unlinkSync, watch, writeFileSync } from 'node:fs';
6
+ import { execFileSync } from 'node:child_process';
7
+ import path from 'node:path';
8
+ /** Debounce вотчера по умолчанию, мс */
9
+ const DEFAULT_DEBOUNCE_MS = 400;
10
+ /** Игнорируемые пути/глобы по умолчанию (всегда применяются вместе с пользовательскими) */
11
+ export const DEFAULT_IGNORE_PATTERNS = [
12
+ '.git',
13
+ 'node_modules',
14
+ 'dist',
15
+ '.consilium',
16
+ 'coverage',
17
+ '*.tmp',
18
+ '*.log',
19
+ '.DS_Store'
20
+ ];
21
+ /** Маркер начала блока consilium в hook-файле */
22
+ const HOOK_BLOCK_START = '# >>> consilium >>>';
23
+ /** Маркер конца блока consilium в hook-файле */
24
+ const HOOK_BLOCK_END = '# <<< consilium <<<';
25
+ /** Все блоки consilium (upsert заменяет их одним, remove удаляет все) */
26
+ const HOOK_BLOCK_RE_ALL = /# >>> consilium >>>[\s\S]*?# <<< consilium <<</g;
27
+ /** Нормализует путь: `\` -> `/` */
28
+ function normalizeSlashes(p) {
29
+ return p.replace(/\\/g, '/');
30
+ }
31
+ /** Преобразует простой `*`-глоб в RegExp, экранируя остальное */
32
+ function globToRegExp(glob) {
33
+ const body = glob
34
+ .split('*')
35
+ .map(part => part.replace(/[.+?^${}()|[\]\\]/g, '\\$&'))
36
+ .join('.*');
37
+ return new RegExp(`^${body}$`);
38
+ }
39
+ /**
40
+ * Проверяет, попадает ли путь под ignore-паттерны.
41
+ * Плоские имена (`.git`, `dist`) матчатся как полный сегмент пути —
42
+ * поэтому работают и для абсолютных путей внутри игнорируемых директорий.
43
+ * Паттерны со `*` (`*.tmp`) матчатся против всего пути и basename.
44
+ * Паттерны со `/` матчатся как префикс/подпуть.
45
+ */
46
+ export function shouldIgnore(relativePath, ignore) {
47
+ const patterns = [...DEFAULT_IGNORE_PATTERNS, ...(ignore ?? [])];
48
+ const normalized = normalizeSlashes(relativePath)
49
+ .replace(/^\.\//, '')
50
+ .replace(/\/+$/, '');
51
+ if (normalized.length === 0 || normalized === '.') {
52
+ return false;
53
+ }
54
+ const segments = normalized.split('/').filter(s => s.length > 0 && s !== '.');
55
+ if (segments.length === 0) {
56
+ return false;
57
+ }
58
+ const basename = segments[segments.length - 1];
59
+ for (const raw of patterns) {
60
+ const pattern = normalizeSlashes(raw).replace(/\/+$/, '');
61
+ if (pattern.length === 0 || pattern === '.') {
62
+ continue;
63
+ }
64
+ if (pattern.includes('*')) {
65
+ const re = globToRegExp(pattern);
66
+ if (re.test(normalized) || re.test(basename)) {
67
+ return true;
68
+ }
69
+ continue;
70
+ }
71
+ if (pattern.includes('/')) {
72
+ const clean = pattern.replace(/^\.\//, '');
73
+ if (normalized === clean ||
74
+ normalized.startsWith(`${clean}/`) ||
75
+ normalized.endsWith(`/${clean}`) ||
76
+ normalized.includes(`/${clean}/`)) {
77
+ return true;
78
+ }
79
+ continue;
80
+ }
81
+ if (segments.includes(pattern)) {
82
+ return true;
83
+ }
84
+ }
85
+ return false;
86
+ }
87
+ /**
88
+ * Создаёт дебаунсер: собирает пути в Set, сбрасывает таймер на каждый push,
89
+ * вызывает fn один раз с отсортированным массивом. flush срабатывает сразу,
90
+ * cancel всё сбрасывает; повторный вызов без новых данных ничего не делает.
91
+ */
92
+ export function createDebouncer(ms, fn) {
93
+ const pending = new Set();
94
+ let timer;
95
+ const fire = () => {
96
+ timer = undefined;
97
+ if (pending.size === 0) {
98
+ return;
99
+ }
100
+ const paths = [...pending].sort();
101
+ pending.clear();
102
+ fn(paths);
103
+ };
104
+ return {
105
+ push(filePath) {
106
+ pending.add(filePath);
107
+ if (timer !== undefined) {
108
+ clearTimeout(timer);
109
+ }
110
+ timer = setTimeout(fire, ms);
111
+ },
112
+ cancel() {
113
+ if (timer !== undefined) {
114
+ clearTimeout(timer);
115
+ timer = undefined;
116
+ }
117
+ pending.clear();
118
+ },
119
+ flush() {
120
+ if (timer !== undefined) {
121
+ clearTimeout(timer);
122
+ timer = undefined;
123
+ }
124
+ fire();
125
+ }
126
+ };
127
+ }
128
+ /**
129
+ * Создаёт вотчер проекта: `fs.watch(cwd, { recursive: true })`,
130
+ * при исключении — обычный (нерекурсивный) watch cwd.
131
+ * Игнорируемые пути отфильтровываются, срабатывания дебаунсятся
132
+ * (400 мс по умолчанию). Ошибки onTrigger логируются в console.error
133
+ * и никогда не роняют процесс. Возвращает `{ close }`.
134
+ */
135
+ export function createProjectWatcher(options) {
136
+ const cwd = options.cwd ?? process.cwd();
137
+ const debounceMs = options.debounceMs ?? DEFAULT_DEBOUNCE_MS;
138
+ const ignore = [...DEFAULT_IGNORE_PATTERNS, ...(options.ignore ?? [])];
139
+ let stopped = false;
140
+ const debouncer = createDebouncer(debounceMs, (paths) => {
141
+ if (stopped) {
142
+ return;
143
+ }
144
+ try {
145
+ const result = options.onTrigger(paths);
146
+ if (result instanceof Promise) {
147
+ result.catch((error) => {
148
+ console.error(error);
149
+ });
150
+ }
151
+ }
152
+ catch (error) {
153
+ console.error(error);
154
+ }
155
+ });
156
+ const onFilename = (filename) => {
157
+ if (stopped || filename === null) {
158
+ return;
159
+ }
160
+ const name = typeof filename === 'string' ? filename : filename.toString('utf8');
161
+ if (name.length === 0) {
162
+ return;
163
+ }
164
+ const normalized = normalizeSlashes(name);
165
+ if (shouldIgnore(normalized, ignore)) {
166
+ return;
167
+ }
168
+ debouncer.push(normalized);
169
+ };
170
+ let watcher;
171
+ try {
172
+ watcher = watch(cwd, { recursive: true }, (_event, filename) => onFilename(filename));
173
+ }
174
+ catch {
175
+ watcher = watch(cwd, (_event, filename) => onFilename(filename));
176
+ }
177
+ watcher.on('error', (error) => {
178
+ if (!stopped) {
179
+ console.error(error);
180
+ }
181
+ });
182
+ return {
183
+ close() {
184
+ stopped = true;
185
+ debouncer.cancel();
186
+ watcher.close();
187
+ }
188
+ };
189
+ }
190
+ /** Возвращает полный текст standalone pre-commit hook для команды */
191
+ export function buildPreCommitHook(command) {
192
+ return `#!/bin/sh\nset -e\n${command}\n`;
193
+ }
194
+ /** Допустимые интерпретаторы для существующего pre-commit hook */
195
+ const SHELL_INTERPRETERS = new Set(['sh', 'bash', 'dash', 'zsh', 'ksh']);
196
+ /**
197
+ * Возвращает каталог hooks с учётом `.git`-файла (worktree/submodule) и
198
+ * настройки `core.hooksPath`. Бросает понятную ошибку, если репозитория нет.
199
+ */
200
+ export function resolveHooksDir(cwd) {
201
+ // git сам знает про worktrees, submodules, core.hooksPath и подкаталоги.
202
+ try {
203
+ const resolved = execFileSync('git', ['rev-parse', '--git-path', 'hooks'], {
204
+ cwd,
205
+ encoding: 'utf8',
206
+ windowsHide: true
207
+ }).trim();
208
+ if (resolved.length > 0) {
209
+ return path.isAbsolute(resolved) ? resolved : path.resolve(cwd, resolved);
210
+ }
211
+ }
212
+ catch {
213
+ // git недоступен или каталог не является репозиторием — пробуем вручную
214
+ }
215
+ const gitPath = path.join(cwd, '.git');
216
+ if (!existsSync(gitPath)) {
217
+ throw new Error(`Cannot install pre-commit hook: no .git directory in "${cwd}" (run "git init" first).`);
218
+ }
219
+ let gitDir = gitPath;
220
+ if (statSync(gitPath).isFile()) {
221
+ const content = readFileSync(gitPath, 'utf8').trim();
222
+ const match = /^gitdir:\s*(.+)$/m.exec(content);
223
+ if (!match || match[1] === undefined) {
224
+ throw new Error(`Не удалось разобрать .git-файл в "${cwd}"`);
225
+ }
226
+ gitDir = path.resolve(cwd, match[1].trim());
227
+ }
228
+ return path.join(gitDir, 'hooks');
229
+ }
230
+ /**
231
+ * Вставляет/обновляет блок consilium в существующем hook-файле.
232
+ * Все существующие блоки заменяются одним (дублей нет).
233
+ * Блок вставляется перед первым `exit`/`exec`, чтобы не оказаться мёртвым.
234
+ */
235
+ export function upsertHookBlock(existing, command) {
236
+ const eol = existing.includes('\r\n') ? '\r\n' : '\n';
237
+ const block = `${HOOK_BLOCK_START}${eol}${command}${eol}${HOOK_BLOCK_END}`;
238
+ const firstStart = existing.indexOf(HOOK_BLOCK_START);
239
+ const lastEnd = existing.lastIndexOf(HOOK_BLOCK_END);
240
+ if (firstStart !== -1 && lastEnd > firstStart) {
241
+ // Схлопываем все блоки (включая мусор между ними) в один.
242
+ return (existing.slice(0, firstStart) +
243
+ block +
244
+ existing.slice(lastEnd + HOOK_BLOCK_END.length));
245
+ }
246
+ if (existing.trim().length === 0) {
247
+ return `#!/bin/sh${eol}set -e${eol}${block}${eol}`;
248
+ }
249
+ const exitMatch = /^[ \t]*(?:exit|exec)\b.*$/m.exec(existing);
250
+ if (exitMatch && exitMatch.index !== undefined) {
251
+ const before = existing.slice(0, exitMatch.index);
252
+ const after = existing.slice(exitMatch.index);
253
+ const separator = before.endsWith('\n') ? '' : eol;
254
+ return `${before}${separator}${block}${eol}${after}`;
255
+ }
256
+ const prefix = existing.endsWith('\n') ? existing : `${existing}${eol}`;
257
+ return `${prefix}${block}${eol}`;
258
+ }
259
+ /** Удаляет блок(и) consilium, остальное содержимое оставляет нетронутым */
260
+ export function removeHookBlock(existing) {
261
+ const without = existing.replace(HOOK_BLOCK_RE_ALL, () => '');
262
+ if (without === existing) {
263
+ return existing;
264
+ }
265
+ return without;
266
+ }
267
+ /** Атомарная запись (tmp-файл + rename в той же директории) */
268
+ function writeFileAtomic(filePath, content) {
269
+ const tmpPath = `${filePath}.consilium-${process.pid}.tmp`;
270
+ writeFileSync(tmpPath, content, 'utf8');
271
+ renameSync(tmpPath, filePath);
272
+ }
273
+ /**
274
+ * Устанавливает pre-commit hook в `<cwd>/.git/hooks/pre-commit`.
275
+ * Существующий hook сохраняется (блок добавляется/обновляется через upsert).
276
+ * Бросает понятную ошибку, если `.git` отсутствует.
277
+ * Возвращает `created=false`, если файл уже существовал.
278
+ */
279
+ export function installPreCommitHook(cwd, command) {
280
+ const hooksDir = resolveHooksDir(cwd);
281
+ mkdirSync(hooksDir, { recursive: true });
282
+ const hookPath = path.join(hooksDir, 'pre-commit');
283
+ const existed = existsSync(hookPath);
284
+ const existing = existed ? readFileSync(hookPath, 'utf8') : '';
285
+ if (existing.trim().length > 0) {
286
+ const firstLine = existing.split(/\r?\n/).find(line => line.trim().length > 0) ?? '';
287
+ if (firstLine.startsWith('#!')) {
288
+ const interpreter = firstLine.slice(2).trim().split(/\s+/)[0] ?? '';
289
+ const base = path.basename(interpreter);
290
+ if (!SHELL_INTERPRETERS.has(base)) {
291
+ throw new Error(`Cannot install pre-commit hook: existing hook uses "${interpreter}". ` +
292
+ 'Add the consilium block manually or convert the hook to a shell script.');
293
+ }
294
+ }
295
+ }
296
+ writeFileAtomic(hookPath, upsertHookBlock(existing, command));
297
+ try {
298
+ chmodSync(hookPath, 0o755);
299
+ }
300
+ catch {
301
+ // Windows: chmod без эффекта, игнорируем.
302
+ }
303
+ return { path: hookPath, created: !existed };
304
+ }
305
+ /** Проверяет, не осталось ли в hook-файле только служебного содержимого */
306
+ function isEffectivelyEmptyHook(content) {
307
+ return content
308
+ .split('\n')
309
+ .every(line => {
310
+ const trimmed = line.trim();
311
+ return trimmed === '' || trimmed.startsWith('#!') || trimmed.startsWith('set ');
312
+ });
313
+ }
314
+ /**
315
+ * Удаляет блок consilium из pre-commit hook.
316
+ * Если файл стал фактически пустым (только shebang/whitespace/set) —
317
+ * удаляет его. Возвращает true, если блок был удалён, иначе false.
318
+ */
319
+ export function uninstallPreCommitHook(cwd) {
320
+ let hookPath;
321
+ try {
322
+ hookPath = path.join(resolveHooksDir(cwd), 'pre-commit');
323
+ }
324
+ catch {
325
+ return false;
326
+ }
327
+ if (!existsSync(hookPath)) {
328
+ return false;
329
+ }
330
+ const existing = readFileSync(hookPath, 'utf8');
331
+ const next = removeHookBlock(existing);
332
+ if (next === existing) {
333
+ return false;
334
+ }
335
+ if (isEffectivelyEmptyHook(next)) {
336
+ unlinkSync(hookPath);
337
+ return true;
338
+ }
339
+ const mode = statSync(hookPath).mode & 0o777;
340
+ writeFileAtomic(hookPath, next);
341
+ try {
342
+ // Сохраняем исполняемость: иначе git молча перестанет запускать hook.
343
+ chmodSync(hookPath, mode | 0o100);
344
+ }
345
+ catch {
346
+ // Windows: chmod без эффекта, игнорируем.
347
+ }
348
+ return true;
349
+ }
350
+ //# sourceMappingURL=watch.js.map