@pathbit/agentdevkit 0.0.7 → 0.0.9
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/.pbai4devs/COMPATIBILIDADE.md +4 -2
- package/.pbai4devs/INSTRUCTIONS.md +4 -0
- package/.pbai4devs/commands/sincronizar/COMMAND.md +4 -3
- package/.pbai4devs/rules/documentacao/RULE.md +4 -0
- package/.pbai4devs/rules/qualidade/RULE.md +4 -2
- package/.pbai4devs/scripts/README.md +3 -3
- package/.pbai4devs/scripts/inspecionar-projeto/README.md +31 -0
- package/.pbai4devs/scripts/inspecionar-projeto/SCRIPT.mjs +142 -1
- package/.pbai4devs/scripts/inspecionar-projeto/tests/TEST.mjs +168 -1
- package/.pbai4devs/scripts/lib/gitignore.mjs +84 -15
- package/.pbai4devs/scripts/lib/native-paths.mjs +35 -0
- package/.pbai4devs/scripts/preparar-ferramentas/SCRIPT.mjs +41 -20
- package/.pbai4devs/scripts/preparar-ferramentas/tests/TEST.mjs +81 -0
- package/.pbai4devs/scripts/sincronizar-agentes/README.md +3 -0
- package/.pbai4devs/scripts/sincronizar-agentes/SCRIPT.mjs +193 -49
- package/.pbai4devs/scripts/sincronizar-agentes/tests/COEXISTENCE.mjs +190 -0
- package/.pbai4devs/scripts/sincronizar-agentes/tests/HUMAN_OWNERSHIP.mjs +151 -0
- package/.pbai4devs/scripts/sincronizar-agentes/tests/LOCAL_IGNORE.mjs +145 -1
- package/.pbai4devs/scripts/sincronizar-agentes/tests/TEST.mjs +30 -29
- package/.pbai4devs/scripts/verificar-recursos/SCRIPT.mjs +56 -8
- package/.pbai4devs/scripts/verificar-recursos/tests/TEST.mjs +92 -1
- package/.pbai4devs/skills/produto/criar-design-doc/SKILL.md +2 -0
- package/.pbai4devs/skills/produto/criar-prd/SKILL.md +2 -0
- package/.pbai4devs/skills/produto/criar-ux-ui/SKILL.md +2 -0
- package/.pbai4devs/skills/projeto/atualizar-documentacao/SKILL.md +18 -4
- package/.pbai4devs/skills/projeto/configurar-projeto/SKILL.md +2 -0
- package/.pbai4devs/skills/projeto/criar-recurso/SKILL.md +1 -0
- package/.pbai4devs/skills/projeto/sincronizar-agentes/SKILL.md +3 -2
- package/.pbai4devs/skills/specs/criar-spec/SKILL.md +1 -0
- package/.pbai4devs/skills/specs/gerenciar-roadmap/SKILL.md +2 -1
- package/.pbai4devs/templates/README.md +4 -1
- package/.pbai4devs/templates/configuracao/TEMPLATE.md +6 -0
- package/CHANGELOG.md +19 -0
- package/README.en.md +3 -2
- package/README.es.md +3 -2
- package/README.md +3 -2
- package/docs/README.md +1 -0
- package/docs/guides/README.md +1 -0
- package/docs/guides/cli.md +11 -0
- package/docs/guides/documentation.md +63 -0
- package/docs/guides/installation.md +18 -5
- package/docs/guides/resources.md +6 -5
- package/docs/guides/tools-and-testing.md +2 -0
- package/docs/guides/troubleshooting.md +6 -6
- package/docs/guides/workflow.md +2 -0
- package/docs/maintainers/packages.md +1 -1
- package/docs/product/design.md +12 -0
- package/docs/product/prd.md +4 -1
- package/docs/product/roadmap.md +4 -0
- package/docs/specs/README.md +2 -0
- package/docs/specs/v0.0.8/README.md +10 -0
- package/docs/specs/v0.0.8/SPEC.md +62 -0
- package/docs/specs/v0.0.9/README.md +9 -0
- package/docs/specs/v0.0.9/SPEC.md +68 -0
- package/package.json +3 -2
- package/src/cli.mjs +13 -4
- package/src/i18n.mjs +3 -0
- package/src/install.mjs +145 -38
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { lstatSync, readdirSync } from 'node:fs';
|
|
2
|
+
import { isAbsolute, join } from 'node:path';
|
|
3
|
+
|
|
4
|
+
// Inspect native paths without opening a symlink target. Callers restrict this
|
|
5
|
+
// exception to user-owned adapter paths; canonical kit files stay strict.
|
|
6
|
+
export function nativePathBoundary(root, path, ownedPaths = [], { preserveRootAliases = false } = {}) {
|
|
7
|
+
if (typeof path !== 'string' || !path || isAbsolute(path) || /[|\\\x00-\x1f\x7f:]/.test(path) || path.split('/').some(part => !part || part === '.' || part === '..')) {
|
|
8
|
+
throw Object.assign(new Error(`Caminho invalido: ${path}`), { code: 'E_PATH' });
|
|
9
|
+
}
|
|
10
|
+
const parts = path.split('/');
|
|
11
|
+
let current = root;
|
|
12
|
+
for (const [index, part] of parts.entries()) {
|
|
13
|
+
try {
|
|
14
|
+
const aliases = lstatSync(current).isDirectory() ? readdirSync(current).filter(name => name.toLowerCase() === part.toLowerCase() && name !== part).sort() : [];
|
|
15
|
+
if (aliases.length) {
|
|
16
|
+
const rootEntry = parts.length === 1 && ['README.md', 'AGENTS.md', 'CLAUDE.md', 'GEMINI.md'].includes(path);
|
|
17
|
+
const owned = [...ownedPaths].some(value => value.toLowerCase() === path.toLowerCase() || value.toLowerCase().startsWith(`${path.toLowerCase()}/`));
|
|
18
|
+
if (preserveRootAliases && rootEntry && !owned) return { path: aliases[0], reason: 'case-variant' };
|
|
19
|
+
throw Object.assign(new Error(`Caminhos diferem somente por maiusculas/minusculas: ${path}`), { code: 'E_PATH' });
|
|
20
|
+
}
|
|
21
|
+
} catch (error) { if (['ENOENT', 'ENOTDIR'].includes(error.code)) return null; throw error; }
|
|
22
|
+
current = join(current, part);
|
|
23
|
+
let info;
|
|
24
|
+
try { info = lstatSync(current); }
|
|
25
|
+
catch (error) { if (['ENOENT', 'ENOTDIR'].includes(error.code)) return null; throw error; }
|
|
26
|
+
const reason = info.isSymbolicLink() ? 'symbolic-link' : index < parts.length - 1 && !info.isDirectory() ? 'non-directory' : null;
|
|
27
|
+
if (!reason) continue;
|
|
28
|
+
const boundary = parts.slice(0, index + 1).join('/');
|
|
29
|
+
if ([...ownedPaths].some(value => value === boundary || value.startsWith(`${boundary}/`))) {
|
|
30
|
+
throw Object.assign(new Error(`${reason === 'symbolic-link' ? 'Link simbolico' : 'Ancestral nao e diretorio'} em caminho gerenciado: ${boundary}`), { code: reason === 'symbolic-link' ? 'E_SYMLINK' : 'E_PATH' });
|
|
31
|
+
}
|
|
32
|
+
return { path: boundary, reason };
|
|
33
|
+
}
|
|
34
|
+
return null;
|
|
35
|
+
}
|
|
@@ -101,6 +101,14 @@ function sourcePath(root, name) {
|
|
|
101
101
|
return file;
|
|
102
102
|
}
|
|
103
103
|
|
|
104
|
+
function storagePath(root, name, kind) {
|
|
105
|
+
const file = sourcePath(root, name);
|
|
106
|
+
let item;
|
|
107
|
+
try { item = lstatSync(file); } catch (error) { if (error.code !== 'ENOENT') throw error; }
|
|
108
|
+
if (item && !(kind === 'directory' ? item.isDirectory() : item.isFile())) fail(`Caminho local exige ${kind}: ${name}`);
|
|
109
|
+
return file;
|
|
110
|
+
}
|
|
111
|
+
|
|
104
112
|
export function loadCatalog(root = projectRoot) {
|
|
105
113
|
root = realpathSync(root);
|
|
106
114
|
const home = sourcePath(root, '.pbai4devs/tools'); const catalog = Object.create(null);
|
|
@@ -272,15 +280,21 @@ export class ToolManager {
|
|
|
272
280
|
return { mode: 'setup', selection: selected, platform: this.target, node: process.version, nodePath: process.execPath, npm, results: entries, summary, status: summary.failed ? 'failed' : summary.passed ? 'passed' : 'not-applicable', exitCode: summary.failed ? 1 : 0 };
|
|
273
281
|
}
|
|
274
282
|
local(name) { this.entry(name); return join(this.home, name, '.local'); }
|
|
283
|
+
storage(name) {
|
|
284
|
+
const local = this.local(name), tool = join(this.home, name);
|
|
285
|
+
for (const path of [this.home, tool, local, join(local, 'versions'), join(tool, 'versions'), join(this.home, '.cache'), join(this.home, '.cache/npm'), join(this.home, 'npm-cache')]) storagePath(this.root, path, 'directory');
|
|
286
|
+
for (const path of [join(local, 'CURRENT.json'), join(tool, 'CURRENT.json')]) storagePath(this.root, path, 'file');
|
|
287
|
+
return { local, tool };
|
|
288
|
+
}
|
|
275
289
|
current(name) {
|
|
276
|
-
this.
|
|
277
|
-
const local = this.local(name); const legacy = join(this.home, name);
|
|
290
|
+
const { local, tool: legacy } = this.storage(name);
|
|
278
291
|
const base = existsSync(join(local, 'CURRENT.json')) ? local : legacy;
|
|
279
292
|
const file = join(base, 'CURRENT.json');
|
|
280
293
|
if (!existsSync(file)) fail(`Ferramenta ausente: ${name}; execute install ${name}.`);
|
|
281
294
|
const info = json(file);
|
|
282
295
|
if (info.name !== name || info.platform !== this.target || !versionPattern.test(info.version || '')) fail(`Instalacao incompativel: ${name}`);
|
|
283
|
-
|
|
296
|
+
const prefix = storagePath(this.root, join(base, 'versions', `${info.version}-${this.target}`), 'directory');
|
|
297
|
+
return { ...info, layout: base === local ? 'local' : 'legacy', prefix };
|
|
284
298
|
}
|
|
285
299
|
check(name) {
|
|
286
300
|
const entry = this.entry(name); const runtime = this.runtime(name); const info = this.current(name);
|
|
@@ -306,7 +320,7 @@ export class ToolManager {
|
|
|
306
320
|
for (const legacy of [name, `${name}.cmd`, `${name}.ps1`, `${name}.sh`]) rmSync(join(bin, legacy), { force: true });
|
|
307
321
|
}
|
|
308
322
|
migrate(name) {
|
|
309
|
-
const
|
|
323
|
+
const { local, tool } = this.storage(name);
|
|
310
324
|
const legacyState = join(tool, 'CURRENT.json'); const legacyVersions = join(tool, 'versions');
|
|
311
325
|
if (existsSync(legacyState)) {
|
|
312
326
|
if (existsSync(join(local, 'CURRENT.json')) || (existsSync(join(local, 'versions')) && readdirSync(join(local, 'versions')).length)) fail(`Layouts local e antigo coexistem: ${name}; revise antes de migrar.`);
|
|
@@ -318,15 +332,15 @@ export class ToolManager {
|
|
|
318
332
|
}
|
|
319
333
|
const oldCache = join(this.home, 'npm-cache');
|
|
320
334
|
if (existsSync(oldCache)) {
|
|
321
|
-
const cache = join(this.home, '.cache'); mkdirSync(cache, { recursive: true });
|
|
322
|
-
|
|
335
|
+
const cache = storagePath(this.root, join(this.home, '.cache'), 'directory'); mkdirSync(cache, { recursive: true });
|
|
336
|
+
const destination = storagePath(this.root, join(cache, existsSync(join(cache, 'npm')) ? `npm-legacy-${randomUUID()}` : 'npm'), 'directory');
|
|
337
|
+
try { renameSync(oldCache, destination); }
|
|
323
338
|
catch (error) { if (error.code !== 'ENOENT' || existsSync(oldCache)) throw error; }
|
|
324
339
|
}
|
|
325
340
|
}
|
|
326
341
|
async withLock(name, action) {
|
|
327
|
-
const local = this.
|
|
328
|
-
|
|
329
|
-
mkdirSync(local, { recursive: true }); const lock = join(local, '.update-lock');
|
|
342
|
+
const { local } = this.storage(name);
|
|
343
|
+
mkdirSync(local, { recursive: true }); const lock = storagePath(this.root, join(local, '.update-lock'), 'directory');
|
|
330
344
|
try { mkdirSync(lock); } catch (error) {
|
|
331
345
|
if (error.code === 'EEXIST') {
|
|
332
346
|
const status = this.lockStatus(name);
|
|
@@ -334,8 +348,8 @@ export class ToolManager {
|
|
|
334
348
|
}
|
|
335
349
|
throw error;
|
|
336
350
|
}
|
|
337
|
-
try { writeFileSync(join(lock, 'owner.json'), JSON.stringify({ pid: process.pid, hostname: hostname(), startedAt: new Date().toISOString() })); return await action(); }
|
|
338
|
-
finally { rmSync(lock, { recursive: true, force: true }); }
|
|
351
|
+
try { writeFileSync(storagePath(this.root, join(lock, 'owner.json'), 'file'), JSON.stringify({ pid: process.pid, hostname: hostname(), startedAt: new Date().toISOString() }), { flag: 'wx' }); return await action(); }
|
|
352
|
+
finally { rmSync(storagePath(this.root, lock, 'directory'), { recursive: true, force: true }); }
|
|
339
353
|
}
|
|
340
354
|
lockStatus(name) {
|
|
341
355
|
const lock = sourcePath(this.root, join(this.local(name), '.update-lock'));
|
|
@@ -380,29 +394,36 @@ export class ToolManager {
|
|
|
380
394
|
return this.updateLocked(name);
|
|
381
395
|
}); }
|
|
382
396
|
async update(name) { this.preflight([name]); return this.withLock(name, async () => { this.migrate(name); return this.updateLocked(name); }); }
|
|
383
|
-
commitState(name, temporary) {
|
|
397
|
+
commitState(name, temporary) {
|
|
398
|
+
const { local } = this.storage(name);
|
|
399
|
+
renameSync(storagePath(this.root, temporary, 'file'), storagePath(this.root, join(local, 'CURRENT.json'), 'file'));
|
|
400
|
+
}
|
|
384
401
|
async updateLocked(name) {
|
|
385
402
|
const entry = this.entry(name); const runtime = this.runtime(name); const release = validateRelease(name, entry, await this.fetchRelease(name, entry));
|
|
403
|
+
const { local } = this.storage(name);
|
|
404
|
+
const target = storagePath(this.root, join(local, 'versions', `${release.version}-${this.target}`), 'directory');
|
|
386
405
|
let installed;
|
|
387
406
|
try { installed = this.check(name); } catch { /* Ausente ou quebrado: reparar em staging. */ }
|
|
388
407
|
if (installed?.version === release.version) { this.launchers(name); return { ...installed, changed: false }; }
|
|
389
|
-
|
|
390
|
-
const stage = mkdtempSync(join(local, '.stage-'));
|
|
391
|
-
const temporary = join(local, `CURRENT.${
|
|
408
|
+
mkdirSync(storagePath(this.root, join(local, 'versions'), 'directory'), { recursive: true });
|
|
409
|
+
const stage = mkdtempSync(join(local, '.stage-'));
|
|
410
|
+
const temporary = storagePath(this.root, join(local, `CURRENT.${randomUUID()}.json`), 'file'); let previous; let moved = false;
|
|
392
411
|
try {
|
|
393
412
|
this.installPackage(stage, entry.download.package, release.version, entry, runtime);
|
|
413
|
+
this.storage(name); storagePath(this.root, stage, 'directory');
|
|
394
414
|
const pkg = json(join(stage, 'node_modules', entry.download.package, 'package.json')); const lock = json(join(stage, 'package-lock.json'));
|
|
395
415
|
if (pkg.name !== entry.download.package || pkg.version !== release.version || lock.packages?.[`node_modules/${entry.download.package}`]?.integrity !== release.dist.integrity) fail('Pacote instalado nao corresponde ao registro.');
|
|
396
416
|
prepareExecutables(stage, entry, this.target);
|
|
397
417
|
execute(stage, entry, entry.probe, true, entry.probeInput, toolEnvironment(entry, local, runtime), runtime);
|
|
418
|
+
this.storage(name); storagePath(this.root, target, 'directory');
|
|
398
419
|
const info = { name, package: entry.download.package, version: release.version, platform: this.target, node: runtime.version, nodePath: runtime.file, source: entry.source, tarball: release.dist.tarball, integrity: release.dist.integrity, checkedAt: new Date().toISOString() };
|
|
399
|
-
writeFileSync(temporary, `${JSON.stringify(info, null, 2)}\n
|
|
420
|
+
writeFileSync(storagePath(this.root, temporary, 'file'), `${JSON.stringify(info, null, 2)}\n`, { flag: 'wx' });
|
|
400
421
|
this.launchers(name);
|
|
401
|
-
if (existsSync(target)) { previous = `${target}.previous-${
|
|
402
|
-
try { renameSync(stage, target); moved = true; this.commitState(name, temporary); }
|
|
403
|
-
catch (error) { if (moved) rmSync(target, { recursive: true, force: true }); if (previous) renameSync(previous, target); throw error; }
|
|
422
|
+
if (existsSync(target)) { previous = storagePath(this.root, `${target}.previous-${randomUUID()}`, 'directory'); renameSync(storagePath(this.root, target, 'directory'), previous); }
|
|
423
|
+
try { renameSync(storagePath(this.root, stage, 'directory'), storagePath(this.root, target, 'directory')); moved = true; this.commitState(name, temporary); }
|
|
424
|
+
catch (error) { if (moved) rmSync(storagePath(this.root, target, 'directory'), { recursive: true, force: true }); if (previous) renameSync(storagePath(this.root, previous, 'directory'), storagePath(this.root, target, 'directory')); throw error; }
|
|
404
425
|
return { ...info, layout: 'local', prefix: target, changed: true };
|
|
405
|
-
} finally { rmSync(stage, { recursive: true, force: true }); rmSync(temporary, { force: true }); }
|
|
426
|
+
} finally { rmSync(storagePath(this.root, stage, 'directory'), { recursive: true, force: true }); rmSync(storagePath(this.root, temporary, 'file'), { force: true }); }
|
|
406
427
|
}
|
|
407
428
|
run(name, args) { const entry = this.entry(name); const runtime = this.runtime(name); return execute(this.current(name).prefix, entry, args, false, undefined, toolEnvironment(entry, this.local(name), runtime), runtime); }
|
|
408
429
|
}
|
|
@@ -232,6 +232,87 @@ test('falha ao gravar CURRENT restaura o target anterior e o pacote em uso', asy
|
|
|
232
232
|
assert.equal(manager.check('test-tool').version, '1.0.0'); assert.ok(!existsSync(join(local, '.update-lock')));
|
|
233
233
|
});
|
|
234
234
|
|
|
235
|
+
test('versions simbolico existente ou quebrado e recusado antes de baixar ou escrever', async t => {
|
|
236
|
+
for (const broken of [false, true]) await t.test(broken ? 'broken' : 'existing', async subtest => {
|
|
237
|
+
const { root, manager, state, local } = fixture(subtest);
|
|
238
|
+
const outside = join(root, 'outside'); mkdirSync(outside);
|
|
239
|
+
writeFileSync(join(outside, 'keep.txt'), 'preservado'); mkdirSync(local);
|
|
240
|
+
if (!createSymlink(subtest, broken ? join(outside, 'missing') : outside, join(local, 'versions'), process.platform === 'win32' ? 'junction' : 'dir')) return;
|
|
241
|
+
for (const action of ['install', 'update']) await assert.rejects(manager[action]('test-tool'), /nao pode ser um link/);
|
|
242
|
+
assert.equal(state.fetches, 0); assert.equal(state.installs, 0);
|
|
243
|
+
assert.deepEqual(readdirSync(outside), ['keep.txt']);
|
|
244
|
+
assert.equal(readFileSync(join(outside, 'keep.txt'), 'utf8'), 'preservado');
|
|
245
|
+
assert.equal(existsSync(join(local, 'CURRENT.json')), false);
|
|
246
|
+
assert.equal(existsSync(join(local, '.update-lock')), false);
|
|
247
|
+
});
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
test('recibo CURRENT e destino de versao simbolicos nao sao seguidos nem substituidos', async t => {
|
|
251
|
+
for (const kind of ['current', 'target', 'broken-target']) await t.test(kind, async subtest => {
|
|
252
|
+
const { root, manager, state, local } = fixture(subtest); await manager.install('test-tool');
|
|
253
|
+
const receipt = readFileSync(join(local, 'CURRENT.json'));
|
|
254
|
+
const outside = join(root, 'outside'); mkdirSync(outside); writeFileSync(join(outside, 'keep.txt'), 'preservado');
|
|
255
|
+
if (kind === 'current') {
|
|
256
|
+
writeFileSync(join(outside, 'CURRENT.json'), receipt); rmSync(join(local, 'CURRENT.json'));
|
|
257
|
+
if (!createSymlink(subtest, join(outside, 'CURRENT.json'), join(local, 'CURRENT.json'))) return;
|
|
258
|
+
assert.throws(() => manager.check('test-tool'), /nao pode ser um link/);
|
|
259
|
+
} else {
|
|
260
|
+
state.version = '2.0.0';
|
|
261
|
+
if (!createSymlink(subtest, kind === 'target' ? outside : join(outside, 'missing'), join(local, 'versions', `2.0.0-${platformKey()}`), process.platform === 'win32' ? 'junction' : 'dir')) return;
|
|
262
|
+
}
|
|
263
|
+
const before = readdirSync(outside), installs = state.installs;
|
|
264
|
+
await assert.rejects(manager.update('test-tool'), /nao pode ser um link/);
|
|
265
|
+
assert.equal(state.installs, installs); assert.deepEqual(readdirSync(outside), before);
|
|
266
|
+
assert.equal(readFileSync(join(outside, 'keep.txt'), 'utf8'), 'preservado');
|
|
267
|
+
assert.deepEqual(readFileSync(kind === 'current' ? join(outside, 'CURRENT.json') : join(local, 'CURRENT.json')), receipt);
|
|
268
|
+
});
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
test('troca de versions durante fetch ou install nao escapa da arvore local', async t => {
|
|
272
|
+
for (const phase of ['fetch', 'install']) await t.test(phase, async subtest => {
|
|
273
|
+
const { root, manager, state, local } = fixture(subtest); await manager.install('test-tool');
|
|
274
|
+
const receipt = readFileSync(join(local, 'CURRENT.json')); state.version = '2.0.0';
|
|
275
|
+
const outside = join(root, 'outside'); mkdirSync(outside);
|
|
276
|
+
const probe = join(local, 'symlink-probe');
|
|
277
|
+
if (!createSymlink(subtest, outside, probe, process.platform === 'win32' ? 'junction' : 'dir')) return;
|
|
278
|
+
unlinkSync(probe);
|
|
279
|
+
const versions = join(local, 'versions'), kept = join(local, 'versions-kept'); let linked = false;
|
|
280
|
+
const replace = () => {
|
|
281
|
+
renameSync(versions, kept);
|
|
282
|
+
symlinkSync(outside, versions, process.platform === 'win32' ? 'junction' : 'dir'); linked = true;
|
|
283
|
+
};
|
|
284
|
+
if (phase === 'fetch') manager.fetchRelease = async () => { replace(); return release('2.0.0'); };
|
|
285
|
+
else manager.installPackage = (prefix, name, version) => { packageFiles(prefix, name, version); replace(); };
|
|
286
|
+
try {
|
|
287
|
+
await assert.rejects(manager.update('test-tool'), /nao pode ser um link/);
|
|
288
|
+
assert.deepEqual(readdirSync(outside), []); assert.deepEqual(readFileSync(join(local, 'CURRENT.json')), receipt);
|
|
289
|
+
} finally { if (linked) { unlinkSync(versions); renameSync(kept, versions); } }
|
|
290
|
+
assert.equal(manager.check('test-tool').version, '1.0.0');
|
|
291
|
+
assert.equal(existsSync(join(local, '.update-lock')), false);
|
|
292
|
+
});
|
|
293
|
+
});
|
|
294
|
+
|
|
295
|
+
test('migracao recusa links de versions antigo e cache compartilhado sem mover dados', async t => {
|
|
296
|
+
for (const kind of ['legacy-versions', 'cache']) await t.test(kind, async subtest => {
|
|
297
|
+
const { root, manager, state } = fixture(subtest);
|
|
298
|
+
const home = join(root, '.pbai4devs/tools'), legacy = join(home, 'test-tool');
|
|
299
|
+
const outside = join(root, 'outside'); mkdirSync(outside); writeFileSync(join(outside, 'keep.txt'), 'preservado');
|
|
300
|
+
if (kind === 'legacy-versions') {
|
|
301
|
+
writeFileSync(join(legacy, 'CURRENT.json'), JSON.stringify({ name: 'test-tool', version: '1.0.0', platform: platformKey() }));
|
|
302
|
+
if (!createSymlink(subtest, outside, join(legacy, 'versions'), process.platform === 'win32' ? 'junction' : 'dir')) return;
|
|
303
|
+
} else {
|
|
304
|
+
mkdirSync(join(home, 'npm-cache')); writeFileSync(join(home, 'npm-cache/keep.txt'), 'cache antigo');
|
|
305
|
+
if (!createSymlink(subtest, outside, join(home, '.cache'), process.platform === 'win32' ? 'junction' : 'dir')) return;
|
|
306
|
+
}
|
|
307
|
+
await assert.rejects(manager.install('test-tool'), /nao pode ser um link/);
|
|
308
|
+
assert.equal(state.fetches, 0); assert.equal(state.installs, 0);
|
|
309
|
+
assert.deepEqual(readdirSync(outside), ['keep.txt']);
|
|
310
|
+
assert.equal(readFileSync(join(outside, 'keep.txt'), 'utf8'), 'preservado');
|
|
311
|
+
if (kind === 'legacy-versions') assert.ok(existsSync(join(legacy, 'CURRENT.json')));
|
|
312
|
+
else assert.equal(readFileSync(join(home, 'npm-cache/keep.txt'), 'utf8'), 'cache antigo');
|
|
313
|
+
});
|
|
314
|
+
});
|
|
315
|
+
|
|
235
316
|
test('duas atualizacoes nao disputam a mesma ferramenta nem apagam lock alheio', async t => {
|
|
236
317
|
const { manager, state, local } = fixture(t); let enter; let proceed;
|
|
237
318
|
const entered = new Promise(resolve => { enter = resolve; });
|
|
@@ -12,6 +12,9 @@ node --test .pbai4devs/scripts/sincronizar-agentes/tests/TEST.mjs
|
|
|
12
12
|
|
|
13
13
|
`--check` só confere; a execução normal atualiza os recursos. `--force` salva
|
|
14
14
|
conflitos em `.agent-tests/` antes de reparar os caminhos gerenciados.
|
|
15
|
+
Recursos nativos humanos e seus links permanecem intactos, inclusive com
|
|
16
|
+
`--force`. O retorno inclui `preserved`; esses caminhos não contam como
|
|
17
|
+
derivados pendentes nem passam a pertencer ao kit.
|
|
15
18
|
|
|
16
19
|
Metadados vêm da fonte ou do YAML nativo declarado no manifesto. Regras exigem
|
|
17
20
|
`trigger` válido; `glob` exige `globs`. Origens nativas precisam pertencer ao
|