Skip to content
Merged
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
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -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/
31 changes: 31 additions & 0 deletions cli/__tests__/sdk-bundle.test.mjs
Original file line number Diff line number Diff line change
@@ -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);
});
});
3 changes: 2 additions & 1 deletion cli/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
},
"files": [
"src",
"assets",
"README.md",
"skills"
],
Expand All @@ -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": {
Expand Down
20 changes: 20 additions & 0 deletions cli/scripts/bundle-sdk-examples.mjs
Original file line number Diff line number Diff line change
@@ -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);
}
21 changes: 11 additions & 10 deletions cli/src/commands/agent.js
Original file line number Diff line number Diff line change
Expand Up @@ -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`,
Expand Down
Loading