@eventmodelers/cli 0.0.29 → 0.0.30

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/cli.js +86 -6
  2. package/package.json +1 -1
package/cli.js CHANGED
@@ -14,7 +14,7 @@ import {
14
14
  readFileSync,
15
15
  appendFileSync,
16
16
  } from 'fs';
17
- import { execSync, spawn } from 'child_process';
17
+ import { execSync, execFileSync, spawn } from 'child_process';
18
18
  import { createInterface, emitKeypressEvents, moveCursor, clearScreenDown } from 'readline';
19
19
  import { homedir } from 'os';
20
20
  import { randomUUID } from 'crypto';
@@ -546,6 +546,55 @@ function copyDirContents(srcDir, destDir, { skip = [] } = {}) {
546
546
  if (count) console.log(` ✓ Installed ${count} item${count === 1 ? '' : 's'} into ${relative(process.cwd(), destDir) || '.'}`);
547
547
  }
548
548
 
549
+ // Community/custom build kits (`init --stack <name> --git <url>`) are cloned fresh on
550
+ // every run rather than pulled/updated in place — there's no local state worth
551
+ // preserving between installs, and re-cloning from scratch means a broken or partial
552
+ // previous clone can never linger and cause a confusing stale-file bug. Cached under
553
+ // ~/.eventmodelers (not inside the target project) so it's reusable across projects and
554
+ // definitely not something `uninstall` or the project's own .gitignore need to know about.
555
+ function cloneGitStack(url, branch) {
556
+ const dest = join(homedir(), '.eventmodelers', 'git-stacks', `${url}${branch ? `#${branch}` : ''}`.replace(/[^a-zA-Z0-9._-]/g, '_'));
557
+ rmSync(dest, { recursive: true, force: true });
558
+ mkdirSync(dirname(dest), { recursive: true });
559
+ console.log(`📥 Cloning ${url}${branch ? ` (branch: ${branch})` : ''}...`);
560
+ const cloneArgs = ['clone', '--depth', '1', ...(branch ? ['--branch', branch] : []), url, dest];
561
+ try {
562
+ execFileSync('git', cloneArgs, { stdio: ['ignore', 'inherit', 'inherit'] });
563
+ } catch {
564
+ console.error(`❌ Failed to clone "${url}"${branch ? ` (branch: ${branch})` : ''} — check the URL, the branch name, your git access, and that git is installed, then try again.`);
565
+ process.exit(1);
566
+ }
567
+ return dest;
568
+ }
569
+
570
+ // A community stack repo must mirror the internal stacks/<key>/templates layout exactly
571
+ // (templates/.claude, templates/root, templates/<kitSubdir>) so installStack() can treat
572
+ // it identically to a built-in stack — see STACKS above for the shape. An optional
573
+ // stack.json at the repo root can declare label/kitSubdir/useShared/needsBoardId;
574
+ // kitDirName is always forced to .build-kit (the same runtime contract every built-in
575
+ // stack already uses), so run/status/uninstall recognize a git-installed stack with no
576
+ // changes of their own.
577
+ function resolveGitStackConfig(clonedDir, name) {
578
+ const templatesSource = join(clonedDir, 'templates');
579
+ if (!existsSync(templatesSource)) {
580
+ console.error(`❌ ${relative(process.cwd(), clonedDir) || clonedDir} has no templates/ directory — a build kit repo needs templates/.claude, templates/root, and templates/<kitSubdir>, the same layout this CLI's own stacks/<name>/templates use.`);
581
+ process.exit(1);
582
+ }
583
+ const manifest = readJsonSafe(join(clonedDir, 'stack.json'));
584
+ const kitSubdir = manifest.kitSubdir || 'build-kit';
585
+ if (!existsSync(join(templatesSource, kitSubdir))) {
586
+ console.error(`❌ ${relative(process.cwd(), templatesSource) || templatesSource} is missing its kit subdirectory "${kitSubdir}/" (from stack.json, or the "build-kit" default) — nothing to install.`);
587
+ process.exit(1);
588
+ }
589
+ return {
590
+ label: manifest.label || name,
591
+ kitSubdir,
592
+ kitDirName: '.build-kit',
593
+ useShared: manifest.useShared !== false,
594
+ needsBoardId: manifest.needsBoardId !== false,
595
+ };
596
+ }
597
+
549
598
  async function resolveStack(cliStack) {
550
599
  if (cliStack) {
551
600
  if (!STACKS[cliStack]) {
@@ -566,7 +615,9 @@ async function installStack(stackKey, stackCfg, options = {}) {
566
615
  console.log(`Using: ${stackKey} (${stackCfg.label})\n`);
567
616
 
568
617
  const targetDir = process.cwd();
569
- const templatesSource = join(__dirname, 'stacks', stackKey, 'templates');
618
+ // `init --git <url>` passes a resolved clone dir's templates/ here instead — every
619
+ // other input (STACKS, MODELING_KIT, BRIDGE_KIT) keeps using the built-in path.
620
+ const templatesSource = options.templatesSource || join(__dirname, 'stacks', stackKey, 'templates');
570
621
  const sharedBuildKit = join(__dirname, 'shared', 'build-kit');
571
622
  // Skills with no stack-specific content (connect, learn-eventmodelers-api,
572
623
  // update-slice-status, ...) live once here instead of being copy-pasted into
@@ -1162,8 +1213,10 @@ function credentialOverridesFromOpts(opts) {
1162
1213
  credentialFlags(program
1163
1214
  .command('init')
1164
1215
  .alias('install')
1165
- .description('Scaffold a stack + install the agent kit into the current directory (or --modeling for skills + agent loop only, no backend scaffold; or --bridge to translate board slices into another spec framework)')
1166
- .option('--stack <name>', `Stack to install (${Object.keys(STACKS).join(', ')})`)
1216
+ .description('Scaffold a stack + install the agent kit into the current directory (or --modeling for skills + agent loop only, no backend scaffold; or --bridge to translate board slices into another spec framework; or --git to install a community/custom build kit from a git repo)')
1217
+ .option('--stack <name>', `Stack to install (${Object.keys(STACKS).join(', ')}), or a name of your choosing when combined with --git`)
1218
+ .option('--git <url>', 'Install a build kit not built into this CLI by cloning this git repo (used with --stack <name> to name it) — the repo must mirror the templates/.claude, templates/root, templates/<kitSubdir> layout of this CLI\'s own stacks/<name>/templates, optionally with a stack.json declaring label/kitSubdir/useShared/needsBoardId')
1219
+ .option('--branch <name>', 'Branch to clone — only meaningful with --git (defaults to the repo\'s default branch)')
1167
1220
  .option('--modeling', 'Install skills + the agent loop only — no backend scaffold. Mutually exclusive with --stack/--bridge.')
1168
1221
  .option('--bridge', 'Install a bridge kit — translates board slices into another spec framework instead of building code. Mutually exclusive with --stack/--modeling. Requires --target.')
1169
1222
  .option('--target <name>', `Bridge target framework (${Object.keys(BRIDGE_TARGETS).join(', ')}) — only meaningful with --bridge`)
@@ -1174,8 +1227,8 @@ credentialFlags(program
1174
1227
  const globalOpts = command.optsWithGlobals();
1175
1228
 
1176
1229
  if (opts.modeling || opts.bridge) {
1177
- if (opts.stack || (opts.modeling && opts.bridge)) {
1178
- console.error('❌ --stack, --modeling, and --bridge are mutually exclusive — pick one.');
1230
+ if (opts.stack || opts.git || (opts.modeling && opts.bridge)) {
1231
+ console.error('❌ --stack/--git, --modeling, and --bridge are mutually exclusive — pick one.');
1179
1232
  process.exit(1);
1180
1233
  }
1181
1234
  }
@@ -1222,6 +1275,33 @@ credentialFlags(program
1222
1275
  return;
1223
1276
  }
1224
1277
 
1278
+ if (opts.branch && !opts.git) {
1279
+ console.error('❌ --branch only applies to --git — nothing to clone without it.');
1280
+ process.exit(1);
1281
+ }
1282
+
1283
+ if (opts.git) {
1284
+ if (!opts.stack) {
1285
+ console.error('❌ --git requires --stack <name> to name the installed stack.');
1286
+ process.exit(1);
1287
+ }
1288
+ if (STACKS[opts.stack]) {
1289
+ console.error(`❌ "${opts.stack}" is already a built-in stack (${Object.keys(STACKS).join(', ')}) — --git is only for installing a stack that isn't built in.`);
1290
+ process.exit(1);
1291
+ }
1292
+ const clonedDir = cloneGitStack(opts.git, opts.branch);
1293
+ const stackCfg = resolveGitStackConfig(clonedDir, opts.stack);
1294
+ await installStack(opts.stack, stackCfg, {
1295
+ configPath: globalOpts.config,
1296
+ print: globalOpts.print,
1297
+ global: opts.global,
1298
+ force: opts.force,
1299
+ credentialOverrides: credentialOverridesFromOpts(opts),
1300
+ templatesSource: join(clonedDir, 'templates'),
1301
+ });
1302
+ return;
1303
+ }
1304
+
1225
1305
  const stackKey = await resolveStack(opts.stack);
1226
1306
  await installStack(stackKey, STACKS[stackKey], {
1227
1307
  configPath: globalOpts.config,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@eventmodelers/cli",
3
- "version": "0.0.29",
3
+ "version": "0.0.30",
4
4
  "description": "Eventmodelers CLI — real-time Claude agent + skills for Claude Code, for any stack (Node, Supabase, Axon, Cratis, or modeling-only)",
5
5
  "type": "module",
6
6
  "bin": {