@7n/rules 1.32.0 → 1.32.1
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/CHANGELOG.md +6 -0
- package/bin/n-rules.js +9 -2
- package/package.json +1 -1
- package/scripts/docs/skills-cli.md +1 -1
- package/scripts/lib/auto-worktree.mjs +72 -15
- package/scripts/lib/docs/auto-worktree.md +1 -1
- package/scripts/skills-cli.mjs +17 -0
- package/skills/taze/js/docs/orchestrate.md +1 -1
- package/skills/taze/js/orchestrate.mjs +37 -2
package/CHANGELOG.md
CHANGED
package/bin/n-rules.js
CHANGED
|
@@ -101,7 +101,7 @@ import { syncClaudeConfig } from '../scripts/sync-claude-config.mjs'
|
|
|
101
101
|
import { syncGitignoreWorktree } from '../scripts/lib/sync-gitignore-worktree.mjs'
|
|
102
102
|
import { upgradeNRulesToLatestAndBunInstall } from '../scripts/upgrade-n-rules-and-install.mjs'
|
|
103
103
|
import { runRenameYamlExtensionsCli } from './rename-yaml-extensions.mjs'
|
|
104
|
-
import { runSkillsCli } from '../scripts/skills-cli.mjs'
|
|
104
|
+
import { isTazeOrchestratorSkillArgs, runSkillsCli } from '../scripts/skills-cli.mjs'
|
|
105
105
|
import { syncSetupBunDepsAction } from '../scripts/sync-setup-bun-deps-action.mjs'
|
|
106
106
|
|
|
107
107
|
const PACKAGE_NAME = '@7n/rules'
|
|
@@ -1729,7 +1729,14 @@ try {
|
|
|
1729
1729
|
}
|
|
1730
1730
|
// `ci` (plan) — read-only гейт-команда для CI-джоб: не мутує package.json
|
|
1731
1731
|
// (ensure дописав би devDependency прямо в чекауті pipeline-агента).
|
|
1732
|
-
|
|
1732
|
+
// `skill <runner> taze` — JS-оркестрований worktree-only шлях
|
|
1733
|
+
// (skills/taze/js/orchestrate.mjs): сам створює worktree і гейтить на
|
|
1734
|
+
// чистоту дерева ДО checkout. Мутація тут забруднила б дерево прямо
|
|
1735
|
+
// перед тим гейтом і провалювала б auto-create на інакше чистому дереві —
|
|
1736
|
+
// оркестратор сам робить self-upgrade devDependency вже ВСЕРЕДИНІ
|
|
1737
|
+
// щойно створеного worktree, після власного гейту чистоти.
|
|
1738
|
+
const skipDevDepsEnsure = command === 'skill' && isTazeOrchestratorSkillArgs(args)
|
|
1739
|
+
if (command !== 'ci' && !skipDevDepsEnsure) await ensureNRulesInRootDevDependencies(effectiveRoot)
|
|
1733
1740
|
// Підкоманди-оркестратори (hook/lint/skill/adr-normalize-local/taze/release тощо)
|
|
1734
1741
|
// можуть спавнити внутрішню agent/LLM-сесію — ADR Stop-hooks (capture/normalize)
|
|
1735
1742
|
// мають пропустити її як технічний шум, не людську думку (spec 2026-06-30).
|
package/package.json
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
/** @see ./docs/auto-worktree.md */
|
|
2
|
-
import { existsSync } from 'node:fs'
|
|
3
|
-
import { copyFile, mkdir, rm } from 'node:fs/promises'
|
|
4
|
-
import { dirname, join } from 'node:path'
|
|
2
|
+
import { existsSync, statSync } from 'node:fs'
|
|
3
|
+
import { copyFile, mkdir, readdir, rm } from 'node:fs/promises'
|
|
4
|
+
import { dirname, join, relative } from 'node:path'
|
|
5
5
|
import { createInterface } from 'node:readline/promises'
|
|
6
6
|
|
|
7
7
|
const YES_RE = /^y(es)?$/i
|
|
8
|
+
const TRAILING_SLASH_RE = /\/$/
|
|
8
9
|
|
|
9
10
|
/**
|
|
10
11
|
* Питає y/N у терміналі. Поза TTY (CI, неінтерактивний виклик) — одразу `false`,
|
|
@@ -112,6 +113,31 @@ export async function ensureRunningInWorktree(
|
|
|
112
113
|
return { cwd: newCwd, autoCreated: true, branchArg }
|
|
113
114
|
}
|
|
114
115
|
|
|
116
|
+
/**
|
|
117
|
+
* Рекурсивно копіює вміст директорії (лише файли-листки; підкаталоги
|
|
118
|
+
* створюються по дорозі через `makeDir`). `listDir` — інжектований `readdir`
|
|
119
|
+
* з `{ recursive: true, withFileTypes: true }` (Node ≥20.1): кожен `Dirent`
|
|
120
|
+
* несе `parentPath`/`path` — реальний каталог, у якому файл лежить.
|
|
121
|
+
* @param {string} srcDir директорія-джерело (у worktree)
|
|
122
|
+
* @param {string} destDir директорія-призначення (у оригінальному дереві)
|
|
123
|
+
* @param {{ copy: (src: string, dest: string) => Promise<void>, makeDir: (path: string, opts?: object) => Promise<void>, listDir: (path: string, opts?: object) => Promise<Array<{ name: string, parentPath?: string, path?: string, isFile: () => boolean }>> }} io інжекти
|
|
124
|
+
* @returns {Promise<string[]>} шляхи скопійованих файлів відносно `srcDir` (forward-slash)
|
|
125
|
+
*/
|
|
126
|
+
async function copyDirectoryRecursive(srcDir, destDir, { copy, makeDir, listDir }) {
|
|
127
|
+
const entries = await listDir(srcDir, { recursive: true, withFileTypes: true })
|
|
128
|
+
const copiedRelPaths = []
|
|
129
|
+
for (const entry of entries) {
|
|
130
|
+
if (!entry.isFile()) continue
|
|
131
|
+
const entryDir = entry.parentPath ?? entry.path ?? srcDir
|
|
132
|
+
const relFromSrc = relative(srcDir, join(entryDir, entry.name)).replaceAll('\\', '/')
|
|
133
|
+
const dest = join(destDir, relFromSrc)
|
|
134
|
+
await makeDir(dirname(dest), { recursive: true })
|
|
135
|
+
await copy(join(entryDir, entry.name), dest)
|
|
136
|
+
copiedRelPaths.push(relFromSrc)
|
|
137
|
+
}
|
|
138
|
+
return copiedRelPaths
|
|
139
|
+
}
|
|
140
|
+
|
|
115
141
|
/**
|
|
116
142
|
* Переносить зміни з автоствореного worktree назад у вихідне дерево як
|
|
117
143
|
* **untracked/незакомічені** правки (просте копіювання файлів, без git
|
|
@@ -122,48 +148,79 @@ export async function ensureRunningInWorktree(
|
|
|
122
148
|
* лише нову назву — стара лишається як була, прийнятний компроміс для
|
|
123
149
|
* інструментів, що самі файли не перейменовують (ефект можливий лише як
|
|
124
150
|
* побічний результат LLM-рефакторингу чи форматера).
|
|
151
|
+
*
|
|
152
|
+
* **Untracked-директорія цілком.** Git схлопує щойно створену untracked
|
|
153
|
+
* директорію в один porcelain-рядок із `/` на кінці (якщо в ній нема жодного
|
|
154
|
+
* файлу, вже відомого git) — `copyFile` на такий шлях впав би (`EISDIR`/`ENOENT`)
|
|
155
|
+
* і обривав би цикл, гублячи все, що йшло по порядку ітерації ДАЛІ. Такий
|
|
156
|
+
* рядок (суфікс `/` або реальний каталог на диску) копіюється рекурсивно
|
|
157
|
+
* (`copyDirectoryRecursive`) — весь вміст, а не один `copyFile`.
|
|
125
158
|
* @param {string} worktreeCwd автостворений worktree, з якого переносимо
|
|
126
159
|
* @param {string} originalCwd вихідне дерево, куди переносимо
|
|
127
160
|
* @param {typeof import('node:child_process').spawnSync} spawnFn інжект для тестів
|
|
128
161
|
* @param {(line: string) => void} log колбек прогресу
|
|
129
|
-
* @param {{ copyFile?: (src: string, dest: string) => Promise<void>, rm?: (path: string, opts?: object) => Promise<void>, mkdir?: (path: string, opts?: object) => Promise<void
|
|
130
|
-
* @returns {Promise<string[]>} відносні шляхи перенесених файлів
|
|
162
|
+
* @param {{ copyFile?: (src: string, dest: string) => Promise<void>, rm?: (path: string, opts?: object) => Promise<void>, mkdir?: (path: string, opts?: object) => Promise<void>, readdir?: (path: string, opts?: object) => Promise<Array<object>> }} [deps] інжекти для тестів
|
|
163
|
+
* @returns {Promise<{ brought: string[], failed: boolean }>} відносні шляхи перенесених файлів і чи стався провал (частковий чи повний)
|
|
131
164
|
*/
|
|
132
165
|
export async function bringChangesBackToOriginal(worktreeCwd, originalCwd, spawnFn, log, deps = {}) {
|
|
133
166
|
const copy = deps.copyFile ?? copyFile
|
|
134
167
|
const removeFile = deps.rm ?? rm
|
|
135
168
|
const makeDir = deps.mkdir ?? mkdir
|
|
169
|
+
const listDir = deps.readdir ?? readdir
|
|
136
170
|
|
|
137
171
|
const statusResult = spawnFn('git', ['status', '--porcelain'], { cwd: worktreeCwd, encoding: 'utf8' })
|
|
138
172
|
if (statusResult.status !== 0) {
|
|
139
173
|
log(
|
|
140
174
|
`⚠️ Не вдалось прочитати git status у "${worktreeCwd}" — зміни НЕ перенесені назад, worktree лишиться для ручного розбору.`
|
|
141
175
|
)
|
|
142
|
-
return []
|
|
176
|
+
return { brought: [], failed: true }
|
|
143
177
|
}
|
|
144
178
|
|
|
145
179
|
const lines = statusResult.stdout.split('\n').filter(Boolean)
|
|
146
180
|
if (lines.length === 0) {
|
|
147
181
|
log('ℹ️ Worktree без змін — нічого переносити назад.')
|
|
148
|
-
return []
|
|
182
|
+
return { brought: [], failed: false }
|
|
149
183
|
}
|
|
150
184
|
|
|
151
185
|
const brought = []
|
|
186
|
+
let failed = false
|
|
152
187
|
for (const line of lines) {
|
|
153
188
|
const rest = line.slice(3)
|
|
154
189
|
const relPath = rest.includes(' -> ') ? rest.split(' -> ', 2)[1] : rest
|
|
155
190
|
const srcPath = join(worktreeCwd, relPath)
|
|
156
191
|
const destPath = join(originalCwd, relPath)
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
192
|
+
try {
|
|
193
|
+
if (!existsSync(srcPath)) {
|
|
194
|
+
await removeFile(destPath, { force: true, recursive: true })
|
|
195
|
+
brought.push(relPath)
|
|
196
|
+
continue
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
const isDir = relPath.endsWith('/') || statSync(srcPath).isDirectory()
|
|
200
|
+
if (isDir) {
|
|
201
|
+
const trimmedRelPath = relPath.replace(TRAILING_SLASH_RE, '')
|
|
202
|
+
const copiedRelPaths = await copyDirectoryRecursive(srcPath, destPath, { copy, makeDir, listDir })
|
|
203
|
+
for (const nestedRelPath of copiedRelPaths) {
|
|
204
|
+
brought.push(`${trimmedRelPath}/${nestedRelPath}`)
|
|
205
|
+
}
|
|
206
|
+
} else {
|
|
207
|
+
await makeDir(dirname(destPath), { recursive: true })
|
|
208
|
+
await copy(srcPath, destPath)
|
|
209
|
+
brought.push(relPath)
|
|
210
|
+
}
|
|
211
|
+
} catch (error) {
|
|
212
|
+
failed = true
|
|
213
|
+
log(
|
|
214
|
+
`⚠️ Не вдалось перенести "${relPath}" назад у "${originalCwd}" — ${error instanceof Error ? error.message : String(error)}`
|
|
215
|
+
)
|
|
162
216
|
}
|
|
163
|
-
brought.push(relPath)
|
|
164
217
|
}
|
|
165
|
-
log(
|
|
166
|
-
|
|
218
|
+
log(
|
|
219
|
+
failed
|
|
220
|
+
? `⚠️ Перенесення назад у "${originalCwd}" частково провалилось — перенесено: ${brought.join(', ') || '(нічого)'}`
|
|
221
|
+
: `📤 Перенесено назад у "${originalCwd}" як untracked: ${brought.join(', ')}`
|
|
222
|
+
)
|
|
223
|
+
return { brought, failed }
|
|
167
224
|
}
|
|
168
225
|
|
|
169
226
|
/**
|
package/scripts/skills-cli.mjs
CHANGED
|
@@ -244,6 +244,23 @@ export function resolveBundledPackageRoot(fromModuleUrl = import.meta.url) {
|
|
|
244
244
|
return join(dirname(fileURLToPath(fromModuleUrl)), '..')
|
|
245
245
|
}
|
|
246
246
|
|
|
247
|
+
/**
|
|
248
|
+
* Чи `argv` (аргументи після `skill`) резолвиться в JS-оркестрований
|
|
249
|
+
* worktree-only `taze`-шлях (`runTazeOrchestratorCli`) — той самий критерій,
|
|
250
|
+
* що й нижче в `runSkillsCli`. Використовується `n-rules.js`, щоб не мутувати
|
|
251
|
+
* root `package.json` (self-upgrade `@7n/rules`) ДО власного worktree-гейту
|
|
252
|
+
* оркестратора: той сам створює worktree і перевіряє чистоту дерева
|
|
253
|
+
* (`ensureRunningInWorktree`, `requireCleanTree: true`) — мутація package.json
|
|
254
|
+
* прямо перед цим викликом примусово провалила б auto-create там, де дерево
|
|
255
|
+
* інакше було б чисте.
|
|
256
|
+
* @param {string[]} argv аргументи після `skill`
|
|
257
|
+
* @returns {boolean} `true`, якщо запуск піде через `runTazeOrchestratorCli`
|
|
258
|
+
*/
|
|
259
|
+
export function isTazeOrchestratorSkillArgs(argv) {
|
|
260
|
+
const [first, second] = argv
|
|
261
|
+
return RUNNERS.has(first) && first !== 'claude' && normalizeSkillId(second) === 'taze'
|
|
262
|
+
}
|
|
263
|
+
|
|
247
264
|
/**
|
|
248
265
|
* @param {string[]} argv аргументи після `skill` у `n-rules`
|
|
249
266
|
* @param {{ packageRoot?: string, projectDir?: string, log?: (line: string) => void, logError?: (line: string) => void, deps?: { runPiAgentSkill?: (prompt: string, opts?: object) => Promise<{ ok: boolean, error: string|null }> } }} [options] перевизначення кореня пакета, каталогу проєкту, функцій виводу та інжектів (для тестів)
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
import { spawnSync } from 'node:child_process'
|
|
3
3
|
import { pathToFileURL } from 'node:url'
|
|
4
4
|
|
|
5
|
+
import { ensureNRulesInRootDevDependencies } from '../../../scripts/ensure-n-rules-dev-dependencies.mjs'
|
|
5
6
|
import {
|
|
6
7
|
bringChangesBackToOriginal,
|
|
7
8
|
ensureRunningInWorktree,
|
|
@@ -138,7 +139,13 @@ async function runEcosystem(provider, { cwd, runner, log, deps, spawnFn, call })
|
|
|
138
139
|
eco.results.push({ ...entry, ...outcome })
|
|
139
140
|
log(outcome.ok ? ` ✅ ${entry.pkg}` : ` ❌ ${entry.pkg}: ${outcome.error}`)
|
|
140
141
|
if (outcome.ok && outcome.text) {
|
|
141
|
-
await writeCache(
|
|
142
|
+
await writeCache(
|
|
143
|
+
entry.pkg,
|
|
144
|
+
entry.from,
|
|
145
|
+
entry.to,
|
|
146
|
+
{ notes: outcome.text, sourceRepo: cwd, updatedAt: new Date().toISOString() },
|
|
147
|
+
deps
|
|
148
|
+
)
|
|
142
149
|
}
|
|
143
150
|
}
|
|
144
151
|
|
|
@@ -241,22 +248,50 @@ export async function runTazeOrchestrator(options = {}) {
|
|
|
241
248
|
})
|
|
242
249
|
const cwd = worktree.cwd
|
|
243
250
|
|
|
251
|
+
// Self-upgrade root package.json devDependency (`@7n/rules`) — навмисно
|
|
252
|
+
// ВСЕРЕДИНІ worktree, ПІСЛЯ гейту `ensureRunningInWorktree` на чистоту
|
|
253
|
+
// дерева: та ж мутація у `originalCwd` ДО цього гейту забруднила б дерево
|
|
254
|
+
// прямо перед перевіркою, яку сам гейт щойно пройшов (n-rules.js навмисно
|
|
255
|
+
// пропускає її для цього шляху). Провал — не критичний для taze-прогону,
|
|
256
|
+
// лише лог.
|
|
257
|
+
try {
|
|
258
|
+
const ensureDevDeps = deps.ensureNRulesInRootDevDependencies ?? ensureNRulesInRootDevDependencies
|
|
259
|
+
await ensureDevDeps(cwd, { silent: true })
|
|
260
|
+
} catch (error) {
|
|
261
|
+
log(
|
|
262
|
+
`⚠️ Self-upgrade @7n/rules у "${cwd}" провалився — пропускаю (${error instanceof Error ? error.message : String(error)})`
|
|
263
|
+
)
|
|
264
|
+
}
|
|
265
|
+
|
|
244
266
|
let cleanedUp = false
|
|
245
267
|
/**
|
|
246
268
|
* Переносить зміни назад і прибирає автостворений worktree — не більше
|
|
247
269
|
* одного разу (idempotent), щоб і сигнальний обробник, і `finally` могли
|
|
248
270
|
* безпечно кликати те саме без подвійного `bringChangesBackToOriginal`/
|
|
249
271
|
* `removeAutoCreatedWorktree`.
|
|
272
|
+
*
|
|
273
|
+
* **Захист від втрати даних.** Якщо перенесення провалилось (частково чи
|
|
274
|
+
* повністю — `bringChangesBackToOriginal` кидає або повертає
|
|
275
|
+
* `failed: true`), worktree НЕ прибирається: джерело ще не перенесених
|
|
276
|
+
* файлів лишається на диску для ручного відновлення замість того, щоб
|
|
277
|
+
* зникнути разом з `npx \@7n/mt worktree remove`.
|
|
250
278
|
* @returns {Promise<void>}
|
|
251
279
|
*/
|
|
252
280
|
const cleanupAutoCreatedWorktree = async () => {
|
|
253
281
|
if (cleanedUp) return
|
|
254
282
|
cleanedUp = true
|
|
283
|
+
let failed = true
|
|
255
284
|
try {
|
|
256
|
-
await bringChangesBackToOriginal(cwd, originalCwd, spawnFn, log, deps)
|
|
285
|
+
;({ failed } = await bringChangesBackToOriginal(cwd, originalCwd, spawnFn, log, deps))
|
|
257
286
|
} catch (error) {
|
|
258
287
|
log(`⚠️ Перенесення змін назад провалилось: ${error instanceof Error ? error.message : String(error)}`)
|
|
259
288
|
}
|
|
289
|
+
if (failed) {
|
|
290
|
+
log(
|
|
291
|
+
`⚠️ Перенесення назад не вдалось повністю — worktree "${cwd}" (гілка "${worktree.branchArg}") НЕ прибирається, розберіться вручну.`
|
|
292
|
+
)
|
|
293
|
+
return
|
|
294
|
+
}
|
|
260
295
|
removeAutoCreatedWorktree(worktree.branchArg, originalCwd, spawnFn, log)
|
|
261
296
|
}
|
|
262
297
|
|