🎩 You're Invited:Meet the Socket team at Black Hat in Las Vegas, August 3-6.RSVP
Sign In

@sapiom/cli

Package Overview
Dependencies
Maintainers
3
Versions
21
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

@sapiom/cli - npm Package Compare versions

Comparing version
0.4.1
to
0.4.2
+327
dist/__tests__/dev.test.js
/**
* Tests for `sapiom dev`:
* 1. Command registration — `dev` is registered with the correct flags and
* allowUnknownOption so future harness flags don't error.
* 2. Harness resolution — injectable resolver seam: HARNESS_NOT_INSTALLED,
* HARNESS_BIN_NOT_FOUND, ERR_PACKAGE_PATH_NOT_EXPORTED.
* 3. Flag passthrough — buildHarnessArgv assembles the correct argv including
* unknown/extra flags forwarded verbatim.
* 4. Spawn behaviour — async spawn mocked: HARNESS_SPAWN_FAILED, exit code
* propagation, SIGTERM forwarding (child.kill asserted).
* 5. Analytics — command.run fires with flag names only; [dir] never leaks.
*
* No real harness server is started in any of these tests.
*/
import { EventEmitter } from 'node:events';
import { mkdtempSync, writeFileSync } from 'node:fs';
import * as os from 'node:os';
import * as path from 'node:path';
import { Command } from 'commander';
import { buildHarnessArgv, resolveHarnessBin, runDev } from '../commands/dev.js';
import { registerDevCommand } from '../commands/dev-register.js';
import { registerCommandAnalytics } from '../lib/analytics.js';
import { action } from '../commands/shared.js';
function makeFakeChild(opts = {}) {
const ee = new EventEmitter();
ee.kill = jest.fn();
if (opts.errorOnSpawn) {
setImmediate(() => ee.emit('error', opts.errorOnSpawn));
}
else {
setImmediate(() => ee.emit('close', opts.code ?? 0, opts.signal ?? null));
}
return ee;
}
// ---------------------------------------------------------------------------
// 1. Command registration
// ---------------------------------------------------------------------------
describe('sapiom dev — command registration', () => {
function buildMinimalProgram() {
const program = new Command('sapiom');
registerDevCommand(program);
return program;
}
it('registers a top-level "dev" command on the program', () => {
const program = buildMinimalProgram();
expect(program.commands.map((c) => c.name())).toContain('dev');
});
it('accepts --port, --no-open, --no-auth, --no-telemetry, --no-session', () => {
const program = buildMinimalProgram();
const devCmd = program.commands.find((c) => c.name() === 'dev');
const optionNames = devCmd.options.map((o) => o.long);
expect(optionNames).toContain('--port');
expect(optionNames).toContain('--no-open');
expect(optionNames).toContain('--no-auth');
expect(optionNames).toContain('--no-telemetry');
expect(optionNames).toContain('--no-session');
});
it('does not error when an unknown flag is passed', async () => {
// commander would throw/exit if allowUnknownOption() weren't set.
const program = new Command('sapiom');
registerDevCommand(program);
// Override the action after registration with a safe stub so runDev isn't called.
const devCmd = program.commands.find((c) => c.name() === 'dev');
devCmd.action(async () => { });
await expect(program.parseAsync(['dev', '--future-harness-flag'], { from: 'user' })).resolves.toBeDefined();
});
});
// ---------------------------------------------------------------------------
// 2. Harness resolution — injectable resolver
// ---------------------------------------------------------------------------
describe('resolveHarnessBin — injectable resolver', () => {
it('throws HARNESS_NOT_INSTALLED when resolver throws MODULE_NOT_FOUND', () => {
const err = Object.assign(new Error('Cannot find module'), { code: 'MODULE_NOT_FOUND' });
const resolver = {
resolvePackageJson() {
throw err;
},
};
expect(() => resolveHarnessBin(resolver)).toThrow(expect.objectContaining({ code: 'HARNESS_NOT_INSTALLED' }));
});
it('throws HARNESS_NOT_INSTALLED when resolver throws ERR_PACKAGE_PATH_NOT_EXPORTED', () => {
const err = Object.assign(new Error('Package path not exported'), {
code: 'ERR_PACKAGE_PATH_NOT_EXPORTED',
});
const resolver = {
resolvePackageJson() {
throw err;
},
};
expect(() => resolveHarnessBin(resolver)).toThrow(expect.objectContaining({ code: 'HARNESS_NOT_INSTALLED' }));
});
it('includes an install hint in HARNESS_NOT_INSTALLED', () => {
const resolver = {
resolvePackageJson() {
throw Object.assign(new Error('not found'), { code: 'MODULE_NOT_FOUND' });
},
};
let thrown = null;
try {
resolveHarnessBin(resolver);
}
catch (e) {
thrown = e;
}
expect(thrown?.hint).toContain('npm i -g @sapiom/harness');
});
it('throws HARNESS_BIN_NOT_FOUND when package.json has no bin field', () => {
const tmpDir = mkdtempSync(path.join(os.tmpdir(), 'sapiom-dev-test-'));
const pkgPath = path.join(tmpDir, 'package.json');
writeFileSync(pkgPath, JSON.stringify({ name: '@sapiom/harness', version: '0.1.1' }));
const resolver = { resolvePackageJson: () => pkgPath };
expect(() => resolveHarnessBin(resolver)).toThrow(expect.objectContaining({ code: 'HARNESS_BIN_NOT_FOUND' }));
});
it('throws HARNESS_BIN_NOT_FOUND when bin file does not exist on disk', () => {
const tmpDir = mkdtempSync(path.join(os.tmpdir(), 'sapiom-dev-test-'));
const pkgPath = path.join(tmpDir, 'package.json');
writeFileSync(pkgPath, JSON.stringify({ bin: { 'sapiom-harness': './dist/cli/bin.js' } }));
const resolver = { resolvePackageJson: () => pkgPath };
expect(() => resolveHarnessBin(resolver)).toThrow(expect.objectContaining({ code: 'HARNESS_BIN_NOT_FOUND' }));
});
});
// ---------------------------------------------------------------------------
// 3. Flag passthrough — argv construction (pure unit test, no process spawn)
// ---------------------------------------------------------------------------
describe('buildHarnessArgv', () => {
it('returns an empty array when no options are given', () => {
expect(buildHarnessArgv({})).toEqual([]);
});
it('places dir as the first positional argument', () => {
expect(buildHarnessArgv({ dir: '/my/project' })[0]).toBe('/my/project');
});
it('appends --port with its value', () => {
expect(buildHarnessArgv({ port: '4200' })).toEqual(['--port', '4200']);
});
it('appends --no-open when noOpen is true', () => {
expect(buildHarnessArgv({ noOpen: true })).toContain('--no-open');
});
it('does NOT append --no-open when noOpen is false or absent', () => {
expect(buildHarnessArgv({ noOpen: false })).not.toContain('--no-open');
expect(buildHarnessArgv({})).not.toContain('--no-open');
});
it('appends --no-auth when noAuth is true', () => {
expect(buildHarnessArgv({ noAuth: true })).toContain('--no-auth');
});
it('appends --no-telemetry when noTelemetry is true', () => {
expect(buildHarnessArgv({ noTelemetry: true })).toContain('--no-telemetry');
});
it('appends --no-session when noSession is true', () => {
expect(buildHarnessArgv({ noSession: true })).toContain('--no-session');
});
it('builds a full argv with dir and multiple flags', () => {
const opts = {
dir: '/workspace/my-app',
port: '4567',
noOpen: true,
noTelemetry: true,
};
expect(buildHarnessArgv(opts)).toEqual(['/workspace/my-app', '--port', '4567', '--no-open', '--no-telemetry']);
});
it('omits dir when not provided, even alongside flags', () => {
const argv = buildHarnessArgv({ port: '9000', noOpen: true });
expect(argv[0]).toBe('--port');
});
it('appends extraArgs (unknown flags) verbatim after known flags', () => {
const argv = buildHarnessArgv({
port: '4100',
extraArgs: ['--future-flag', '--debug'],
});
expect(argv).toEqual(['--port', '4100', '--future-flag', '--debug']);
});
it('unknown flags alone reach the child without error', () => {
expect(buildHarnessArgv({ extraArgs: ['--new-harness-only-flag'] })).toEqual([
'--new-harness-only-flag',
]);
});
});
// ---------------------------------------------------------------------------
// 4. Spawn behaviour — async spawn mocked
// ---------------------------------------------------------------------------
describe('runDev — spawn behaviour', () => {
let spawnMock;
let originalExitCode;
// A resolver that points at a real (fabricated) bin path so resolveHarnessBin passes.
function makeOkResolver() {
const tmpDir = mkdtempSync(path.join(os.tmpdir(), 'sapiom-dev-test-'));
const binPath = path.join(tmpDir, 'bin.js');
writeFileSync(binPath, '');
const pkgPath = path.join(tmpDir, 'package.json');
writeFileSync(pkgPath, JSON.stringify({ bin: { 'sapiom-harness': './bin.js' } }));
return { resolvePackageJson: () => pkgPath };
}
beforeEach(() => {
originalExitCode = process.exitCode;
// eslint-disable-next-line @typescript-eslint/no-var-requires
spawnMock = jest.spyOn(require('node:child_process'), 'spawn');
});
afterEach(() => {
spawnMock.mockRestore();
process.exitCode = originalExitCode;
});
it('throws HARNESS_SPAWN_FAILED when the child emits an error', async () => {
const fakeChild = makeFakeChild({ errorOnSpawn: new Error('ENOENT no such file') });
spawnMock.mockReturnValue(fakeChild);
await expect(runDev(undefined, {}, makeOkResolver())).rejects.toMatchObject({
code: 'HARNESS_SPAWN_FAILED',
});
});
it('propagates non-zero exit code to process.exitCode', async () => {
const fakeChild = makeFakeChild({ code: 2 });
spawnMock.mockReturnValue(fakeChild);
await runDev(undefined, {}, makeOkResolver());
expect(process.exitCode).toBe(2);
});
it('sets exitCode 0 (unchanged) on clean exit', async () => {
const fakeChild = makeFakeChild({ code: 0 });
spawnMock.mockReturnValue(fakeChild);
process.exitCode = undefined;
await runDev(undefined, {}, makeOkResolver());
expect(process.exitCode ?? 0).toBe(0);
});
it('sets 128+signum (143) when child exits via SIGTERM', async () => {
const fakeChild = makeFakeChild({ code: null, signal: 'SIGTERM' });
spawnMock.mockReturnValue(fakeChild);
await runDev(undefined, {}, makeOkResolver());
expect(process.exitCode).toBe(143); // 128 + 15
});
it('forwards SIGTERM to the child and does not throw', async () => {
let sigTermHandler;
const origOn = process.on.bind(process);
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const onSpy = jest.spyOn(process, 'on').mockImplementation((event, handler) => {
if (event === 'SIGTERM')
sigTermHandler = handler;
return origOn(event, handler);
});
const fakeChild = makeFakeChild({ code: 0 });
spawnMock.mockReturnValue(fakeChild);
const devPromise = runDev(undefined, {}, makeOkResolver());
// Fire SIGTERM before the child closes.
if (sigTermHandler)
sigTermHandler();
await devPromise;
expect(fakeChild.kill).toHaveBeenCalledWith('SIGTERM');
onSpy.mockRestore();
});
it('forwards rawArgs verbatim to the child spawn argv', async () => {
let capturedArgs;
spawnMock.mockImplementation((_execPath, args) => {
capturedArgs = args;
return makeFakeChild({ code: 0 });
});
const rawArgs = ['/my/project', '--port', '5000', '--future-harness-flag'];
await runDev(undefined, { rawArgs }, makeOkResolver());
// The spawned args are [harnessBin, ...rawArgs]; rawArgs start at index 1.
expect(capturedArgs?.slice(1)).toEqual(rawArgs);
});
});
function recordingTracker() {
const events = [];
return {
events,
tracker: {
track(eventType, data) {
events.push({ eventType, data: data ?? {} });
},
},
};
}
describe('sapiom dev — analytics', () => {
let stdoutSpy;
let stderrSpy;
let originalExitCode;
beforeEach(() => {
originalExitCode = process.exitCode;
stdoutSpy = jest.spyOn(process.stdout, 'write').mockImplementation(() => true);
stderrSpy = jest.spyOn(process.stderr, 'write').mockImplementation(() => true);
});
afterEach(() => {
stdoutSpy.mockRestore();
stderrSpy.mockRestore();
process.exitCode = originalExitCode;
});
function buildDevTestProgram(tracker, act = async () => { }) {
const program = new Command('sapiom');
registerCommandAnalytics(program, () => tracker);
program
.command('dev [dir]')
.option('--port <port>', 'port for the harness server')
.option('--no-open', 'skip browser open')
.option('--no-auth', 'skip auth')
.option('--no-telemetry', 'skip telemetry')
.option('--no-session', 'skip session')
.action(action(act));
return program;
}
it('emits command.run with command path "dev" and exit code 0', async () => {
const { tracker, events } = recordingTracker();
await buildDevTestProgram(tracker).parseAsync(['dev'], { from: 'user' });
expect(events).toHaveLength(1);
expect(events[0].eventType).toBe('command.run');
expect(events[0].data.command).toBe('dev');
expect(events[0].data.exit_code).toBe(0);
});
it('records --port and --no-open flag names, never their values or the dir positional', async () => {
const { tracker, events } = recordingTracker();
const SECRET_DIR = '/secret/project/path';
await buildDevTestProgram(tracker).parseAsync(['dev', SECRET_DIR, '--port', '9999', '--no-open'], { from: 'user' });
expect(events[0].data.flags).toEqual(['--port', '--no-open']);
const serialized = JSON.stringify(events[0].data);
expect(serialized).not.toContain(SECRET_DIR);
expect(serialized).not.toContain('/secret');
expect(serialized).not.toContain('9999');
});
it('records no flags when dev is called with only a positional [dir]', async () => {
const { tracker, events } = recordingTracker();
await buildDevTestProgram(tracker).parseAsync(['dev', '/some/dir'], { from: 'user' });
expect(events[0].data.flags).toEqual([]);
expect(JSON.stringify(events[0].data)).not.toContain('/some/dir');
});
it('records only the flags that were explicitly passed — no defaults', async () => {
const { tracker, events } = recordingTracker();
await buildDevTestProgram(tracker).parseAsync(['dev', '--no-auth', '--no-telemetry'], {
from: 'user',
});
expect(events[0].data.flags).toEqual(['--no-auth', '--no-telemetry']);
});
});
import { runDev } from './dev.js';
import { fail } from '../lib/output.js';
/**
* Register the `sapiom dev [dir]` command.
*
* The command spawns the `sapiom-harness` bin with stdio inherited, forwarding
* SIGTERM/SIGHUP and propagating the child's exit code. The program-level
* analytics hooks fire automatically via the preAction/postAction mechanism.
*
* Privacy: [dir] is a positional argument, not an option, so it never appears
* in specifiedFlagNames() and never reaches the analytics payload.
*
* Unknown flags are allowed via .allowUnknownOption() so that future harness
* flags (e.g. --some-new-flag) pass through without a CLI update. We forward
* everything after the "dev" token verbatim to the harness so the harness
* performs its own full argument parsing.
*/
export function registerDevCommand(program) {
program
.command('dev [dir]')
.description('Launch the Sapiom Harness — a local coding environment with MCP pre-wired.')
.option('--port <port>', 'port for the harness server')
.option('--no-open', 'skip opening the browser after the server starts')
.option('--no-auth', 'skip authentication (for offline/dev use)')
.option('--no-telemetry', 'disable harness telemetry collection')
.option('--no-session', 'skip creating an initial terminal session on boot')
// Allow flags not declared above; they pass through to the harness verbatim.
.allowUnknownOption()
.allowExcessArguments(true)
.action(async (dir, _opts) => {
try {
// Extract everything after the 'dev' token from the original argv and
// pass it raw to the harness. This avoids the commander ambiguity where
// unknown flags in front of [dir] get absorbed as the positional.
const rawArgs = sliceAfterDev(process.argv);
await runDev(dir, { rawArgs });
}
catch (err) {
fail(err);
}
});
}
/**
* Return all argv tokens that follow the first 'dev' token. This covers:
* node bin.js dev /mydir --port 4200 --future-flag
* sapiom dev --no-open /mydir
* The harness receives the same sequence the user typed.
*/
function sliceAfterDev(argv) {
const idx = argv.indexOf('dev');
return idx === -1 ? [] : argv.slice(idx + 1);
}
import { spawn } from 'node:child_process';
import { existsSync, readFileSync, realpathSync } from 'node:fs';
import { createRequire } from 'node:module';
import path from 'node:path';
import { CliError } from '../lib/output.js';
/**
* A CJS-compatible require bound to the CLI's real on-disk location.
*
* - CJS (ts-jest): __filename is defined — bind to this source file directly.
* - ESM production: __filename is not defined — resolve the REAL path of
* process.argv[1] before binding. This is the critical step: on a Unix
* global install, `npm i -g @sapiom/cli` places a symlink at e.g.
* `/usr/local/bin/sapiom` → `../lib/node_modules/@sapiom/cli/dist/bin.js`.
* `process.argv[1]` is the symlink path. `createRequire` does NOT follow
* symlinks internally, so resolution walks from `/usr/local/bin/` and never
* reaches `/usr/local/lib/node_modules/` where `@sapiom/harness` lives.
* `realpathSync` resolves the symlink to the real file, so the upward walk
* finds sibling global packages correctly. This is a no-op on Windows (no
* symlink), in the monorepo (already a real path), and in ts-jest (CJS
* __filename branch is taken instead).
*
* We avoid `import.meta.url` because ts-jest (CJS transform) rejects that
* syntax at parse time even inside unreachable branches.
*/
function getCliRequire() {
// CJS environment (ts-jest): __filename is always defined.
if (typeof __filename !== 'undefined') {
return createRequire(__filename);
}
// ESM production: realpath the argv[1] symlink before anchoring so that
// resolution walks from the package's actual on-disk location.
const entry = process.argv[1];
if (entry) {
try {
return createRequire(realpathSync(entry));
}
catch {
// realpathSync can fail if the path doesn't exist (e.g. piped stdin
// script). Fall back to the raw path; resolution may still work.
return createRequire(entry);
}
}
// Last resort — resolve from the process working directory.
return createRequire(process.cwd() + '/noop.js');
}
/** Default resolver: uses the real require.resolve. */
function defaultResolver() {
return {
resolvePackageJson() {
return getCliRequire().resolve('@sapiom/harness/package.json');
},
};
}
/**
* Resolve the `sapiom-harness` bin entry from the installed @sapiom/harness
* package.
*
* - ERR_MODULE_NOT_FOUND / MODULE_NOT_FOUND → HARNESS_NOT_INSTALLED
* - ERR_PACKAGE_PATH_NOT_EXPORTED → HARNESS_NOT_INSTALLED (with a
* hint to add "./package.json" to harness exports — we own the package so
* this should never occur in practice, but we name it explicitly so the
* error never mislabels a version mismatch as "not installed").
* - package.json found but bin absent → HARNESS_BIN_NOT_FOUND
*/
export function resolveHarnessBin(resolver = defaultResolver()) {
let pkgPath;
try {
pkgPath = resolver.resolvePackageJson();
}
catch (err) {
const code = err.code ?? '';
if (code === 'ERR_PACKAGE_PATH_NOT_EXPORTED') {
throw new CliError({
code: 'HARNESS_NOT_INSTALLED',
message: '@sapiom/harness package.json is not exported by its exports map.',
hint: 'Install or reinstall @sapiom/harness: npm i -g @sapiom/harness',
});
}
// MODULE_NOT_FOUND, ERR_MODULE_NOT_FOUND, and any other resolution error.
throw new CliError({
code: 'HARNESS_NOT_INSTALLED',
message: '@sapiom/harness is not installed.',
hint: 'Install it with: npm i -g @sapiom/harness',
});
}
// Read the bin field from package.json using fs (not require()) so we avoid
// loading the entire package just to find the entry path.
let pkg;
try {
pkg = JSON.parse(readFileSync(pkgPath, 'utf8'));
}
catch {
throw new CliError({
code: 'HARNESS_BIN_NOT_FOUND',
message: 'Could not read @sapiom/harness/package.json.',
hint: 'Try reinstalling: npm i -g @sapiom/harness',
});
}
const binEntry = typeof pkg.bin === 'string'
? pkg.bin
: typeof pkg.bin === 'object' && pkg.bin !== null
? (pkg.bin['sapiom-harness'] ?? Object.values(pkg.bin)[0])
: undefined;
if (!binEntry) {
throw new CliError({
code: 'HARNESS_BIN_NOT_FOUND',
message: 'Could not locate the sapiom-harness bin entry in @sapiom/harness.',
hint: 'Try reinstalling: npm i -g @sapiom/harness',
});
}
const binPath = path.resolve(path.dirname(pkgPath), binEntry);
if (!existsSync(binPath)) {
throw new CliError({
code: 'HARNESS_BIN_NOT_FOUND',
message: `sapiom-harness bin not found at ${binPath}.`,
hint: 'Try reinstalling: npm i -g @sapiom/harness',
});
}
return binPath;
}
/**
* Construct the argv array passed to the harness bin from typed options.
* Positional [dir] comes first (if provided), then known flags, then any extra
* flags forwarded verbatim. Used in unit tests and as documentation of the
* known flag set; production passes rawArgs directly.
*/
export function buildHarnessArgv(opts) {
const args = [];
if (opts.dir)
args.push(opts.dir);
if (opts.port)
args.push('--port', opts.port);
if (opts.noOpen)
args.push('--no-open');
if (opts.noAuth)
args.push('--no-auth');
if (opts.noTelemetry)
args.push('--no-telemetry');
if (opts.noSession)
args.push('--no-session');
if (opts.extraArgs && opts.extraArgs.length > 0)
args.push(...opts.extraArgs);
return args;
}
/**
* `sapiom dev [dir]` — launch the Sapiom Harness.
*
* Spawns the `sapiom-harness` bin with stdio inherited so the terminal is
* handed over cleanly. SIGTERM and SIGHUP are forwarded to the child process;
* SIGINT is intentionally NOT forwarded here — the TTY process group delivers
* it to both parent and child simultaneously, so double-forwarding would cause
* the child to receive it twice. The child's exit code is propagated; if the
* child is killed by a signal, the process exits with 128+signum per POSIX
* convention. The harness handles its own doctor check, auth, consent prompt,
* browser open, and startup banner.
*
* When `opts.rawArgs` is provided it is used as-is (production path). When
* absent, `buildHarnessArgv(opts)` is used (test/programmatic path).
*/
export async function runDev(dir, opts, resolver) {
const harnessBin = resolveHarnessBin(resolver);
const argv = opts.rawArgs !== undefined ? opts.rawArgs : buildHarnessArgv({ ...opts, dir });
await new Promise((resolve, reject) => {
const child = spawn(process.execPath, [harnessBin, ...argv], {
stdio: 'inherit',
env: process.env,
});
// Forward SIGTERM and SIGHUP to the child. SIGINT is handled by the TTY
// process group (Ctrl-C reaches both parent and child) — do NOT re-send it.
const forwardSigterm = () => {
child.kill('SIGTERM');
};
const forwardSighup = () => {
child.kill('SIGHUP');
};
process.on('SIGTERM', forwardSigterm);
process.on('SIGHUP', forwardSighup);
child.on('error', (err) => {
process.off('SIGTERM', forwardSigterm);
process.off('SIGHUP', forwardSighup);
reject(new CliError({
code: 'HARNESS_SPAWN_FAILED',
message: `Failed to launch sapiom-harness: ${err.message}`,
}));
});
child.on('close', (code, signal) => {
process.off('SIGTERM', forwardSigterm);
process.off('SIGHUP', forwardSighup);
if (signal) {
// Mirror POSIX 128+signum convention so callers / shell scripts can
// distinguish signal termination from a clean non-zero exit.
const signum = signalToNumber(signal);
process.exitCode = 128 + signum;
}
else if (code !== null && code !== 0) {
process.exitCode = code;
}
resolve();
});
});
}
/** Map a signal name to its POSIX number for exit-code propagation. */
function signalToNumber(signal) {
const table = {
SIGHUP: 1,
SIGINT: 2,
SIGQUIT: 3,
SIGKILL: 9,
SIGTERM: 15,
};
return table[signal] ?? 0;
}
+2
-0

@@ -5,2 +5,3 @@ import { Command } from 'commander';

import { registerAgentsCommands } from './commands/agents/index.js';
import { registerDevCommand } from './commands/dev-register.js';
import { registerCommandAnalytics } from './lib/analytics.js';

@@ -21,4 +22,5 @@ import { registerSandboxCommands } from './commands/sandbox/index.js';

registerAgentsCommands(program);
registerDevCommand(program);
registerSandboxCommands(program);
return program;
}
+12
-4
{
"name": "@sapiom/cli",
"version": "0.4.1",
"version": "0.4.2",
"description": "The Sapiom command-line interface — scaffold, validate, and ship Sapiom orchestrations.",

@@ -34,6 +34,14 @@ "license": "MIT",

"@sapiom/agent": "^0.6.2",
"@sapiom/agent-core": "^0.9.1",
"@sapiom/analytics-core": "^0.2.0",
"@sapiom/sandbox-preview": "^0.1.2"
"@sapiom/agent-core": "^0.9.2",
"@sapiom/analytics-core": "^0.2.1",
"@sapiom/sandbox-preview": "^0.1.3"
},
"peerDependencies": {
"@sapiom/harness": ">=0.1.2"
},
"peerDependenciesMeta": {
"@sapiom/harness": {
"optional": true
}
},
"devDependencies": {

@@ -40,0 +48,0 @@ "@types/jest": "^29.5.14",

@@ -11,2 +11,19 @@ # @sapiom/cli

## Harness
Launch a local coding environment with MCP pre-wired and your agent running in
an embedded terminal:
```sh
sapiom dev [dir] # open the harness in the current (or given) directory
sapiom dev --port 4200 # use a custom port
sapiom dev --no-open # skip opening the browser automatically
```
`sapiom dev` requires `@sapiom/harness` to be installed. Install it once with:
```sh
npm install -g @sapiom/harness
```
## Agents

@@ -13,0 +30,0 @@