Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions tools/loop-cost/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

83 changes: 67 additions & 16 deletions tools/loop-init/scripts/bundle-assets.mjs
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
#!/usr/bin/env node
import { cp, rm, access } from 'node:fs/promises';
import { cp, rm, access, rename, mkdir } from 'node:fs/promises';
import path from 'node:path';
import { fileURLToPath } from 'node:url';

const PACKAGE_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
const REPO_ROOT = path.resolve(PACKAGE_ROOT, '../..');
const LOCK_DIR = path.join(PACKAGE_ROOT, '.bundle-assets.lock');

async function exists(p) {
try {
Expand All @@ -15,21 +16,71 @@ async function exists(p) {
}
}

for (const dir of ['starters', 'templates']) {
const dest = path.join(PACKAGE_ROOT, dir);
const src = path.join(REPO_ROOT, dir);
if (!(await exists(src))) {
console.error(`bundle-assets: missing ${src}`);
process.exit(1);
async function replaceDirectory(src, dest) {
const tempDest = `${dest}.tmp-${process.pid}-${Date.now()}`;
let moved = false;
await rm(tempDest, { recursive: true, force: true });
try {
await cp(src, tempDest, { recursive: true });

for (let attempt = 0; attempt < 3; attempt += 1) {
await rm(dest, { recursive: true, force: true });
try {
await rename(tempDest, dest);
moved = true;
return;
} catch (err) {
if (err?.code !== 'EEXIST' || attempt === 2) {
throw err;
}
}
}
} finally {
if (!moved) {
await rm(tempDest, { recursive: true, force: true });
}
}
await rm(dest, { recursive: true, force: true });
await cp(src, dest, { recursive: true });
console.log(`bundled ${dir}/ → tools/loop-init/${dir}/`);
}

const registrySrc = path.join(REPO_ROOT, 'patterns', 'registry.yaml');
const registryDest = path.join(PACKAGE_ROOT, 'registry.yaml');
if (await exists(registrySrc)) {
await cp(registrySrc, registryDest);
console.log('bundled patterns/registry.yaml → tools/loop-init/registry.yaml');
}
async function sleep(ms) {
return new Promise((resolve) => {
setTimeout(resolve, ms);
});
}

async function acquireLock() {
for (let attempt = 0; attempt < 100; attempt += 1) {
try {
await mkdir(LOCK_DIR);
return async () => rm(LOCK_DIR, { recursive: true, force: true });
} catch (err) {
if (err?.code !== 'EEXIST') {
throw err;
}
await sleep(50);
}
}
throw new Error(`bundle-assets: timed out waiting for ${LOCK_DIR}`);
}

const releaseLock = await acquireLock();
try {
for (const dir of ['starters', 'templates']) {
const dest = path.join(PACKAGE_ROOT, dir);
const src = path.join(REPO_ROOT, dir);
if (!(await exists(src))) {
throw new Error(`bundle-assets: missing ${src}`);
}
await replaceDirectory(src, dest);
console.log(`bundled ${dir}/ → tools/loop-init/${dir}/`);
}

const registrySrc = path.join(REPO_ROOT, 'patterns', 'registry.yaml');
const registryDest = path.join(PACKAGE_ROOT, 'registry.yaml');
if (await exists(registrySrc)) {
await cp(registrySrc, registryDest);
console.log('bundled patterns/registry.yaml → tools/loop-init/registry.yaml');
}
} finally {
await releaseLock();
}
12 changes: 11 additions & 1 deletion tools/loop-init/test/cli.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,16 @@ import { promisify } from 'node:util';
const exec = promisify(execFile);
const CLI = path.resolve('dist/cli.js');

test('bundle-assets tolerates concurrent rebuilds', async () => {
await Promise.all([
exec('node', ['scripts/bundle-assets.mjs']),
exec('node', ['scripts/bundle-assets.mjs']),
]);
await access(path.join('starters', 'issue-triage', 'README.md'));
await access(path.join('templates', 'SKILL.md.issue-triage'));
await access('registry.yaml');
});

test('loop-init --help exits 0', async () => {
const { stdout } = await exec('node', [CLI, '--help']);
assert.match(stdout, /changelog-drafter/);
Expand Down Expand Up @@ -75,4 +85,4 @@ test('loop-init scaffolds ci-sweeper with bundled assets', async () => {
} finally {
await rm(dir, { recursive: true, force: true });
}
});
});