jfl 0.7.0 → 0.7.2

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.
package/README.md CHANGED
@@ -4,10 +4,6 @@
4
4
  <strong>Multiplayer AI. Persistent context. Self-driving agents.</strong>
5
5
  </p>
6
6
 
7
- <p align="center">
8
- <img src="https://raw.githubusercontent.com/402goose/jfl-cli/main/.github/assets/jfl-kanban-demo.gif" width="700" alt="JFL Dashboard — Kanban, Eval, Agents" />
9
- </p>
10
-
11
7
  <p align="center">
12
8
  <a href="https://www.npmjs.com/package/jfl"><img src="https://img.shields.io/npm/v/jfl.svg" alt="npm version" /></a>
13
9
  <a href="https://github.com/402goose/jfl-cli/actions"><img src="https://img.shields.io/github/actions/workflow/status/402goose/jfl-cli/ci.yml?branch=main" alt="CI" /></a>
@@ -1,4 +1,15 @@
1
- export declare function initCommand(options?: {
1
+ interface InitOptions {
2
2
  name?: string;
3
- }): Promise<void>;
3
+ type?: string;
4
+ description?: string;
5
+ productRepo?: string;
6
+ service?: string[];
7
+ ci?: boolean;
8
+ interactive?: boolean;
9
+ search?: boolean;
10
+ services?: boolean;
11
+ launch?: boolean;
12
+ }
13
+ export declare function initCommand(options?: InitOptions): Promise<void>;
14
+ export {};
4
15
  //# sourceMappingURL=init.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"init.d.ts","sourceRoot":"","sources":["../../src/commands/init.ts"],"names":[],"mappings":"AAoBA,wBAAsB,WAAW,CAAC,OAAO,CAAC,EAAE;IAAE,IAAI,CAAC,EAAE,MAAM,CAAA;CAAE,iBAsgC5D"}
1
+ {"version":3,"file":"init.d.ts","sourceRoot":"","sources":["../../src/commands/init.ts"],"names":[],"mappings":"AAoBA,UAAU,WAAW;IACnB,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,OAAO,CAAC,EAAE,MAAM,EAAE,CAAA;IAClB,EAAE,CAAC,EAAE,OAAO,CAAA;IACZ,WAAW,CAAC,EAAE,OAAO,CAAA;IACrB,MAAM,CAAC,EAAE,OAAO,CAAA;IAChB,QAAQ,CAAC,EAAE,OAAO,CAAA;IAClB,MAAM,CAAC,EAAE,OAAO,CAAA;CACjB;AAED,wBAAsB,WAAW,CAAC,OAAO,CAAC,EAAE,WAAW,iBA6lCtD"}
@@ -16,6 +16,7 @@ import { persistProjectPort } from "../utils/context-hub-port.js";
16
16
  import { ensureDaemonInstalled } from "./context-hub.js";
17
17
  const TEMPLATE_REPO = "https://github.com/402goose/jfl-template.git";
18
18
  export async function initCommand(options) {
19
+ const interactive = options?.interactive !== false && process.stdout.isTTY !== false;
19
20
  // Start Clawdbot-style flow
20
21
  p.intro(chalk.hex("#FFD700")("┌ JFL - Initialize Workspace"));
21
22
  // Check authentication - owner needs to be verified
@@ -49,7 +50,7 @@ export async function initCommand(options) {
49
50
  }
50
51
  // Get project name
51
52
  let projectName = options?.name;
52
- if (!projectName) {
53
+ if (!projectName && interactive) {
53
54
  const name = await p.text({
54
55
  message: "Project name:",
55
56
  placeholder: "my-project-gtm",
@@ -67,17 +68,27 @@ export async function initCommand(options) {
67
68
  }
68
69
  projectName = name;
69
70
  }
71
+ if (!projectName) {
72
+ // Derive from current directory name
73
+ projectName = basename(process.cwd()).replace(/[^a-z0-9-]/g, "-").toLowerCase() || "my-project";
74
+ if (!interactive) {
75
+ p.log.info(`Using project name: ${projectName} (pass -n to override)`);
76
+ }
77
+ }
70
78
  // Ask workspace type
71
- const workspaceType = await p.select({
72
- message: "Workspace type:",
73
- options: [
74
- { label: "GTM", value: "gtm", hint: "Single product workspace" },
75
- { label: "Portfolio", value: "portfolio", hint: "Manages multiple products" },
76
- ],
77
- });
78
- if (p.isCancel(workspaceType)) {
79
- p.cancel("Setup cancelled.");
80
- process.exit(0);
79
+ let workspaceType = options?.type ?? "gtm";
80
+ if (interactive && !options?.type) {
81
+ workspaceType = await p.select({
82
+ message: "Workspace type:",
83
+ options: [
84
+ { label: "GTM", value: "gtm", hint: "Single product workspace" },
85
+ { label: "Portfolio", value: "portfolio", hint: "Manages multiple products" },
86
+ ],
87
+ });
88
+ if (p.isCancel(workspaceType)) {
89
+ p.cancel("Setup cancelled.");
90
+ process.exit(0);
91
+ }
81
92
  }
82
93
  const isPortfolio = workspaceType === "portfolio";
83
94
  // If cwd already matches the project name, initialize in place
@@ -185,13 +196,16 @@ export async function initCommand(options) {
185
196
  validationSpinner.warn("Could not validate settings.json");
186
197
  }
187
198
  }
188
- const description = await p.text({
189
- message: "One-line description:",
190
- placeholder: "My project",
191
- });
192
- if (p.isCancel(description)) {
193
- p.cancel("Setup cancelled.");
194
- process.exit(0);
199
+ let description = options?.description || projectName;
200
+ if (interactive) {
201
+ description = await p.text({
202
+ message: "One-line description:",
203
+ placeholder: "My project",
204
+ });
205
+ if (p.isCancel(description)) {
206
+ p.cancel("Setup cancelled.");
207
+ process.exit(0);
208
+ }
195
209
  }
196
210
  let productRepo = null;
197
211
  const portfolioChildGtms = [];
@@ -200,10 +214,13 @@ export async function initCommand(options) {
200
214
  p.note("Register existing GTM workspaces under this portfolio.\n" +
201
215
  "Each GTM gets its own eval pipeline, journals, and event bus.\n" +
202
216
  "The portfolio aggregates across all of them.", chalk.hex("#FFA500")("Portfolio Setup"));
203
- const registerGtms = await p.confirm({
204
- message: "Register GTM workspaces now?",
205
- initialValue: true,
206
- });
217
+ let registerGtms = false;
218
+ if (interactive) {
219
+ registerGtms = await p.confirm({
220
+ message: "Register GTM workspaces now?",
221
+ initialValue: true,
222
+ });
223
+ }
207
224
  if (!p.isCancel(registerGtms) && registerGtms) {
208
225
  let adding = true;
209
226
  let gtmCount = 0;
@@ -262,44 +279,53 @@ export async function initCommand(options) {
262
279
  }
263
280
  if (!isPortfolio) {
264
281
  // Ask about product repo (registered as service, NOT a submodule)
265
- const productChoice = await p.select({
266
- message: "Product repo:",
267
- options: [
268
- {
269
- label: "I have an existing repo",
270
- value: "existing",
271
- hint: "Register as a service"
272
- },
273
- {
274
- label: "Create a new repo for me",
275
- value: "create",
276
- hint: "Requires: gh CLI"
277
- },
278
- {
279
- label: "I'll add it later",
280
- value: "later",
281
- hint: "Recommended if unsure"
282
- },
283
- ],
284
- });
285
- if (p.isCancel(productChoice)) {
286
- p.cancel("Setup cancelled.");
287
- process.exit(0);
282
+ let productChoice = "later";
283
+ if (interactive) {
284
+ productChoice = await p.select({
285
+ message: "Product repo:",
286
+ options: [
287
+ {
288
+ label: "I have an existing repo",
289
+ value: "existing",
290
+ hint: "Register as a service"
291
+ },
292
+ {
293
+ label: "Create a new repo for me",
294
+ value: "create",
295
+ hint: "Requires: gh CLI"
296
+ },
297
+ {
298
+ label: "I'll add it later",
299
+ value: "later",
300
+ hint: "Recommended if unsure"
301
+ },
302
+ ],
303
+ });
304
+ if (p.isCancel(productChoice)) {
305
+ p.cancel("Setup cancelled.");
306
+ process.exit(0);
307
+ }
308
+ }
309
+ else if (options?.productRepo) {
310
+ productChoice = "existing";
288
311
  }
289
312
  if (productChoice !== "later") {
290
313
  if (productChoice === "existing") {
291
- const repoUrl = await p.text({
292
- message: "Product repo URL:",
293
- placeholder: "https://github.com/user/repo.git",
294
- validate: (input) => {
295
- if (!input.trim()) {
296
- return "Please enter a repo URL";
297
- }
298
- },
299
- });
300
- if (p.isCancel(repoUrl)) {
301
- p.cancel("Setup cancelled.");
302
- process.exit(0);
314
+ let repoUrl = options?.productRepo || "";
315
+ if (interactive) {
316
+ repoUrl = await p.text({
317
+ message: "Product repo URL:",
318
+ placeholder: "https://github.com/user/repo.git",
319
+ validate: (input) => {
320
+ if (!input.trim()) {
321
+ return "Please enter a repo URL";
322
+ }
323
+ },
324
+ });
325
+ if (p.isCancel(repoUrl)) {
326
+ p.cancel("Setup cancelled.");
327
+ process.exit(0);
328
+ }
303
329
  }
304
330
  productRepo = repoUrl;
305
331
  p.log.success(`Product repo registered: ${productRepo}`);
@@ -314,37 +340,43 @@ export async function initCommand(options) {
314
340
  const maxAttempts = 3;
315
341
  while (!repoCreated && attemptCount < maxAttempts) {
316
342
  attemptCount++;
317
- const repoName = await p.text({
318
- message: attemptCount > 1 ? "Try a different name:" : "New repo name:",
319
- placeholder: attemptCount > 1
320
- ? `${projectName.replace(/-gtm$/, "")}-${attemptCount}`
321
- : projectName.replace(/-gtm$/, ""),
322
- validate: (input) => {
323
- if (!input.trim()) {
324
- return "Please enter a repo name";
325
- }
326
- if (/\s/.test(input)) {
327
- return "Repo names cannot contain spaces. Use hyphens instead (e.g., 'my-project')";
328
- }
329
- if (!/^[a-zA-Z0-9._-]+$/.test(input)) {
330
- return "Repo names can only contain letters, numbers, hyphens, underscores, and dots";
331
- }
332
- },
333
- });
334
- if (p.isCancel(repoName)) {
335
- p.cancel("Setup cancelled.");
336
- process.exit(0);
343
+ let repoName = projectName.replace(/-gtm$/, "");
344
+ if (interactive) {
345
+ repoName = await p.text({
346
+ message: attemptCount > 1 ? "Try a different name:" : "New repo name:",
347
+ placeholder: attemptCount > 1
348
+ ? `${projectName.replace(/-gtm$/, "")}-${attemptCount}`
349
+ : projectName.replace(/-gtm$/, ""),
350
+ validate: (input) => {
351
+ if (!input.trim()) {
352
+ return "Please enter a repo name";
353
+ }
354
+ if (/\s/.test(input)) {
355
+ return "Repo names cannot contain spaces. Use hyphens instead (e.g., 'my-project')";
356
+ }
357
+ if (!/^[a-zA-Z0-9._-]+$/.test(input)) {
358
+ return "Repo names can only contain letters, numbers, hyphens, underscores, and dots";
359
+ }
360
+ },
361
+ });
362
+ if (p.isCancel(repoName)) {
363
+ p.cancel("Setup cancelled.");
364
+ process.exit(0);
365
+ }
337
366
  }
338
- const visibility = await p.select({
339
- message: "Visibility:",
340
- options: [
341
- { label: "Private", value: "private" },
342
- { label: "Public", value: "public" },
343
- ],
344
- });
345
- if (p.isCancel(visibility)) {
346
- p.cancel("Setup cancelled.");
347
- process.exit(0);
367
+ let visibility = "private";
368
+ if (interactive) {
369
+ visibility = await p.select({
370
+ message: "Visibility:",
371
+ options: [
372
+ { label: "Private", value: "private" },
373
+ { label: "Public", value: "public" },
374
+ ],
375
+ });
376
+ if (p.isCancel(visibility)) {
377
+ p.cancel("Setup cancelled.");
378
+ process.exit(0);
379
+ }
348
380
  }
349
381
  const createSpinner = ora("Creating product repo...").start();
350
382
  try {
@@ -373,10 +405,13 @@ export async function initCommand(options) {
373
405
  console.log(chalk.yellow("That name is already taken on your GitHub"));
374
406
  console.log("");
375
407
  if (attemptCount < maxAttempts) {
376
- const retry = await p.confirm({
377
- message: "Try a different name?",
378
- initialValue: true,
379
- });
408
+ let retry = false;
409
+ if (interactive) {
410
+ retry = await p.confirm({
411
+ message: "Try a different name?",
412
+ initialValue: true,
413
+ });
414
+ }
380
415
  if (p.isCancel(retry) || !retry) {
381
416
  p.log.info("Skipping repo creation. Register it later with:");
382
417
  p.log.info(" jfl services add <repo-url>");
@@ -507,13 +542,16 @@ export async function initCommand(options) {
507
542
  // Offer semantic search setup (before service onboarding for smoother flow)
508
543
  p.note("Search your workspace by meaning, not just keywords.\n" +
509
544
  "Requires qmd (local search engine) + ~1.5GB for models.", chalk.hex("#FFA500")("📚 Semantic Search"));
510
- const enableSearch = await p.confirm({
511
- message: "Enable semantic search?",
512
- initialValue: true,
513
- });
514
- if (p.isCancel(enableSearch)) {
515
- p.cancel("Setup cancelled.");
516
- process.exit(0);
545
+ let enableSearch = options?.search !== false;
546
+ if (interactive) {
547
+ enableSearch = await p.confirm({
548
+ message: "Enable semantic search?",
549
+ initialValue: true,
550
+ });
551
+ if (p.isCancel(enableSearch)) {
552
+ p.cancel("Setup cancelled.");
553
+ process.exit(0);
554
+ }
517
555
  }
518
556
  if (enableSearch) {
519
557
  // Check if qmd is installed
@@ -536,13 +574,16 @@ export async function initCommand(options) {
536
574
  // bun not installed
537
575
  }
538
576
  if (bunInstalled) {
539
- const installQmd = await p.confirm({
540
- message: "qmd not found. Install it now?",
541
- initialValue: true,
542
- });
543
- if (p.isCancel(installQmd)) {
544
- p.cancel("Setup cancelled.");
545
- process.exit(0);
577
+ let installQmd = false;
578
+ if (interactive) {
579
+ installQmd = await p.confirm({
580
+ message: "qmd not found. Install it now?",
581
+ initialValue: true,
582
+ });
583
+ if (p.isCancel(installQmd)) {
584
+ p.cancel("Setup cancelled.");
585
+ process.exit(0);
586
+ }
546
587
  }
547
588
  if (installQmd) {
548
589
  const qmdSpinner = ora("Installing qmd...").start();
@@ -606,19 +647,44 @@ export async function initCommand(options) {
606
647
  // Offer service onboarding
607
648
  p.note("Set up service agents for your repos (API, web app, etc.).\n" +
608
649
  "Each service gets its own agent that you can invoke via @-mentions.", chalk.hex("#FFA500")("Service Agents"));
609
- const onboardServices = await p.confirm({
610
- message: "Onboard services now?",
611
- initialValue: true,
612
- });
613
- if (p.isCancel(onboardServices)) {
614
- p.cancel("Setup cancelled.");
615
- process.exit(0);
650
+ let onboardServices = !!(options?.services === true || (options?.service && options.service.length > 0));
651
+ if (interactive && !options?.service?.length) {
652
+ onboardServices = await p.confirm({
653
+ message: "Onboard services now?",
654
+ initialValue: true,
655
+ });
656
+ if (p.isCancel(onboardServices)) {
657
+ p.cancel("Setup cancelled.");
658
+ process.exit(0);
659
+ }
616
660
  }
617
661
  const onboardedServices = [];
618
662
  let serviceCount = 0;
619
- if (onboardServices) {
663
+ // Auto-onboard services from --service flags (non-interactive)
664
+ if (options?.service && options.service.length > 0) {
665
+ for (const svcPath of options.service) {
666
+ const serviceSpinner = ora(`Onboarding service: ${svcPath}`).start();
667
+ try {
668
+ const metadata = extractServiceMetadata(svcPath);
669
+ const agentDef = generateAgentDefinition(metadata, svcPath, projectPath);
670
+ writeAgentDefinition(agentDef, projectPath);
671
+ writeSkillFiles(metadata, svcPath, projectPath);
672
+ onboardedServices.push(metadata.name);
673
+ serviceCount++;
674
+ serviceSpinner.succeed(`Service onboarded: ${metadata.name}`);
675
+ }
676
+ catch (err) {
677
+ serviceSpinner.fail(`Failed to onboard ${svcPath}: ${err.message}`);
678
+ }
679
+ }
680
+ }
681
+ if (onboardServices && !options?.service?.length) {
620
682
  let addingServices = true;
621
683
  while (addingServices) {
684
+ if (!interactive) {
685
+ addingServices = false;
686
+ break;
687
+ }
622
688
  const servicePath = await p.text({
623
689
  message: onboardedServices.length === 0
624
690
  ? "Service path or git URL:"
@@ -859,26 +925,47 @@ export async function initCommand(options) {
859
925
  });
860
926
  }
861
927
  p.note(successMessage, chalk.hex("#00FF88")("✅ GTM workspace initialized!"));
862
- // Ask about launching Claude Code
863
- const launchClaude = await p.confirm({
864
- message: "Start Claude Code now?",
865
- initialValue: true,
866
- });
867
- if (p.isCancel(launchClaude)) {
868
- p.cancel("Setup cancelled.");
869
- process.exit(0);
928
+ // Deploy CI workflows if --ci flag
929
+ if (options?.ci) {
930
+ const ciSpinner = ora("Deploying eval + review CI workflows...").start();
931
+ try {
932
+ const { ciSetupCommand } = await import("./ci-setup.js");
933
+ const origCwd = process.cwd();
934
+ process.chdir(projectPath);
935
+ await ciSetupCommand();
936
+ process.chdir(origCwd);
937
+ ciSpinner.succeed("CI workflows deployed (jfl-eval.yml + jfl-review.yml)");
938
+ }
939
+ catch (err) {
940
+ ciSpinner.fail(`CI setup failed: ${err.message}`);
941
+ p.log.info("Run manually later: jfl ci setup");
942
+ }
870
943
  }
871
- let dangerMode = false;
872
- if (launchClaude) {
873
- const dangerouslySkip = await p.confirm({
874
- message: "Skip permission prompts? (dangerously-skip-permissions)",
944
+ // Ask about launching Claude Code
945
+ let launchClaude = options?.launch === true;
946
+ if (interactive) {
947
+ launchClaude = await p.confirm({
948
+ message: "Start Claude Code now?",
875
949
  initialValue: true,
876
950
  });
877
- if (p.isCancel(dangerouslySkip)) {
951
+ if (p.isCancel(launchClaude)) {
878
952
  p.cancel("Setup cancelled.");
879
953
  process.exit(0);
880
954
  }
881
- dangerMode = dangerouslySkip;
955
+ }
956
+ let dangerMode = false;
957
+ if (launchClaude) {
958
+ if (interactive) {
959
+ const dangerouslySkip = await p.confirm({
960
+ message: "Skip permission prompts? (dangerously-skip-permissions)",
961
+ initialValue: true,
962
+ });
963
+ if (p.isCancel(dangerouslySkip)) {
964
+ p.cancel("Setup cancelled.");
965
+ process.exit(0);
966
+ }
967
+ dangerMode = dangerouslySkip;
968
+ }
882
969
  }
883
970
  if (launchClaude) {
884
971
  p.log.info(chalk.hex("#FFA500")("🚀 Launching Claude Code..."));