build(skill): add build:skill for a readable, non-bundled skill artifact (Phase 6)
`npm run build:skill` emits dist/obelisk-skill/: the whole scripts/ Core
compiled to readable JS (module structure + comments preserved, ~1:1 with
source, no bundling/minifying per ADR-0004), plus schema.sql, SKILL.md,
references/, and a package.json. It excludes app/, tests/, and release/, which
is what keeps the artifact small.
- tsconfig.skill.json compiles scripts/**/*.{ts,mjs} (allowJs) with
rewriteRelativeImportExtensions, so the .ts specifiers inside the .mjs shells
(runtime.mjs -> ./core.js, indexer.mjs -> ./providers/*.js, ./persist.js) are
rewritten to .js. declaration:false — the skill needs no .d.ts.
- packaging/skill-package.json provides the artifact's package.json; it sets
"type":"module" because the emitted .ts->.js files are ESM.
- build:skill runs tsc then copies schema.sql/SKILL.md/references/package.json.
Add tests/build-skill.test.mjs: runs the real build:skill, asserts the artifact
structure, that no emitted .js/.mjs still imports a .ts module, and that the
compiled artifact builds an index and answers a search end-to-end under plain
Node (no type-stripping) against a temp HOME.
Verified: suite 123/123, typecheck clean, artifact smoke run green.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
c964653362
commit
e44ab7a1da
+2
-1
@@ -9,7 +9,8 @@
|
||||
"test": "node --experimental-test-module-mocks --test tests/*.test.mjs",
|
||||
"typecheck": "tsc --noEmit && tsc --noEmit -p app/tsconfig.json",
|
||||
"lint": "eslint .",
|
||||
"build:core": "rm -rf dist && tsc -p tsconfig.build.json"
|
||||
"build:core": "rm -rf dist && tsc -p tsconfig.build.json",
|
||||
"build:skill": "rm -rf dist/obelisk-skill && tsc -p tsconfig.skill.json && cp scripts/schema.sql dist/obelisk-skill/scripts/ && cp SKILL.md dist/obelisk-skill/ && cp -R references dist/obelisk-skill/references && cp packaging/skill-package.json dist/obelisk-skill/package.json"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/js": "^10.0.1",
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"name": "obelisk-skill",
|
||||
"version": "0.1.0",
|
||||
"type": "module",
|
||||
"description": "Obelisk skill artifact — readable compiled Core (providers + persist + runtime) over local Claude Code and Codex history. Built by `npm run build:skill`; sources live in the main repo.",
|
||||
"license": "AGPL-3.0"
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
// Phase 6 acceptance: `npm run build:skill` must produce a runnable, readable,
|
||||
// .ts-free skill artifact under dist/obelisk-skill. This guards ADR-0004 (ship
|
||||
// readable non-bundled compiled JS) and catches import-rewriting / config drift
|
||||
// that would only surface when the installed skill runs under plain Node (no
|
||||
// type-stripping, no .ts resolution).
|
||||
import { test } from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { execFileSync, spawnSync } from 'node:child_process';
|
||||
import { readFileSync, readdirSync, mkdtempSync, mkdirSync, writeFileSync, rmSync, existsSync } from 'node:fs';
|
||||
import { tmpdir } from 'node:os';
|
||||
import { dirname, join, resolve } from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
|
||||
const repoRoot = resolve(dirname(fileURLToPath(import.meta.url)), '..');
|
||||
const skillDir = join(repoRoot, 'dist', 'obelisk-skill');
|
||||
|
||||
function walk(dir) {
|
||||
const out = [];
|
||||
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
||||
const full = join(dir, entry.name);
|
||||
if (entry.isDirectory()) out.push(...walk(full));
|
||||
else out.push(full);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
test('build:skill produces a runnable, readable, .ts-free skill artifact', () => {
|
||||
execFileSync('npm', ['run', 'build:skill'], { cwd: repoRoot, encoding: 'utf8', stdio: 'pipe' });
|
||||
|
||||
// Structure: compiled Core + copied .mjs + schema + docs + package.json.
|
||||
for (const rel of [
|
||||
'package.json', 'SKILL.md', 'references/api-reference.md',
|
||||
'scripts/core.js', 'scripts/persist.js', 'scripts/providers/claude.js',
|
||||
'scripts/providers/codex.js', 'scripts/runtime.mjs', 'scripts/indexer.mjs',
|
||||
'scripts/db.mjs', 'scripts/parsing.mjs', 'scripts/query.mjs', 'scripts/schema.sql',
|
||||
]) {
|
||||
assert.ok(existsSync(join(skillDir, rel)), `artifact missing ${rel}`);
|
||||
}
|
||||
assert.equal(JSON.parse(readFileSync(join(skillDir, 'package.json'), 'utf8')).type, 'module');
|
||||
|
||||
// Readable, not bundled: emitted files stay ~1:1 with source, and no relative
|
||||
// import may still point at a .ts file (that would break under plain Node).
|
||||
const jsFiles = walk(join(skillDir, 'scripts')).filter(f => f.endsWith('.js') || f.endsWith('.mjs'));
|
||||
assert.ok(jsFiles.length >= 6, 'expected multiple un-bundled script files');
|
||||
for (const file of jsFiles) {
|
||||
const src = readFileSync(file, 'utf8');
|
||||
assert.ok(!/from\s+['"][^'"]*\.ts['"]/.test(src), `${file} still imports a .ts module`);
|
||||
assert.ok(!/import\(['"][^'"]*\.ts['"]\)/.test(src), `${file} still dynamic-imports a .ts module`);
|
||||
}
|
||||
|
||||
// Runs end to end under plain Node against a fresh HOME (no type-stripping).
|
||||
const home = mkdtempSync(join(tmpdir(), 'obelisk-skill-artifact-'));
|
||||
try {
|
||||
const projDir = join(home, '.claude', 'projects', '-tmp-proj');
|
||||
mkdirSync(projDir, { recursive: true });
|
||||
writeFileSync(join(projDir, 'smoke.jsonl'),
|
||||
JSON.stringify({ uuid: 'm1', type: 'user', timestamp: '2026-06-10T10:00:00Z', cwd: '/tmp/proj', message: { role: 'user', content: 'hello artifact' } }) + '\n');
|
||||
const env = { ...process.env, HOME: home };
|
||||
const runtime = join(skillDir, 'scripts', 'runtime.mjs');
|
||||
|
||||
const build = spawnSync(process.execPath, [runtime, '--build'], { env, encoding: 'utf8' });
|
||||
assert.equal(build.status, 0, build.stderr || build.stdout);
|
||||
|
||||
const search = spawnSync(process.execPath, [runtime, '--search', 'hello artifact'], { env, encoding: 'utf8' });
|
||||
assert.equal(search.status, 0, search.stderr || search.stdout);
|
||||
const hits = JSON.parse(search.stdout);
|
||||
assert.equal(hits[0]?.message?.text, 'hello artifact', 'compiled artifact indexed and found the message');
|
||||
} finally {
|
||||
rmSync(home, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"extends": "./tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"noEmit": false,
|
||||
"outDir": "dist/obelisk-skill/scripts",
|
||||
"rootDir": "scripts",
|
||||
"declaration": false,
|
||||
"rewriteRelativeImportExtensions": true
|
||||
},
|
||||
"include": ["scripts/**/*.ts", "scripts/**/*.mjs"]
|
||||
}
|
||||
Reference in New Issue
Block a user