diff --git a/.gitignore b/.gitignore index b0c8178eba..ff4f0c8bc9 100644 --- a/.gitignore +++ b/.gitignore @@ -61,3 +61,6 @@ frontend/.env.production.local # Playwright browser state (may contain session tokens) state.json __pycache__ + +# Generated by cli/scripts/bundle-sdk-examples.mjs at prepack +cli/assets/ diff --git a/cli/__tests__/sdk-bundle.test.mjs b/cli/__tests__/sdk-bundle.test.mjs new file mode 100644 index 0000000000..b9f6fed47f --- /dev/null +++ b/cli/__tests__/sdk-bundle.test.mjs @@ -0,0 +1,31 @@ +/** + * sdk-bundle.test.mjs — TASK-300. The npm package must ship the webhook SDK and + * bot that `agent init` copies. prepack runs bundle-sdk-examples.mjs, which + * copies them from examples/ into assets/. This pins that the bundled copies are + * byte-identical to the source, so the package cannot carry a stale SDK. + */ + +import { beforeAll, describe, expect, it } from '@jest/globals'; +import { execFileSync } from 'child_process'; +import fs from 'fs'; +import path from 'path'; + +const CLI_ROOT = path.resolve(path.dirname(new URL(import.meta.url).pathname), '..'); +const REPO_ROOT = path.resolve(CLI_ROOT, '..'); + +const PAIRS = [ + [path.join(REPO_ROOT, 'examples', 'sdk', 'python', 'commonly.py'), path.join('python', 'commonly.py')], + [path.join(REPO_ROOT, 'examples', 'hello-world-python', 'bot.py'), path.join('python', 'bot.py')], +]; + +describe('bundled webhook SDK (TASK-300)', () => { + beforeAll(() => { + execFileSync(process.execPath, [path.join(CLI_ROOT, 'scripts', 'bundle-sdk-examples.mjs')]); + }); + + it.each(PAIRS)('ships %s byte-identical to the examples/ source', (src, rel) => { + const bundled = path.join(CLI_ROOT, 'assets', 'webhook-sdk', rel); + expect(fs.existsSync(bundled)).toBe(true); + expect(fs.readFileSync(bundled).equals(fs.readFileSync(src))).toBe(true); + }); +}); diff --git a/cli/package.json b/cli/package.json index fc5a0e3441..b10716dd97 100644 --- a/cli/package.json +++ b/cli/package.json @@ -10,6 +10,7 @@ }, "files": [ "src", + "assets", "README.md", "skills" ], @@ -32,7 +33,7 @@ "lint:fix": "eslint src --ext .js --fix", "test": "node --experimental-vm-modules node_modules/.bin/jest", "test:temp-cleanup": "node scripts/verify-temp-cleanup.mjs", - "prepack": "node scripts/require-bundled-seats.mjs", + "prepack": "node scripts/require-bundled-seats.mjs && node scripts/bundle-sdk-examples.mjs", "prepublishOnly": "node -e \"require('fs').copyFileSync('../docs/agents/skills/commonly/SKILL.md','skills/commonly/SKILL.md')\"" }, "dependencies": { diff --git a/cli/scripts/bundle-sdk-examples.mjs b/cli/scripts/bundle-sdk-examples.mjs new file mode 100644 index 0000000000..5e61e40e29 --- /dev/null +++ b/cli/scripts/bundle-sdk-examples.mjs @@ -0,0 +1,20 @@ +// prepack: copy the webhook SDK and the hello-world bot out of examples/ into +// cli/assets/webhook-sdk/ so the published package ships them. agent init reads +// them from the package. The copies are generated and gitignored; examples/ +// stays the source of truth. +import { copyFileSync, mkdirSync } from 'fs'; +import { dirname, join, resolve } from 'path'; +import { fileURLToPath } from 'url'; + +const cliRoot = resolve(dirname(fileURLToPath(import.meta.url)), '..'); +const repoRoot = resolve(cliRoot, '..'); +const bundles = [ + [join(repoRoot, 'examples', 'sdk', 'python', 'commonly.py'), join('python', 'commonly.py')], + [join(repoRoot, 'examples', 'hello-world-python', 'bot.py'), join('python', 'bot.py')], +]; + +for (const [src, rel] of bundles) { + const dest = join(cliRoot, 'assets', 'webhook-sdk', rel); + mkdirSync(dirname(dest), { recursive: true }); + copyFileSync(src, dest); +} diff --git a/cli/src/commands/agent.js b/cli/src/commands/agent.js index 15004bee07..fb470e48cc 100644 --- a/cli/src/commands/agent.js +++ b/cli/src/commands/agent.js @@ -3298,19 +3298,20 @@ const SUPPORTED_LANGUAGES = ['python']; // three levels up. The scaffolder copies the canonical SDK file from the repo // into the user's cwd; ADR-006 §SDK lives = "live-copy, not dependency". // -// Removal condition: ADR-006 §Migration path Phase 4 — when the CLI is -// published as `@commonlyai/cli` on npm, the example files won't sit at a -// repo-relative path; we'll need to bundle them into the package at build -// time and resolve via `import.meta.resolve` or a packaged-data lookup. -// Until that phase, repo-relative is the simplest correct answer. -const repoFile = (...parts) => pathResolve( - dirname(fileURLToPath(import.meta.url)), '..', '..', '..', ...parts, -); +// The published package carries these files under cli/assets/webhook-sdk/, +// bundled from examples/ by scripts/bundle-sdk-examples.mjs at prepack (npm +// pack and publish run it). A repo clone that has not packed yet falls back to +// the examples/ tree, which stays the single source of truth. +const cliRoot = pathResolve(dirname(fileURLToPath(import.meta.url)), '..', '..'); +const sdkAsset = (bundledRel, repoRel) => { + const bundled = pathResolve(cliRoot, 'assets', 'webhook-sdk', bundledRel); + return existsSync(bundled) ? bundled : pathResolve(cliRoot, '..', 'examples', repoRel); +}; const SDK_SOURCES = { python: { - sdkSrc: () => repoFile('examples', 'sdk', 'python', 'commonly.py'), - botSrc: () => repoFile('examples', 'hello-world-python', 'bot.py'), + sdkSrc: () => sdkAsset('python/commonly.py', join('sdk', 'python', 'commonly.py')), + botSrc: () => sdkAsset('python/bot.py', join('hello-world-python', 'bot.py')), sdkOut: 'commonly.py', botOut: (name) => `${name}.py`, runHint: (name) => `COMMONLY_TOKEN=$(cat .commonly-env) python3 ${name}.py`,