diff --git a/.opencode/package.json b/.opencode/package.json index e0b95bf..a286ab1 100644 --- a/.opencode/package.json +++ b/.opencode/package.json @@ -1,5 +1,6 @@ { + "type": "module", "dependencies": { - "@opencode-ai/plugin": "1.4.9" + "@opencode-ai/plugin": "1.4.10" } } diff --git a/.opencode/plugins/propulsion.ts b/.opencode/plugins/propulsion.js similarity index 81% rename from .opencode/plugins/propulsion.ts rename to .opencode/plugins/propulsion.js index 27c6a5c..1885750 100644 --- a/.opencode/plugins/propulsion.ts +++ b/.opencode/plugins/propulsion.js @@ -2,8 +2,6 @@ import fs from 'node:fs'; import path from 'node:path'; import { fileURLToPath } from 'node:url'; -import type { Plugin } from '@opencode-ai/plugin'; - const __dirname = path.dirname(fileURLToPath(import.meta.url)); const skillsDir = path.resolve(__dirname, '../../skills'); const additionalSkillsDir = path.resolve(__dirname, '../../additional/skills'); @@ -17,35 +15,7 @@ const propulsionWorkflowPath = path.join( 'SKILL.md', ); -type PropulsionHooks = Awaited>; -type PropulsionConfig = Parameters< - NonNullable ->[0] & { - skills?: { - paths?: string[]; - }; - command?: Record; -}; -type CommandFrontmatter = { - description?: string; - agent?: string; - model?: string; - subtask?: boolean; -}; - -type CommandDefinition = { - template: string; - description?: string; - agent?: string; - model?: string; - subtask?: boolean; -}; - -type PropulsionOptions = { - additional?: boolean; -}; - -const parseFrontmatterValue = (value: string): string | boolean => { +const parseFrontmatterValue = (value) => { const trimmed = value.trim(); if ( @@ -66,19 +36,14 @@ const parseFrontmatterValue = (value: string): string | boolean => { return trimmed; }; -const extractFrontmatter = ( - raw: string, -): { - frontmatter: CommandFrontmatter; - content: string; -} => { +const extractFrontmatter = (raw) => { const match = raw.match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n?([\s\S]*)$/); if (!match) { return { frontmatter: {}, content: raw }; } - const frontmatter: CommandFrontmatter = {}; + const frontmatter = {}; const frontmatterBlock = match[1] ?? ''; const content = match[2] ?? ''; @@ -129,7 +94,7 @@ const extractFrontmatter = ( return { frontmatter, content }; }; -const addSkillsPath = (config: PropulsionConfig, skillsPath: string) => { +const addSkillsPath = (config, skillsPath) => { config.skills = config.skills ?? {}; config.skills.paths = config.skills.paths ?? []; @@ -138,12 +103,12 @@ const addSkillsPath = (config: PropulsionConfig, skillsPath: string) => { } }; -const loadAdditionalCommands = (): Record => { +const loadAdditionalCommands = () => { if (!fs.existsSync(additionalCommandsDir)) { return {}; } - const commands: Record = {}; + const commands = {}; for (const entry of fs.readdirSync(additionalCommandsDir, { withFileTypes: true, @@ -176,10 +141,7 @@ const loadAdditionalCommands = (): Record => { return commands; }; -const mergeAdditionalCommands = ( - config: PropulsionConfig, - additionalCommands: Record, -) => { +const mergeAdditionalCommands = (config, additionalCommands) => { if (Object.keys(additionalCommands).length === 0) { return; } @@ -194,7 +156,7 @@ const mergeAdditionalCommands = ( } }; -const getBootstrapContent = (): string | null => { +const getBootstrapContent = () => { if (!fs.existsSync(propulsionWorkflowPath)) { return null; } @@ -213,8 +175,8 @@ ${content} `; }; -export const PropulsionPlugin: Plugin = async (_pluginInput, options = {}) => { - const { additional = false } = options as PropulsionOptions; +export const PropulsionPlugin = async (_pluginInput, options = {}) => { + const { additional = false } = options; const additionalCommands = additional ? loadAdditionalCommands() : {}; return { diff --git a/bun.lock b/bun.lock index 726b5c7..189a3e3 100644 --- a/bun.lock +++ b/bun.lock @@ -8,11 +8,9 @@ "@opencode-ai/plugin": "^1.4.0", }, "devDependencies": { - "@types/node": "^25.5.2", "oxfmt": "^0.44.0", "oxlint": "^1.59.0", "oxlint-tsgolint": "^0.20.0", - "typescript": "^6.0.2", }, }, }, @@ -109,8 +107,6 @@ "@oxlint/binding-win32-x64-msvc": ["@oxlint/binding-win32-x64-msvc@1.59.0", "", { "os": "win32", "cpu": "x64" }, "sha512-xkE7puteDS/vUyRngLXW0t8WgdWoS/tfxXjhP/P7SMqPDx+hs44SpssO3h3qmTqECYEuXBUPzcAw5257Ka+ofA=="], - "@types/node": ["@types/node@25.5.2", "", { "dependencies": { "undici-types": "~7.18.0" } }, "sha512-tO4ZIRKNC+MDWV4qKVZe3Ql/woTnmHDr5JD8UI5hn2pwBrHEwOEMZK7WlNb5RKB6EoJ02gwmQS9OrjuFnZYdpg=="], - "cross-spawn": ["cross-spawn@7.0.6", "", { "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" } }, "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA=="], "isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="], @@ -129,10 +125,6 @@ "tinypool": ["tinypool@2.1.0", "", {}, "sha512-Pugqs6M0m7Lv1I7FtxN4aoyToKg1C4tu+/381vH35y8oENM/Ai7f7C4StcoK4/+BSw9ebcS8jRiVrORFKCALLw=="], - "typescript": ["typescript@6.0.2", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-bGdAIrZ0wiGDo5l8c++HWtbaNCWTS4UTv7RaTH/ThVIgjkveJt83m74bBHMJkuCbslY8ixgLBVZJIOiQlQTjfQ=="], - - "undici-types": ["undici-types@7.18.2", "", {}, "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w=="], - "which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="], "zod": ["zod@4.1.8", "", {}, "sha512-5R1P+WwQqmmMIEACyzSvo4JXHY5WiAFHRMg+zBZKgKS+Q1viRa0C1hmUKtHltoIFKtIdki3pRxkmpP74jnNYHQ=="], diff --git a/package.json b/package.json index ca50681..54c5881 100644 --- a/package.json +++ b/package.json @@ -1,27 +1,46 @@ { "name": "propulsion", - "version": "0.8.0", + "version": "0.9.0", + "description": "Compact workflow skills for agentic coding in OpenCode.", + "homepage": "https://github.com/moonpixels/propulsion#readme", + "bugs": { + "url": "https://github.com/moonpixels/propulsion/issues" + }, + "license": "ISC", + "repository": { + "type": "git", + "url": "git+https://github.com/moonpixels/propulsion.git" + }, + "files": [ + ".opencode/package.json", + ".opencode/plugins/propulsion.js", + "skills", + "additional/commands", + "additional/skills", + "README.md", + "additional/README.md" + ], "type": "module", - "main": ".opencode/plugins/propulsion.ts", + "main": "./.opencode/plugins/propulsion.js", + "exports": { + ".": "./.opencode/plugins/propulsion.js" + }, "scripts": { "checks": "bun run lint && bun run format && bun run test", "format": "oxfmt .", "format:check": "oxfmt --check .", - "lint": "oxlint && bun run typecheck", + "lint": "oxlint", "test": "bun test ./tests && bash tests/opencode/run-tests.sh", "test:opencode": "bash tests/opencode/run-tests.sh", - "test:unit": "bun test ./tests", - "typecheck": "tsc --noEmit" + "test:unit": "bun test ./tests" }, "dependencies": { "@opencode-ai/plugin": "^1.4.0" }, "devDependencies": { - "@types/node": "^25.5.2", "oxfmt": "^0.44.0", "oxlint": "^1.59.0", - "oxlint-tsgolint": "^0.20.0", - "typescript": "^6.0.2" + "oxlint-tsgolint": "^0.20.0" }, "packageManager": "bun@1.3.11" } diff --git a/tests/opencode/test-skill-loading.sh b/tests/opencode/test-skill-loading.sh index e5fb9bb..fed8c6a 100644 --- a/tests/opencode/test-skill-loading.sh +++ b/tests/opencode/test-skill-loading.sh @@ -3,62 +3,29 @@ set -euo pipefail REPO_ROOT="$(cd "$(dirname "$0")/../.." && pwd)" -child_pids() { - ps -ax -o pid=,ppid= | awk -v parent="$1" '$2 == parent { print $1 }' -} - -kill_descendants() { - local child - - for child in $(child_pids "$1"); do - kill_descendants "$child" - kill "$child" 2>/dev/null || true - done -} - skills_output="$(mktemp)" pure_output="$(mktemp)" -trap 'rm -f "$skills_output" "$pure_output"' EXIT +config_home="$(mktemp -d)" +trap 'rm -f "$skills_output" "$pure_output"; rm -rf "$config_home"' EXIT cd "$REPO_ROOT" -(script -q "$skills_output" opencode debug skill | /bin/cat >/dev/null) & -skills_pid=$! - -attempt=0 -while [ "$attempt" -lt 20 ]; do - if /usr/bin/grep -a -q '"name": "propulsion-workflow"' "$skills_output"; then - break - fi - - sleep 1 - attempt=$((attempt + 1)) -done - -kill "$skills_pid" 2>/dev/null || true -kill_descendants "$skills_pid" -wait "$skills_pid" 2>/dev/null || true - -(script -q "$pure_output" opencode debug skill --pure | /bin/cat >/dev/null) & -pure_pid=$! -sleep 2 -kill "$pure_pid" 2>/dev/null || true -kill_descendants "$pure_pid" -wait "$pure_pid" 2>/dev/null || true - workflow_skill='"name": "propulsion-workflow"' workflow_path="$REPO_ROOT/skills/propulsion-workflow/SKILL.md" -if ! /usr/bin/grep -a -q "$workflow_skill" "$skills_output"; then +XDG_CONFIG_HOME="$config_home" opencode debug skill >"$skills_output" 2>&1 +XDG_CONFIG_HOME="$config_home" opencode debug skill --pure >"$pure_output" 2>&1 + +if ! /usr/bin/grep -F -a -q "$workflow_skill" "$skills_output"; then echo "Expected propulsion-workflow skill in plugin-backed skill list" exit 1 fi -if ! /usr/bin/grep -a -q "$workflow_path" "$skills_output"; then +if ! /usr/bin/grep -F -a -q "$workflow_path" "$skills_output"; then echo "Expected propulsion-workflow skill path in plugin-backed skill list" exit 1 fi -if /usr/bin/grep -a -q "$workflow_skill" "$pure_output"; then +if /usr/bin/grep -F -a -q "$workflow_skill" "$pure_output"; then echo "Pure skill listing should not expose propulsion-workflow" exit 1 fi diff --git a/tests/propulsion-plugin.test.js b/tests/propulsion-plugin.test.js index 65e9723..e4416d4 100644 --- a/tests/propulsion-plugin.test.js +++ b/tests/propulsion-plugin.test.js @@ -1,8 +1,11 @@ import { describe, expect, test } from 'bun:test'; +import { execFileSync } from 'node:child_process'; +import fs from 'node:fs'; +import os from 'node:os'; import path from 'node:path'; import { fileURLToPath } from 'node:url'; -import { PropulsionPlugin } from '../.opencode/plugins/propulsion.ts'; +import { PropulsionPlugin } from '../.opencode/plugins/propulsion.js'; const __dirname = path.dirname(fileURLToPath(import.meta.url)); const repoRoot = path.resolve(__dirname, '..'); @@ -107,3 +110,83 @@ describe('PropulsionPlugin transform', () => { ).toHaveLength(1); }); }); + +describe('published package contract', () => { + test('imports from node_modules under plain Node after npm pack', () => { + const tempRoot = fs.mkdtempSync( + path.join(os.tmpdir(), 'propulsion-plugin-test-'), + ); + const packDir = path.join(tempRoot, 'pack'); + + try { + fs.mkdirSync(packDir); + + const tarballName = execFileSync( + 'npm', + ['pack', '--quiet', '--pack-destination', packDir], + { + cwd: repoRoot, + encoding: 'utf8', + }, + ).trim(); + const tarballPath = path.join(packDir, tarballName); + const appDir = path.join(tempRoot, 'app'); + + fs.mkdirSync(appDir); + + execFileSync('npm', ['init', '-y'], { + cwd: appDir, + stdio: 'ignore', + }); + execFileSync('npm', ['install', tarballPath], { + cwd: appDir, + stdio: 'ignore', + }); + + const output = execFileSync( + 'node', + [ + '--input-type=module', + '-e', + "import('propulsion').then(() => console.log('IMPORT_OK'))", + ], + { + cwd: appDir, + encoding: 'utf8', + }, + ); + + expect(output).toContain('IMPORT_OK'); + } finally { + fs.rmSync(tempRoot, { recursive: true, force: true }); + } + }, 30000); + + test('opencode smoke test ignores ambient global config', () => { + const tempRoot = fs.mkdtempSync( + path.join(os.tmpdir(), 'propulsion-opencode-config-test-'), + ); + + try { + const configDir = path.join(tempRoot, 'config', 'opencode'); + fs.mkdirSync(configDir, { recursive: true }); + fs.writeFileSync( + path.join(configDir, 'opencode.json'), + '{ invalid json\n', + ); + + expect(() => + execFileSync('bash', ['tests/opencode/test-skill-loading.sh'], { + cwd: repoRoot, + env: { + ...process.env, + XDG_CONFIG_HOME: path.join(tempRoot, 'config'), + }, + stdio: 'pipe', + }), + ).not.toThrow(); + } finally { + fs.rmSync(tempRoot, { recursive: true, force: true }); + } + }, 30000); +}); diff --git a/tsconfig.json b/tsconfig.json deleted file mode 100644 index 2424c65..0000000 --- a/tsconfig.json +++ /dev/null @@ -1,25 +0,0 @@ -{ - "compilerOptions": { - "esModuleInterop": true, - "forceConsistentCasingInFileNames": true, - "isolatedModules": true, - "lib": ["ESNext"], - "module": "ESNext", - "moduleDetection": "force", - "moduleResolution": "bundler", - "noEmit": true, - "noFallthroughCasesInSwitch": true, - "noImplicitAny": true, - "noImplicitOverride": true, - "noUncheckedIndexedAccess": true, - "noUncheckedSideEffectImports": true, - "noUnusedLocals": true, - "noUnusedParameters": true, - "skipLibCheck": true, - "strict": true, - "target": "ESNext", - "types": ["node"], - "verbatimModuleSyntax": true - }, - "include": [".opencode/plugins/**/*.ts"] -}