@stackable-labs/cli-app-extension 1.1.0 → 1.2.0

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/dist/index.js +287 -83
  2. package/package.json +2 -2
package/dist/index.js CHANGED
@@ -6,8 +6,8 @@ import { render } from "ink";
6
6
 
7
7
  // src/App.tsx
8
8
  import { join as join2 } from "path";
9
- import { Box as Box12, Text as Text12, useApp } from "ink";
10
- import { useCallback, useState as useState8 } from "react";
9
+ import { Box as Box13, Text as Text13, useApp } from "ink";
10
+ import { useCallback, useState as useState9 } from "react";
11
11
 
12
12
  // src/components/Confirm.tsx
13
13
  import { Box as Box2, Text as Text2, useInput } from "ink";
@@ -319,7 +319,7 @@ var stepIcon = (status) => {
319
319
  }
320
320
  };
321
321
  var ScaffoldProgress = ({ steps }) => /* @__PURE__ */ jsxs8(Box8, { flexDirection: "column", gap: 1, children: [
322
- /* @__PURE__ */ jsx8(Text8, { bold: true, children: "Scaffolding your extension\u2026" }),
322
+ /* @__PURE__ */ jsx8(Text8, { bold: true, children: "Scaffolding your Extension\u2026" }),
323
323
  /* @__PURE__ */ jsx8(Box8, { flexDirection: "column", children: steps.map((step) => /* @__PURE__ */ jsxs8(Box8, { gap: 2, children: [
324
324
  stepIcon(step.status),
325
325
  /* @__PURE__ */ jsx8(Text8, { dimColor: step.status === "pending", color: step.status === "running" ? "cyan" : void 0, children: step.label })
@@ -336,11 +336,11 @@ var TARGET_DESCRIPTIONS = {
336
336
  "slot.footer": "Renders a footer bar at the bottom of the panel",
337
337
  "slot.footer-links": "Renders a link row in the global footer"
338
338
  };
339
- var TargetSelect = ({ availableTargets, onSubmit, onBack }) => {
339
+ var TargetSelect = ({ availableTargets, preSelected, onSubmit, onBack }) => {
340
340
  const [cursor, setCursor] = useState6(0);
341
341
  const [backFocused, setBackFocused] = useState6(false);
342
342
  const [selected, setSelected] = useState6(
343
- new Set(availableTargets.includes("slot.content") ? ["slot.content"] : [])
343
+ new Set(preSelected ?? (availableTargets.includes("slot.content") ? ["slot.content"] : []))
344
344
  );
345
345
  const [error, setError] = useState6();
346
346
  useInput3((input, key) => {
@@ -414,18 +414,41 @@ var TargetSelect = ({ availableTargets, onSubmit, onBack }) => {
414
414
  };
415
415
 
416
416
  // src/components/AppSelect.tsx
417
- import { Box as Box11, Text as Text11 } from "ink";
418
- import SelectInput from "ink-select-input";
417
+ import { Box as Box11, Text as Text11, useInput as useInput4 } from "ink";
419
418
  import Spinner2 from "ink-spinner";
420
419
  import { useEffect, useState as useState7 } from "react";
421
420
 
422
421
  // src/lib/api.ts
423
- var API_BASE_URL = "https://api.stackablelabs.io/app-extension/latest";
422
+ var DEFAULT_API_URL = "https://api.stackablelabs.io/app-extension/latest";
423
+ var DEFAULT_ADMIN_API_URL = "https://api-use1.stackablelabs.io/admin";
424
+ var getApiBaseUrl = () => process.env.API_BASE_URL ?? DEFAULT_API_URL;
425
+ var getAdminApiBaseUrl = () => process.env.ADMIN_API_BASE_URL ?? DEFAULT_ADMIN_API_URL;
424
426
  var fetchApps = async () => {
425
- const baseURL = `${process.env.API_BASE_URL ?? API_BASE_URL}/apps`;
427
+ const baseURL = `${getApiBaseUrl()}/apps`;
426
428
  const res = await fetch(baseURL);
427
429
  if (!res.ok) {
428
- throw new Error(`Failed to fetch apps${baseURL !== API_BASE_URL ? ` (using ${baseURL})` : ""}: ${res.status} ${res.statusText}`);
430
+ throw new Error(`Failed to fetch apps: ${res.status} ${res.statusText}`);
431
+ }
432
+ return res.json();
433
+ };
434
+ var createExtensionRemote = async (appId, payload) => {
435
+ const baseUrl = getAdminApiBaseUrl();
436
+ const res = await fetch(`${baseUrl}/app-extension/${appId}/extensions`, {
437
+ method: "POST",
438
+ headers: { "content-type": "application/json" },
439
+ body: JSON.stringify(payload)
440
+ });
441
+ if (!res.ok) {
442
+ const body = await res.text();
443
+ throw new Error(`Failed to create extension: ${res.status} ${body}`);
444
+ }
445
+ return res.json();
446
+ };
447
+ var fetchExtensions = async (appId) => {
448
+ const baseUrl = getApiBaseUrl();
449
+ const res = await fetch(`${baseUrl}/extensions/${appId}`);
450
+ if (!res.ok) {
451
+ throw new Error(`Failed to fetch extensions: ${res.status} ${res.statusText}`);
429
452
  }
430
453
  return res.json();
431
454
  };
@@ -480,15 +503,26 @@ var AppSelect = ({ onSubmit }) => {
480
503
  const [apps, setApps] = useState7([]);
481
504
  const [loading, setLoading] = useState7(true);
482
505
  const [error, setError] = useState7();
506
+ const [cursor, setCursor] = useState7(0);
483
507
  useEffect(() => {
484
508
  fetchApps().then(setApps).catch((err) => setError(err instanceof Error ? err.message : String(err))).finally(() => setLoading(false));
485
509
  }, []);
510
+ useInput4((_, key) => {
511
+ if (loading || error || apps.length === 0) return;
512
+ if (key.upArrow) {
513
+ setCursor((c) => Math.max(0, c - 1));
514
+ } else if (key.downArrow) {
515
+ setCursor((c) => Math.min(apps.length - 1, c + 1));
516
+ } else if (key.return) {
517
+ onSubmit(apps[cursor]);
518
+ }
519
+ });
486
520
  if (loading) {
487
521
  return /* @__PURE__ */ jsxs11(Box11, { flexDirection: "column", children: [
488
522
  /* @__PURE__ */ jsx11(Banner, {}),
489
523
  /* @__PURE__ */ jsxs11(Box11, { gap: 2, paddingX: 1, children: [
490
524
  /* @__PURE__ */ jsx11(Spinner2, { type: "dots" }),
491
- /* @__PURE__ */ jsx11(Text11, { children: "Loading available apps\u2026" })
525
+ /* @__PURE__ */ jsx11(Text11, { children: "Loading available Apps\u2026" })
492
526
  ] })
493
527
  ] });
494
528
  }
@@ -501,20 +535,103 @@ var AppSelect = ({ onSubmit }) => {
501
535
  if (apps.length === 0) {
502
536
  return /* @__PURE__ */ jsx11(Text11, { color: "yellow", children: "No apps available. Contact your administrator." });
503
537
  }
504
- const items = apps.map((app) => ({
505
- label: app.name,
506
- value: app.id
507
- }));
508
- const handleSelect = (item) => {
509
- const app = apps.find((a) => a.id === item.value);
510
- if (app) onSubmit(app);
511
- };
512
538
  return /* @__PURE__ */ jsxs11(Box11, { flexDirection: "column", children: [
513
539
  /* @__PURE__ */ jsx11(Banner, {}),
514
- /* @__PURE__ */ jsx11(StepShell, { title: "Select the App you are building an Extension for:", children: /* @__PURE__ */ jsx11(SelectInput, { items, onSelect: handleSelect }) })
540
+ /* @__PURE__ */ jsx11(StepShell, { title: "Select the App you are building an Extension for:", children: /* @__PURE__ */ jsx11(Box11, { flexDirection: "column", children: apps.map((app, i) => {
541
+ const isCursor = i === cursor;
542
+ return /* @__PURE__ */ jsxs11(Box11, { gap: 1, children: [
543
+ /* @__PURE__ */ jsx11(Text11, { color: isCursor ? "cyan" : void 0, children: isCursor ? "\u276F" : " " }),
544
+ /* @__PURE__ */ jsx11(Text11, { bold: isCursor, children: app.name }),
545
+ /* @__PURE__ */ jsxs11(Text11, { dimColor: true, children: [
546
+ "(",
547
+ app.id,
548
+ ")"
549
+ ] })
550
+ ] }, app.id);
551
+ }) }) })
515
552
  ] });
516
553
  };
517
554
 
555
+ // src/components/ExtensionSelect.tsx
556
+ import { Box as Box12, Text as Text12, useInput as useInput5 } from "ink";
557
+ import Spinner3 from "ink-spinner";
558
+ import { useEffect as useEffect2, useState as useState8 } from "react";
559
+ import { jsx as jsx12, jsxs as jsxs12 } from "react/jsx-runtime";
560
+ var ExtensionSelect = ({ appId, onSubmit, onBack }) => {
561
+ const [extensions, setExtensions] = useState8([]);
562
+ const [loading, setLoading] = useState8(true);
563
+ const [error, setError] = useState8();
564
+ const [cursor, setCursor] = useState8(0);
565
+ const [backFocused, setBackFocused] = useState8(false);
566
+ useEffect2(() => {
567
+ fetchExtensions(appId).then((byId) => setExtensions(Object.values(byId))).catch((err) => setError(err instanceof Error ? err.message : String(err))).finally(() => setLoading(false));
568
+ }, [appId]);
569
+ useInput5((_, key) => {
570
+ if (loading || error || extensions.length === 0) return;
571
+ if (key.upArrow) {
572
+ if (cursor === 0 && onBack) {
573
+ setBackFocused(true);
574
+ } else {
575
+ setBackFocused(false);
576
+ setCursor((c) => Math.max(0, c - 1));
577
+ }
578
+ return;
579
+ }
580
+ if (key.downArrow) {
581
+ if (backFocused) {
582
+ setBackFocused(false);
583
+ } else {
584
+ setCursor((c) => Math.min(extensions.length - 1, c + 1));
585
+ }
586
+ return;
587
+ }
588
+ if (key.return) {
589
+ if (backFocused) {
590
+ onBack?.();
591
+ return;
592
+ }
593
+ onSubmit(extensions[cursor]);
594
+ }
595
+ });
596
+ if (loading) {
597
+ return /* @__PURE__ */ jsxs12(Box12, { gap: 2, paddingX: 1, children: [
598
+ /* @__PURE__ */ jsx12(Spinner3, { type: "dots" }),
599
+ /* @__PURE__ */ jsx12(Text12, { children: "Loading Extensions\u2026" })
600
+ ] });
601
+ }
602
+ if (error) {
603
+ return /* @__PURE__ */ jsxs12(Box12, { flexDirection: "column", gap: 1, children: [
604
+ /* @__PURE__ */ jsx12(Text12, { color: "red", bold: true, children: "Failed to load Extensions" }),
605
+ /* @__PURE__ */ jsx12(Text12, { color: "red", children: error })
606
+ ] });
607
+ }
608
+ if (extensions.length === 0) {
609
+ return /* @__PURE__ */ jsx12(Text12, { color: "yellow", children: "No Extensions found for this App." });
610
+ }
611
+ return /* @__PURE__ */ jsx12(StepShell, { title: "Select an existing Extension to scaffold:", onBack, backFocused, children: /* @__PURE__ */ jsx12(Box12, { flexDirection: "column", children: extensions.map((ext, i) => {
612
+ const isCursor = i === cursor && !backFocused;
613
+ return /* @__PURE__ */ jsxs12(Box12, { gap: 1, children: [
614
+ /* @__PURE__ */ jsx12(Text12, { color: isCursor ? "cyan" : void 0, children: isCursor ? "\u276F" : " " }),
615
+ /* @__PURE__ */ jsx12(Text12, { bold: isCursor, children: ext.manifest.name }),
616
+ /* @__PURE__ */ jsxs12(Text12, { dimColor: true, children: [
617
+ "(",
618
+ ext.id,
619
+ ")"
620
+ ] })
621
+ ] }, ext.id);
622
+ }) }) });
623
+ };
624
+
625
+ // src/constants.ts
626
+ var TEMPLATE_SOURCE = "github:stackable-labs/templates/app-extension";
627
+ var DEFAULT_PERMISSIONS = ["context:read"];
628
+ var TARGET_PERMISSION_MAP = {
629
+ "slot.header": ["context:read"],
630
+ "slot.content": ["context:read", "data:query", "actions:toast", "actions:invoke"],
631
+ "slot.footer": [],
632
+ "slot.footer-links": []
633
+ };
634
+
518
635
  // src/lib/postScaffold.ts
519
636
  import { execFile } from "child_process";
520
637
  import { promisify } from "util";
@@ -541,18 +658,6 @@ var postScaffold = async (options) => {
541
658
  import { readFile, readdir, rm, writeFile } from "fs/promises";
542
659
  import { join } from "path";
543
660
  import { downloadTemplate } from "giget";
544
-
545
- // src/constants.ts
546
- var TEMPLATE_SOURCE = "github:stackable-labs/templates/app-extension";
547
- var DEFAULT_PERMISSIONS = ["context:read"];
548
- var TARGET_PERMISSION_MAP = {
549
- "slot.header": ["context:read"],
550
- "slot.content": ["context:read", "data:query", "actions:toast", "actions:invoke"],
551
- "slot.footer": [],
552
- "slot.footer-links": []
553
- };
554
-
555
- // src/lib/scaffold.ts
556
661
  var normalizeTargets = (targets) => Array.from(new Set(targets));
557
662
  var derivePermissions = (targets) => {
558
663
  const permissions = /* @__PURE__ */ new Set();
@@ -737,7 +842,7 @@ var rewritePreviewApp = async (rootDir, targets, permissions) => {
737
842
  }
738
843
  const appContent = `import { ExtensionProvider, ExtensionSlot } from '@stackable-labs/sdk-extension-host'
739
844
  import type { CapabilityHandlers } from '@stackable-labs/sdk-extension-host'
740
- import { hostComponents } from '@stackable-labs/embeddables/components'
845
+ import hostComponents from '@stackable-labs/embeddables/components'
741
846
  import type {
742
847
  ${typeImports.join(",\n ")}
743
848
  } from '@stackable-labs/sdk-extension-contracts'
@@ -851,47 +956,93 @@ var scaffold = async (options) => {
851
956
  };
852
957
 
853
958
  // src/App.tsx
854
- import { jsx as jsx12, jsxs as jsxs12 } from "react/jsx-runtime";
855
- var STEP_ORDER = ["app", "name", "targets", "ports", "dir", "confirm"];
856
- var INITIAL_STEPS = [
959
+ import { jsx as jsx13, jsxs as jsxs13 } from "react/jsx-runtime";
960
+ var STEP_ORDER_CREATE = ["app", "name", "targets", "ports", "dir", "confirm"];
961
+ var STEP_ORDER_SCAFFOLD = ["app", "extensionSelect", "confirmName", "confirmTargets", "ports", "dir", "confirm"];
962
+ var CREATE_STEPS = [
963
+ { label: "Registering extension", status: "pending" },
964
+ { label: "Fetching template", status: "pending" },
965
+ { label: "Generating files", status: "pending" },
966
+ { label: "Installing dependencies", status: "pending" }
967
+ ];
968
+ var SCAFFOLD_STEPS = [
857
969
  { label: "Fetching template", status: "pending" },
858
970
  { label: "Generating files", status: "pending" },
859
971
  { label: "Installing dependencies", status: "pending" }
860
972
  ];
861
973
  var toKebabCase2 = (value) => value.trim().toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
862
- var App = ({ initialName, options }) => {
974
+ var derivePermissions2 = (targets) => {
975
+ const permissions = /* @__PURE__ */ new Set();
976
+ for (const target of targets) {
977
+ const mapped = TARGET_PERMISSION_MAP[target];
978
+ if (mapped && mapped.length > 0) {
979
+ for (const permission of mapped) {
980
+ permissions.add(permission);
981
+ }
982
+ }
983
+ }
984
+ if (permissions.size === 0) {
985
+ for (const permission of DEFAULT_PERMISSIONS) {
986
+ permissions.add(permission);
987
+ }
988
+ }
989
+ return [...permissions];
990
+ };
991
+ var App = ({ mode, initialName, options }) => {
863
992
  const { exit } = useApp();
864
- const [step, setStep] = useState8("app");
865
- const [name, setName] = useState8(initialName ?? "");
866
- const [extensionId, setExtensionId] = useState8("");
867
- const [selectedApp, setSelectedApp] = useState8(null);
868
- const [extensionPort, setExtensionPort] = useState8(
993
+ const [step, setStep] = useState9("app");
994
+ const [name, setName] = useState9(initialName ?? "");
995
+ const [extensionId, setExtensionId] = useState9("");
996
+ const [selectedApp, setSelectedApp] = useState9(null);
997
+ const [extensionPort, setExtensionPort] = useState9(
869
998
  options?.extensionPort ? parseInt(options.extensionPort, 10) : 5173
870
999
  );
871
- const [previewPort, setPreviewPort] = useState8(
1000
+ const [previewPort, setPreviewPort] = useState9(
872
1001
  options?.previewPort ? parseInt(options.previewPort, 10) : 5174
873
1002
  );
874
- const [targets, setTargets] = useState8([]);
875
- const [outputDir, setOutputDir] = useState8("");
876
- const [progressSteps, setProgressSteps] = useState8(INITIAL_STEPS);
877
- const [errorMessage, setErrorMessage] = useState8();
1003
+ const [targets, setTargets] = useState9([]);
1004
+ const [outputDir, setOutputDir] = useState9("");
1005
+ const [progressSteps, setProgressSteps] = useState9(mode === "create" ? CREATE_STEPS : SCAFFOLD_STEPS);
1006
+ const [errorMessage, setErrorMessage] = useState9();
878
1007
  const updateStep = useCallback((index, status) => {
879
1008
  setProgressSteps((prev) => prev.map((s, i) => i === index ? { ...s, status } : s));
880
1009
  }, []);
1010
+ const activeSteps = useCallback(() => {
1011
+ const base = mode === "create" ? STEP_ORDER_CREATE : STEP_ORDER_SCAFFOLD;
1012
+ const skipped = /* @__PURE__ */ new Set();
1013
+ if (mode === "create" && initialName) skipped.add("name");
1014
+ if (options?.extensionPort || options?.previewPort) skipped.add("ports");
1015
+ return base.filter((s) => !skipped.has(s));
1016
+ }, [mode, initialName, options?.extensionPort, options?.previewPort]);
881
1017
  const goBack = useCallback(() => {
882
- const skippedSteps = /* @__PURE__ */ new Set();
883
- if (initialName) skippedSteps.add("name");
884
- if (options?.extensionPort || options?.previewPort) skippedSteps.add("ports");
885
- const activeSteps = STEP_ORDER.filter((s) => !skippedSteps.has(s));
886
1018
  setStep((prev) => {
887
- const idx = activeSteps.indexOf(prev);
888
- return idx > 0 ? activeSteps[idx - 1] : prev;
1019
+ const steps = activeSteps();
1020
+ const idx = steps.indexOf(prev);
1021
+ return idx > 0 ? steps[idx - 1] : prev;
889
1022
  });
890
- }, [initialName, options?.extensionPort, options?.previewPort]);
1023
+ }, [activeSteps]);
891
1024
  const handleAppSelect = (app) => {
892
1025
  setSelectedApp(app);
1026
+ if (mode === "scaffold") {
1027
+ setStep("extensionSelect");
1028
+ return;
1029
+ }
893
1030
  setStep(initialName ? "targets" : "name");
894
1031
  };
1032
+ const handleExtensionSelect = (ext) => {
1033
+ setName(ext.manifest.name);
1034
+ setExtensionId(ext.id);
1035
+ setTargets(ext.manifest.targets);
1036
+ setStep("confirmName");
1037
+ };
1038
+ const handleConfirmName = (value) => {
1039
+ setName(value);
1040
+ setStep("confirmTargets");
1041
+ };
1042
+ const handleConfirmTargets = (value) => {
1043
+ setTargets(value);
1044
+ setStep(options?.extensionPort || options?.previewPort ? "dir" : "ports");
1045
+ };
895
1046
  const handleName = (value) => {
896
1047
  setName(value);
897
1048
  setExtensionId(toKebabCase2(value));
@@ -912,32 +1063,46 @@ var App = ({ initialName, options }) => {
912
1063
  };
913
1064
  const handleConfirm = async () => {
914
1065
  setStep("scaffolding");
915
- setProgressSteps([
916
- { label: "Fetching template", status: "running" },
917
- { label: "Generating files", status: "pending" },
918
- { label: "Installing dependencies", status: "pending" }
919
- ]);
1066
+ setProgressSteps(mode === "create" ? CREATE_STEPS : SCAFFOLD_STEPS);
920
1067
  try {
921
- updateStep(0, "running");
1068
+ let resolvedExtensionId = extensionId || toKebabCase2(name);
1069
+ let scaffoldStepOffset = 0;
1070
+ if (mode === "create") {
1071
+ scaffoldStepOffset = 1;
1072
+ updateStep(0, "running");
1073
+ const created = await createExtensionRemote(selectedApp.id, {
1074
+ manifest: {
1075
+ name,
1076
+ version: "0.0.0",
1077
+ targets,
1078
+ permissions: derivePermissions2(targets)
1079
+ },
1080
+ bundleUrl: `http://localhost:${extensionPort}`
1081
+ });
1082
+ resolvedExtensionId = created.id;
1083
+ setExtensionId(created.id);
1084
+ updateStep(0, "done");
1085
+ }
1086
+ updateStep(scaffoldStepOffset + 0, "running");
922
1087
  await scaffold({
923
1088
  appId: selectedApp.id,
924
1089
  name,
925
- extensionId,
1090
+ extensionId: resolvedExtensionId,
926
1091
  targets,
927
1092
  outputDir,
928
1093
  extensionPort,
929
1094
  previewPort
930
1095
  });
931
- updateStep(0, "done");
932
- updateStep(1, "running");
1096
+ updateStep(scaffoldStepOffset + 0, "done");
1097
+ updateStep(scaffoldStepOffset + 1, "running");
933
1098
  await new Promise((r) => setTimeout(r, 200));
934
- updateStep(1, "done");
1099
+ updateStep(scaffoldStepOffset + 1, "done");
935
1100
  if (!options?.skipInstall) {
936
- updateStep(2, "running");
1101
+ updateStep(scaffoldStepOffset + 2, "running");
937
1102
  await postScaffold({ outputDir, skipInstall: false, skipGit: options?.skipGit });
938
- updateStep(2, "done");
1103
+ updateStep(scaffoldStepOffset + 2, "done");
939
1104
  } else {
940
- updateStep(2, "done");
1105
+ updateStep(scaffoldStepOffset + 2, "done");
941
1106
  }
942
1107
  setStep("done");
943
1108
  } catch (err) {
@@ -951,10 +1116,41 @@ var App = ({ initialName, options }) => {
951
1116
  };
952
1117
  switch (step) {
953
1118
  case "app": {
954
- return /* @__PURE__ */ jsx12(AppSelect, { onSubmit: handleAppSelect });
1119
+ return /* @__PURE__ */ jsx13(AppSelect, { onSubmit: handleAppSelect });
1120
+ }
1121
+ case "extensionSelect": {
1122
+ return /* @__PURE__ */ jsx13(
1123
+ ExtensionSelect,
1124
+ {
1125
+ appId: selectedApp.id,
1126
+ onSubmit: handleExtensionSelect,
1127
+ onBack: goBack
1128
+ }
1129
+ );
1130
+ }
1131
+ case "confirmName": {
1132
+ return /* @__PURE__ */ jsx13(
1133
+ NamePrompt,
1134
+ {
1135
+ initialValue: name,
1136
+ onSubmit: handleConfirmName,
1137
+ onBack: goBack
1138
+ }
1139
+ );
1140
+ }
1141
+ case "confirmTargets": {
1142
+ return /* @__PURE__ */ jsx13(
1143
+ TargetSelect,
1144
+ {
1145
+ availableTargets: selectedApp?.targets ?? [],
1146
+ preSelected: targets,
1147
+ onSubmit: handleConfirmTargets,
1148
+ onBack: goBack
1149
+ }
1150
+ );
955
1151
  }
956
1152
  case "name": {
957
- return /* @__PURE__ */ jsx12(
1153
+ return /* @__PURE__ */ jsx13(
958
1154
  NamePrompt,
959
1155
  {
960
1156
  initialValue: name,
@@ -964,7 +1160,7 @@ var App = ({ initialName, options }) => {
964
1160
  );
965
1161
  }
966
1162
  case "targets": {
967
- return /* @__PURE__ */ jsx12(
1163
+ return /* @__PURE__ */ jsx13(
968
1164
  TargetSelect,
969
1165
  {
970
1166
  availableTargets: selectedApp?.targets ?? [],
@@ -974,7 +1170,7 @@ var App = ({ initialName, options }) => {
974
1170
  );
975
1171
  }
976
1172
  case "ports": {
977
- return /* @__PURE__ */ jsx12(
1173
+ return /* @__PURE__ */ jsx13(
978
1174
  PortsPrompt,
979
1175
  {
980
1176
  onSubmit: handlePorts,
@@ -983,17 +1179,17 @@ var App = ({ initialName, options }) => {
983
1179
  );
984
1180
  }
985
1181
  case "dir": {
986
- return /* @__PURE__ */ jsx12(
1182
+ return /* @__PURE__ */ jsx13(
987
1183
  DirPrompt,
988
1184
  {
989
- defaultDir: join2(process.cwd(), toKebabCase2(name)),
1185
+ defaultDir: join2(process.cwd(), toKebabCase2(extensionId || name)),
990
1186
  onSubmit: handleDir,
991
1187
  onBack: goBack
992
1188
  }
993
1189
  );
994
1190
  }
995
1191
  case "confirm": {
996
- return /* @__PURE__ */ jsx12(
1192
+ return /* @__PURE__ */ jsx13(
997
1193
  Confirm,
998
1194
  {
999
1195
  name,
@@ -1008,23 +1204,31 @@ var App = ({ initialName, options }) => {
1008
1204
  );
1009
1205
  }
1010
1206
  case "scaffolding": {
1011
- return /* @__PURE__ */ jsx12(ScaffoldProgress, { steps: progressSteps });
1207
+ return /* @__PURE__ */ jsx13(ScaffoldProgress, { steps: progressSteps });
1012
1208
  }
1013
1209
  case "done": {
1014
- return /* @__PURE__ */ jsx12(Done, { name, outputDir });
1210
+ return /* @__PURE__ */ jsx13(Done, { name, outputDir });
1015
1211
  }
1016
1212
  default: {
1017
- return /* @__PURE__ */ jsxs12(Box12, { flexDirection: "column", gap: 1, children: [
1018
- /* @__PURE__ */ jsx12(Text12, { color: "red", bold: true, children: "Scaffold failed" }),
1019
- errorMessage && /* @__PURE__ */ jsx12(Text12, { color: "red", children: errorMessage })
1213
+ return /* @__PURE__ */ jsxs13(Box13, { flexDirection: "column", gap: 1, children: [
1214
+ /* @__PURE__ */ jsx13(Text13, { color: "red", bold: true, children: "Scaffold failed" }),
1215
+ errorMessage && /* @__PURE__ */ jsx13(Text13, { color: "red", children: errorMessage })
1020
1216
  ] });
1021
1217
  }
1022
1218
  }
1023
1219
  };
1024
1220
 
1025
1221
  // src/index.tsx
1026
- import { jsx as jsx13 } from "react/jsx-runtime";
1027
- program.name("create-extension").description("Scaffold a new Stackable extension project").argument("[name]", "Extension project name").option("--extension-port <port>", "Extension dev server port (default: 5173)").option("--preview-port <port>", "Preview host dev server port (default: extension port + 1)").option("--skip-install", "Skip package manager install").option("--skip-git", "Skip git initialization").action((name, options) => {
1028
- render(/* @__PURE__ */ jsx13(App, { initialName: name, options }));
1222
+ import { jsx as jsx14 } from "react/jsx-runtime";
1223
+ program.name("stackable-extension").description("Stackable extension developer CLI");
1224
+ program.command("create").description("Create a new extension project").argument("[name]", "Extension project name").option("--targets <targets>", "Comma-separated target slots").option("--extension-port <port>", "Extension dev server port (default: 5173)").option("--preview-port <port>", "Preview host dev server port").option("--skip-install", "Skip package manager install").option("--skip-git", "Skip git initialization").action((name, options) => {
1225
+ render(/* @__PURE__ */ jsx14(App, { mode: "create", initialName: name, options }));
1226
+ });
1227
+ program.command("scaffold").description("Scaffold a local project from an existing extension").option("--extension-port <port>", "Extension dev server port (default: 5173)").option("--preview-port <port>", "Preview host dev server port").option("--skip-install", "Skip package manager install").option("--skip-git", "Skip git initialization").action((options) => {
1228
+ render(/* @__PURE__ */ jsx14(App, { mode: "scaffold", options }));
1029
1229
  });
1030
- program.parse(process.argv.filter((arg) => arg !== "--"));
1230
+ if (process.argv[1]?.endsWith("create-extension")) {
1231
+ program.parse(["node", "stackable-extension", "create", ...process.argv.slice(2).filter((arg) => arg !== "--")]);
1232
+ } else {
1233
+ program.parse(process.argv.filter((arg) => arg !== "--"));
1234
+ }
package/package.json CHANGED
@@ -1,11 +1,11 @@
1
1
  {
2
2
  "name": "@stackable-labs/cli-app-extension",
3
- "version": "1.1.0",
3
+ "version": "1.2.0",
4
4
  "type": "module",
5
5
  "private": false,
6
6
  "bin": {
7
7
  "create-extension": "./dist/index.js",
8
- "cli-app-extension": "./dist/index.js"
8
+ "stackable-extension": "./dist/index.js"
9
9
  },
10
10
  "files": [
11
11
  "dist/",