@greatstore/cli 0.0.10 → 0.0.11-beta.1

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 (3) hide show
  1. package/README.md +7 -0
  2. package/dist/cli.js +568 -158
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -11,6 +11,13 @@ gs push
11
11
  gs publish my-widget
12
12
  ```
13
13
 
14
+ ## Release channels
15
+
16
+ - `npm install -g @greatstore/cli` — the stable production release (`@latest`).
17
+ - `npm install -g @greatstore/cli@beta` — the prerelease channel for testing in-flight changes before they cut over to `@latest`.
18
+
19
+ Cut a beta by bumping `cli/package.json` to `<x.y.z>-beta.<n>` (e.g. `0.0.12-beta.0`), then push tag `cli-v0.0.12-beta.0`. The publish workflow extracts the prerelease identifier from the version and publishes under that dist-tag, so `@latest` is unaffected. Drop the `-beta.<n>` suffix and tag again to promote to `@latest`.
20
+
14
21
  ## Commands
15
22
 
16
23
  | Command | What it does |
package/dist/cli.js CHANGED
@@ -92,7 +92,7 @@ async function captureLoopbackToken(options) {
92
92
  const open = options.openBrowser ?? ((url) => openInBrowser(url, options.browserCmdEnv));
93
93
  const server = http.createServer();
94
94
  try {
95
- await new Promise((resolve5) => server.listen(0, "127.0.0.1", resolve5));
95
+ await new Promise((resolve4) => server.listen(0, "127.0.0.1", resolve4));
96
96
  const address = server.address();
97
97
  const redirectUri = `http://127.0.0.1:${address.port}${CALLBACK_PATH}`;
98
98
  const authUrl = `${options.navBaseUrl}/connect_oauth_done?redirect_uri=${encodeURIComponent(redirectUri)}&state=${encodeURIComponent(state)}`;
@@ -104,7 +104,7 @@ async function captureLoopbackToken(options) {
104
104
  }
105
105
  }
106
106
  function waitForCallback(server, expectedState, timeoutMs) {
107
- return new Promise((resolve5, reject) => {
107
+ return new Promise((resolve4, reject) => {
108
108
  let settled = false;
109
109
  const settle = (fn) => {
110
110
  if (settled) return;
@@ -150,7 +150,7 @@ function waitForCallback(server, expectedState, timeoutMs) {
150
150
  res.writeHead(200, { "content-type": "text/html" });
151
151
  res.end(SUCCESS_HTML);
152
152
  clearTimeout(timer);
153
- settle(() => resolve5({ token }));
153
+ settle(() => resolve4({ token }));
154
154
  });
155
155
  });
156
156
  }
@@ -273,16 +273,11 @@ var StoreResolutionError = class extends Error {
273
273
  this.name = "StoreResolutionError";
274
274
  }
275
275
  };
276
- function resolveStore(input = {}) {
277
- const flag = input.flag?.trim();
278
- if (flag) return flag;
276
+ function requireProjectStore(input = {}) {
279
277
  const fromRc = findGsrc(input.cwd ?? process.cwd());
280
278
  if (fromRc) return fromRc;
281
- const env = input.env ?? process.env;
282
- const fromEnv = env.GS_STORE?.trim();
283
- if (fromEnv) return fromEnv;
284
279
  throw new StoreResolutionError(
285
- 'No store selected. Pass `--store <slug>`, set GS_STORE, or create a .gsrc file with {"store":"..."}.'
280
+ "No .gsrc found in this directory or any ancestor. Run `gs init --store <slug>` first to scaffold a GreatStore project."
286
281
  );
287
282
  }
288
283
  function findGsrc(cwd) {
@@ -479,7 +474,7 @@ function whoamiCommand() {
479
474
 
480
475
  // src/commands/list.ts
481
476
  async function listCommand(args) {
482
- const slug = resolveStore({ flag: flagString(args.flags, "store") });
477
+ const slug = requireProjectStore();
483
478
  const url = `${apiBaseFor(slug)}/api/builder/components`;
484
479
  const data = await request(url);
485
480
  if (flagBool(args.flags, "json")) {
@@ -516,34 +511,181 @@ function pad(s, width) {
516
511
  }
517
512
 
518
513
  // src/commands/pull.ts
514
+ import * as fs4 from "fs";
515
+ import * as path4 from "path";
516
+
517
+ // src/sync.ts
518
+ import * as crypto2 from "crypto";
519
519
  import * as fs3 from "fs";
520
520
  import * as path3 from "path";
521
- async function pullCommand(args) {
522
- const name = args.positional[0];
523
- if (!name) {
524
- throw new Error("Usage: gs pull <name> [--draft|--live|--version N] [-o <dir>]");
521
+ var SYNC_FILE = ".gssync.json";
522
+ function hashString(text) {
523
+ return crypto2.createHash("sha256").update(text).digest("hex");
524
+ }
525
+ function hashFile(filePath) {
526
+ if (!fs3.existsSync(filePath)) return null;
527
+ return hashString(fs3.readFileSync(filePath, "utf8"));
528
+ }
529
+ function computeComponentHashes(componentDir) {
530
+ const manifestHash = hashFile(path3.join(componentDir, "manifest.json"));
531
+ if (manifestHash === null) {
532
+ throw new Error(`Missing manifest.json in ${componentDir}`);
525
533
  }
526
- const slug = resolveStore({ flag: flagString(args.flags, "store") });
527
- const out = flagString(args.flags, "out") ?? ".";
528
- const query = buildRevisionQuery(args);
529
- const url = `${apiBaseFor(slug)}/api/builder/components/${encodeURIComponent(name)}${query}`;
530
- const data = await request(url);
531
- fs3.mkdirSync(out, { recursive: true });
534
+ return {
535
+ manifestHash,
536
+ sourceHash: hashFile(path3.join(componentDir, "component.tsx"))
537
+ };
538
+ }
539
+ function readSyncState(componentDir) {
540
+ const file = path3.join(componentDir, SYNC_FILE);
541
+ if (!fs3.existsSync(file)) return null;
542
+ try {
543
+ const parsed = JSON.parse(fs3.readFileSync(file, "utf8"));
544
+ if (typeof parsed.version === "number" && typeof parsed.manifestHash === "string" && typeof parsed.sourceHash === "string") {
545
+ return {
546
+ version: parsed.version,
547
+ manifestHash: parsed.manifestHash,
548
+ sourceHash: parsed.sourceHash
549
+ };
550
+ }
551
+ return null;
552
+ } catch {
553
+ return null;
554
+ }
555
+ }
556
+ function writeSyncState(componentDir, state) {
532
557
  fs3.writeFileSync(
533
- path3.join(out, "manifest.json"),
534
- JSON.stringify(data.manifest, null, 2) + "\n"
558
+ path3.join(componentDir, SYNC_FILE),
559
+ JSON.stringify(state, null, 2) + "\n"
535
560
  );
536
- fs3.writeFileSync(path3.join(out, "bundle.js"), data.bundle);
537
- const wrote = ["manifest.json", "bundle.js"];
538
- if (data.source !== null) {
539
- fs3.writeFileSync(path3.join(out, "component.tsx"), data.source);
540
- wrote.push("component.tsx");
561
+ }
562
+ function hasLocalChanges(componentDir, state) {
563
+ const hashes = safeComputeHashes(componentDir);
564
+ if (!hashes) return false;
565
+ if (state === null) return true;
566
+ if (hashes.manifestHash !== state.manifestHash) return true;
567
+ const localSource = hashes.sourceHash ?? "";
568
+ const storedSource = state.sourceHash ?? "";
569
+ return localSource !== storedSource;
570
+ }
571
+ function safeComputeHashes(componentDir) {
572
+ try {
573
+ return computeComponentHashes(componentDir);
574
+ } catch {
575
+ return null;
576
+ }
577
+ }
578
+
579
+ // src/commands/pull.ts
580
+ async function pullCommand(args) {
581
+ const slug = requireProjectStore();
582
+ const force = flagBool(args.flags, "force");
583
+ const target = args.positional[0];
584
+ const root = process.cwd();
585
+ if (!target || target === "*") {
586
+ return pullAll({ slug, root, args, force });
541
587
  }
542
- const sourceNote = data.source === null ? " (no source \u2014 this version was pushed without component.tsx)" : "";
588
+ ensureComponentsDir(root);
589
+ const outcome = await pullOne({
590
+ slug,
591
+ componentDir: path4.join(root, "components", target),
592
+ name: target,
593
+ revisionQuery: buildRevisionQuery(args),
594
+ force
595
+ });
596
+ printOutcome(outcome, force);
597
+ if (outcome.status === "failed") process.exitCode = 1;
598
+ }
599
+ async function pullAll(opts) {
600
+ const { slug, root, args, force } = opts;
601
+ if (flagString(args.flags, "version")) {
602
+ throw new Error(
603
+ "--version is per-component; pass it together with `gs pull <name>`."
604
+ );
605
+ }
606
+ ensureComponentsDir(root);
607
+ const revisionQuery = buildRevisionQuery(args);
608
+ const listUrl = `${apiBaseFor(slug)}/api/builder/components`;
609
+ const list = await request(listUrl);
610
+ if (list.components.length === 0) {
611
+ process.stdout.write(`(no components in ${slug})
612
+ `);
613
+ return;
614
+ }
615
+ const outcomes = [];
616
+ for (const entry of list.components) {
617
+ const componentDir = path4.join(root, "components", entry.name);
618
+ const outcome = await pullOne({
619
+ slug,
620
+ componentDir,
621
+ name: entry.name,
622
+ revisionQuery,
623
+ force
624
+ });
625
+ outcomes.push(outcome);
626
+ printOutcome(outcome, force);
627
+ }
628
+ const pulled = outcomes.filter((o) => o.status === "pulled").length;
629
+ const skipped = outcomes.filter((o) => o.status === "skipped").length;
630
+ const failed = outcomes.filter((o) => o.status === "failed").length;
631
+ const summary = [`${pulled} pulled`, `${skipped} skipped`];
632
+ if (failed > 0) summary.push(`${failed} failed`);
543
633
  process.stdout.write(
544
- `Pulled ${name} v${data.version} \u2192 ${path3.resolve(out)}/{${wrote.join(", ")}}${sourceNote}
634
+ `
635
+ ${summary.join(", ")} (of ${outcomes.length}) \u2190 ${slug}
545
636
  `
546
637
  );
638
+ if (failed > 0) process.exitCode = 1;
639
+ }
640
+ async function pullOne(opts) {
641
+ const { slug, componentDir, name, revisionQuery, force } = opts;
642
+ if (!force && fs4.existsSync(componentDir)) {
643
+ const sync = readSyncState(componentDir);
644
+ if (hasLocalChanges(componentDir, sync)) {
645
+ return {
646
+ name,
647
+ status: "skipped",
648
+ message: "local has uncommitted changes; pass --force to overwrite"
649
+ };
650
+ }
651
+ }
652
+ const url = `${apiBaseFor(slug)}/api/builder/components/${encodeURIComponent(name)}${revisionQuery}`;
653
+ let data;
654
+ try {
655
+ data = await request(url);
656
+ } catch (err) {
657
+ return {
658
+ name,
659
+ status: "failed",
660
+ message: err instanceof Error ? err.message : String(err)
661
+ };
662
+ }
663
+ fs4.mkdirSync(componentDir, { recursive: true });
664
+ const manifestText = JSON.stringify(data.manifest, null, 2) + "\n";
665
+ fs4.writeFileSync(path4.join(componentDir, "manifest.json"), manifestText);
666
+ fs4.writeFileSync(path4.join(componentDir, "bundle.js"), data.bundle);
667
+ const wrote = ["manifest.json", "bundle.js"];
668
+ let sourceHash = "";
669
+ if (data.source !== null) {
670
+ fs4.writeFileSync(path4.join(componentDir, "component.tsx"), data.source);
671
+ wrote.push("component.tsx");
672
+ sourceHash = hashString(data.source);
673
+ }
674
+ writeSyncState(componentDir, {
675
+ version: data.version,
676
+ manifestHash: hashString(manifestText),
677
+ sourceHash
678
+ });
679
+ const onDisk = computeComponentHashes(componentDir);
680
+ writeSyncState(componentDir, {
681
+ version: data.version,
682
+ manifestHash: onDisk.manifestHash,
683
+ sourceHash: onDisk.sourceHash ?? ""
684
+ });
685
+ return { name, status: "pulled", version: data.version, wrote };
686
+ }
687
+ function ensureComponentsDir(root) {
688
+ fs4.mkdirSync(path4.join(root, "components"), { recursive: true });
547
689
  }
548
690
  function buildRevisionQuery(args) {
549
691
  const version = flagString(args.flags, "version");
@@ -552,65 +694,227 @@ function buildRevisionQuery(args) {
552
694
  if (flagBool(args.flags, "live")) return `?revision=live`;
553
695
  return "";
554
696
  }
697
+ function printOutcome(outcome, force) {
698
+ switch (outcome.status) {
699
+ case "pulled": {
700
+ const files = outcome.wrote ? `{${outcome.wrote.join(", ")}}` : "";
701
+ process.stdout.write(
702
+ ` pulled ${outcome.name} v${outcome.version} ${files}
703
+ `
704
+ );
705
+ return;
706
+ }
707
+ case "skipped":
708
+ process.stdout.write(` skipped ${outcome.name}: ${outcome.message}
709
+ `);
710
+ return;
711
+ case "failed":
712
+ process.stdout.write(
713
+ ` failed ${outcome.name}: ${outcome.message ?? "unknown error"}
714
+ `
715
+ );
716
+ return;
717
+ }
718
+ void force;
719
+ }
555
720
 
556
721
  // src/commands/push.ts
557
- import * as fs4 from "fs";
558
- import * as path4 from "path";
722
+ import * as fs5 from "fs";
723
+ import * as path5 from "path";
559
724
  async function pushCommand(args) {
560
- const slug = resolveStore({ flag: flagString(args.flags, "store") });
561
- const manifestPath = path4.resolve(flagString(args.flags, "manifest") ?? "manifest.json");
562
- const bundlePath = path4.resolve(flagString(args.flags, "bundle") ?? "bundle.js");
563
- const sourcePath = path4.resolve(flagString(args.flags, "source") ?? "component.tsx");
564
- if (!fs4.existsSync(manifestPath)) {
565
- throw new Error(`Manifest not found: ${manifestPath}`);
725
+ const slug = requireProjectStore();
726
+ const root = process.cwd();
727
+ rejectLegacyLayout(root);
728
+ const componentsDir = path5.join(root, "components");
729
+ if (!fs5.existsSync(componentsDir)) {
730
+ throw new Error(
731
+ "No components/ directory here. Run `gs init <name>` to scaffold the project root and your first component."
732
+ );
733
+ }
734
+ const named = args.positional[0];
735
+ const manifestOverride = flagString(args.flags, "manifest");
736
+ const bundleOverride = flagString(args.flags, "bundle");
737
+ if (!named && (manifestOverride || bundleOverride)) {
738
+ throw new Error(
739
+ "--manifest / --bundle can only be used together with `gs push <name>`."
740
+ );
741
+ }
742
+ if (named) {
743
+ const outcome = await pushOne({
744
+ slug,
745
+ root,
746
+ name: named,
747
+ ...manifestOverride !== void 0 ? { manifestPath: manifestOverride } : {},
748
+ ...bundleOverride !== void 0 ? { bundlePath: bundleOverride } : {},
749
+ force: true
750
+ });
751
+ printOutcome2(outcome);
752
+ if (outcome.status === "failed") process.exitCode = 1;
753
+ return;
566
754
  }
567
- if (!fs4.existsSync(bundlePath)) {
568
- throw new Error(`Bundle not found: ${bundlePath} (did you run \`npm run build\`?)`);
755
+ const names = listLocalComponents(componentsDir);
756
+ if (names.length === 0) {
757
+ process.stdout.write(
758
+ "(no components in ./components \u2014 run `gs init <name>` to add one)\n"
759
+ );
760
+ return;
569
761
  }
570
- const manifestText = fs4.readFileSync(manifestPath, "utf8");
762
+ const outcomes = [];
763
+ for (const name of names) {
764
+ const outcome = await pushOne({ slug, root, name, force: false });
765
+ outcomes.push(outcome);
766
+ printOutcome2(outcome);
767
+ }
768
+ const pushed = outcomes.filter((o) => o.status === "pushed").length;
769
+ const unchanged = outcomes.filter((o) => o.status === "unchanged").length;
770
+ const failed = outcomes.filter((o) => o.status === "failed").length;
771
+ const summary = [`${pushed} pushed`, `${unchanged} unchanged`];
772
+ if (failed > 0) summary.push(`${failed} failed`);
773
+ process.stdout.write(`
774
+ ${summary.join(", ")} (of ${outcomes.length}) \u2192 ${slug}
775
+ `);
776
+ if (failed > 0) process.exitCode = 1;
777
+ }
778
+ async function pushOne(opts) {
779
+ const { slug, root, name, force } = opts;
780
+ const componentDir = path5.join(root, "components", name);
781
+ if (!fs5.existsSync(componentDir)) {
782
+ return {
783
+ name,
784
+ status: "failed",
785
+ message: `components/${name}/ does not exist`
786
+ };
787
+ }
788
+ const manifestPath = opts.manifestPath ? path5.resolve(opts.manifestPath) : path5.join(componentDir, "manifest.json");
789
+ const bundlePath = opts.bundlePath ? path5.resolve(opts.bundlePath) : path5.join(componentDir, "bundle.js");
790
+ const sourcePath = path5.join(componentDir, "component.tsx");
791
+ if (!fs5.existsSync(manifestPath)) {
792
+ return { name, status: "failed", message: `manifest not found: ${manifestPath}` };
793
+ }
794
+ if (!fs5.existsSync(bundlePath)) {
795
+ return {
796
+ name,
797
+ status: "failed",
798
+ message: `bundle not found: ${bundlePath} (did you run \`npm run build\`?)`
799
+ };
800
+ }
801
+ const manifestText = fs5.readFileSync(manifestPath, "utf8");
571
802
  let manifestName;
572
803
  try {
573
804
  const parsed = JSON.parse(manifestText);
574
805
  if (typeof parsed.name === "string") manifestName = parsed.name;
575
806
  } catch (err) {
576
- throw new Error(`Manifest is not valid JSON: ${err.message}`);
577
- }
578
- const name = args.positional[0] ?? manifestName;
579
- if (!name) {
580
- throw new Error("Could not determine component name. Pass it positionally or set `name` in manifest.json.");
807
+ return {
808
+ name,
809
+ status: "failed",
810
+ message: `manifest is not valid JSON: ${err.message}`
811
+ };
581
812
  }
582
813
  if (manifestName && manifestName !== name) {
583
- throw new Error(
584
- `Manifest name "${manifestName}" does not match argument "${name}". The server will reject this.`
585
- );
814
+ return {
815
+ name,
816
+ status: "failed",
817
+ message: `manifest name "${manifestName}" does not match folder name "${name}"`
818
+ };
819
+ }
820
+ if (!force) {
821
+ const hashes2 = computeComponentHashes(componentDir);
822
+ const sync = readSyncState(componentDir);
823
+ const unchanged = sync !== null && sync.manifestHash === hashes2.manifestHash && sync.sourceHash === (hashes2.sourceHash ?? "");
824
+ if (unchanged) {
825
+ return { name, status: "unchanged" };
826
+ }
586
827
  }
587
- const bundleText = fs4.readFileSync(bundlePath, "utf8");
588
- const sourceText = fs4.existsSync(sourcePath) ? fs4.readFileSync(sourcePath, "utf8") : null;
828
+ const bundleText = fs5.readFileSync(bundlePath, "utf8");
829
+ const sourceText = fs5.existsSync(sourcePath) ? fs5.readFileSync(sourcePath, "utf8") : null;
589
830
  const form = new FormData();
590
- form.append("manifest", new Blob([manifestText], { type: "application/json" }), "manifest.json");
591
- form.append("bundle", new Blob([bundleText], { type: "text/javascript" }), "bundle.js");
831
+ form.append(
832
+ "manifest",
833
+ new Blob([manifestText], { type: "application/json" }),
834
+ "manifest.json"
835
+ );
836
+ form.append(
837
+ "bundle",
838
+ new Blob([bundleText], { type: "text/javascript" }),
839
+ "bundle.js"
840
+ );
592
841
  if (sourceText !== null) {
593
842
  form.append(
594
843
  "source",
595
844
  new Blob([sourceText], { type: "text/plain; charset=utf-8" }),
596
- path4.basename(sourcePath)
845
+ "component.tsx"
597
846
  );
598
847
  }
599
848
  const url = `${apiBaseFor(slug)}/api/builder/components/${encodeURIComponent(name)}`;
600
- const data = await request(url, { method: "POST", multipart: form });
601
- const sourceNote = sourceText === null ? " (no source uploaded \u2014 `component.tsx` not found in cwd)" : "";
602
- process.stdout.write(
603
- `Pushed ${name} v${data.version} (draft) to ${slug}${sourceNote}. View: ${data.permalink}
604
- Run \`gs publish ${name}\` to promote.
849
+ let data;
850
+ try {
851
+ data = await request(url, { method: "POST", multipart: form });
852
+ } catch (err) {
853
+ return {
854
+ name,
855
+ status: "failed",
856
+ message: err instanceof Error ? err.message : String(err)
857
+ };
858
+ }
859
+ const hashes = computeComponentHashes(componentDir);
860
+ writeSyncState(componentDir, {
861
+ version: data.version,
862
+ manifestHash: hashes.manifestHash,
863
+ sourceHash: hashes.sourceHash ?? ""
864
+ });
865
+ return {
866
+ name,
867
+ status: "pushed",
868
+ version: data.version,
869
+ permalink: data.permalink
870
+ };
871
+ }
872
+ function listLocalComponents(componentsDir) {
873
+ return fs5.readdirSync(componentsDir).filter((entry) => {
874
+ const dir = path5.join(componentsDir, entry);
875
+ return fs5.statSync(dir).isDirectory() && fs5.existsSync(path5.join(dir, "manifest.json"));
876
+ }).sort();
877
+ }
878
+ function rejectLegacyLayout(root) {
879
+ const rootManifest = path5.join(root, "manifest.json");
880
+ const componentsDir = path5.join(root, "components");
881
+ if (fs5.existsSync(rootManifest) && !fs5.existsSync(componentsDir)) {
882
+ throw new Error(
883
+ [
884
+ "Detected the old single-component layout (manifest.json at the project root).",
885
+ "The Builder CLI now uses a multi-component layout: scaffold a fresh dir with",
886
+ "`gs init`, then move your component into `components/<name>/`. Re-run `gs push`",
887
+ "from the new project root."
888
+ ].join(" ")
889
+ );
890
+ }
891
+ }
892
+ function printOutcome2(outcome) {
893
+ switch (outcome.status) {
894
+ case "pushed":
895
+ process.stdout.write(
896
+ ` pushed ${outcome.name} v${outcome.version} \u2192 ${outcome.permalink ?? ""}
605
897
  `
606
- );
898
+ );
899
+ return;
900
+ case "unchanged":
901
+ process.stdout.write(` unchanged ${outcome.name}
902
+ `);
903
+ return;
904
+ case "failed":
905
+ process.stdout.write(
906
+ ` failed ${outcome.name}: ${outcome.message ?? "unknown error"}
907
+ `
908
+ );
909
+ return;
910
+ }
607
911
  }
608
912
 
609
913
  // src/commands/publish.ts
610
914
  async function publishCommand(args) {
611
915
  const name = args.positional[0];
612
916
  if (!name) throw new Error("Usage: gs publish <name> [--version N]");
613
- const slug = resolveStore({ flag: flagString(args.flags, "store") });
917
+ const slug = requireProjectStore();
614
918
  const url = `${apiBaseFor(slug)}/api/builder/components/${encodeURIComponent(name)}/publish`;
615
919
  const versionFlag = flagString(args.flags, "version");
616
920
  let body = void 0;
@@ -634,7 +938,7 @@ async function publishCommand(args) {
634
938
  async function unpublishCommand(args) {
635
939
  const name = args.positional[0];
636
940
  if (!name) throw new Error("Usage: gs unpublish <name>");
637
- const slug = resolveStore({ flag: flagString(args.flags, "store") });
941
+ const slug = requireProjectStore();
638
942
  const url = `${apiBaseFor(slug)}/api/builder/components/${encodeURIComponent(name)}/unpublish`;
639
943
  await request(url, { method: "POST", body: {} });
640
944
  process.stdout.write(`Unpublished ${name}. The draft and version history are retained.
@@ -646,7 +950,7 @@ import * as readline from "readline";
646
950
  async function deleteCommand(args) {
647
951
  const name = args.positional[0];
648
952
  if (!name) throw new Error("Usage: gs delete <name> [--yes]");
649
- const slug = resolveStore({ flag: flagString(args.flags, "store") });
953
+ const slug = requireProjectStore();
650
954
  if (!flagBool(args.flags, "yes")) {
651
955
  const confirmed = await prompt(
652
956
  `Soft-delete component "${name}" in store "${slug}"? Type "yes" to confirm: `
@@ -662,71 +966,131 @@ async function deleteCommand(args) {
662
966
  `);
663
967
  }
664
968
  function prompt(question) {
665
- return new Promise((resolve5) => {
969
+ return new Promise((resolve4) => {
666
970
  const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
667
971
  rl.question(question, (answer) => {
668
972
  rl.close();
669
- resolve5(answer);
973
+ resolve4(answer);
670
974
  });
671
975
  });
672
976
  }
673
977
 
674
978
  // src/commands/init.ts
675
- import * as fs5 from "fs";
676
- import * as path5 from "path";
979
+ import * as fs6 from "fs";
980
+ import * as path6 from "path";
677
981
  var NAME_REGEX = /^[a-z][a-z0-9_]*$/;
678
982
  function initCommand(args) {
679
983
  const name = args.positional[0];
680
- if (!name) {
681
- throw new Error("Usage: gs init <name> [--out <dir>] [--store <slug>] [--force]");
682
- }
683
- if (!NAME_REGEX.test(name)) {
984
+ if (name !== void 0 && !NAME_REGEX.test(name)) {
684
985
  throw new Error(`Invalid component name: "${name}" (must match ${NAME_REGEX}).`);
685
986
  }
686
- const outRel = flagString(args.flags, "out") ?? name;
687
- const out = path5.resolve(outRel);
688
987
  const force = flagBool(args.flags, "force");
689
988
  const storeFlag = flagString(args.flags, "store");
690
- ensureWritable(out, force);
691
- fs5.mkdirSync(out, { recursive: true });
692
- for (const [relPath, content] of files({ name, store: storeFlag })) {
693
- const full = path5.join(out, relPath);
694
- fs5.mkdirSync(path5.dirname(full), { recursive: true });
695
- fs5.writeFileSync(full, content);
989
+ const outRel = flagString(args.flags, "out") ?? ".";
990
+ const root = path6.resolve(outRel);
991
+ const rootExisted = hasRootScaffold(root);
992
+ if (rootExisted) {
993
+ if (storeFlag !== void 0) {
994
+ throw new Error(
995
+ `Project at ${root} is already pinned to a store via .gsrc. Drop --store; a project folder ships to exactly one store.`
996
+ );
997
+ }
998
+ } else if (storeFlag === void 0) {
999
+ const example = name ? `gs init ${name} --store <slug>` : `gs init --store <slug>`;
1000
+ throw new Error(
1001
+ `--store <slug> is required when scaffolding a new project root. Run \`${example}\`.`
1002
+ );
1003
+ }
1004
+ ensureRoot(root, rootExisted, force, { store: storeFlag });
1005
+ if (!name) {
1006
+ const created = rootExisted ? "Updated" : "Scaffolded";
1007
+ process.stdout.write(
1008
+ [
1009
+ `${created} GreatStore project root in ${root}.`,
1010
+ "",
1011
+ "Next steps:",
1012
+ ` cd ${path6.relative(process.cwd(), root) || "."}`,
1013
+ " npm install",
1014
+ " gs init <component_name> # add your first component",
1015
+ ""
1016
+ ].join("\n")
1017
+ );
1018
+ return;
696
1019
  }
1020
+ const componentDir = path6.join(root, "components", name);
1021
+ ensureComponent(componentDir, name, force);
1022
+ const projectLabel = path6.relative(process.cwd(), root) || ".";
697
1023
  process.stdout.write(
698
1024
  [
699
- `Scaffolded "${name}" in ${out}.`,
1025
+ `Added component "${name}" at components/${name}/.`,
700
1026
  "",
701
1027
  "Next steps:",
702
- ` cd ${path5.relative(process.cwd(), out) || "."}`,
703
- " npm install",
1028
+ ...rootExisted ? [] : [` cd ${projectLabel}`, " npm install"],
1029
+ ` # edit components/${name}/component.tsx and manifest.json`,
704
1030
  " npm run build",
705
1031
  " gs push",
706
1032
  ""
707
1033
  ].join("\n")
708
1034
  );
709
1035
  }
710
- function ensureWritable(dir, force) {
711
- if (!fs5.existsSync(dir)) return;
712
- const entries = fs5.readdirSync(dir);
713
- if (entries.length === 0) return;
714
- if (force) return;
715
- throw new Error(
716
- `Refusing to scaffold into non-empty directory ${dir}. Pass --force to override.`
717
- );
1036
+ function hasRootScaffold(dir) {
1037
+ return fs6.existsSync(path6.join(dir, "package.json")) && fs6.existsSync(path6.join(dir, "build.mjs"));
1038
+ }
1039
+ function ensureRoot(root, rootExisted, force, opts) {
1040
+ fs6.mkdirSync(root, { recursive: true });
1041
+ if (rootExisted) {
1042
+ fs6.mkdirSync(path6.join(root, "components"), { recursive: true });
1043
+ return;
1044
+ }
1045
+ if (!force) {
1046
+ const entries = fs6.readdirSync(root).filter((e) => e !== ".gsrc");
1047
+ if (entries.length > 0) {
1048
+ throw new Error(
1049
+ `Refusing to scaffold project root into non-empty directory ${root}. Pass --force to override.`
1050
+ );
1051
+ }
1052
+ }
1053
+ for (const [relPath, content] of rootFiles({ store: opts.store })) {
1054
+ const full = path6.join(root, relPath);
1055
+ fs6.mkdirSync(path6.dirname(full), { recursive: true });
1056
+ if (force || !fs6.existsSync(full)) {
1057
+ fs6.writeFileSync(full, content);
1058
+ }
1059
+ }
1060
+ fs6.mkdirSync(path6.join(root, "components"), { recursive: true });
718
1061
  }
719
- function files(opts) {
720
- const { name, store } = opts;
1062
+ function ensureComponent(componentDir, name, force) {
1063
+ if (fs6.existsSync(componentDir) && !force) {
1064
+ const entries = fs6.readdirSync(componentDir);
1065
+ if (entries.length > 0) {
1066
+ throw new Error(
1067
+ `Refusing to overwrite existing components/${name}/. Pass --force to override.`
1068
+ );
1069
+ }
1070
+ }
1071
+ fs6.mkdirSync(componentDir, { recursive: true });
1072
+ for (const [relPath, content] of componentFiles(name)) {
1073
+ fs6.writeFileSync(path6.join(componentDir, relPath), content);
1074
+ }
1075
+ }
1076
+ function rootFiles(opts) {
1077
+ if (!opts.store) {
1078
+ throw new Error("internal: rootFiles called without a store slug");
1079
+ }
721
1080
  return [
722
- ["manifest.json", manifest(name)],
723
- ["component.tsx", component(name)],
724
- ["vite.config.ts", viteConfig()],
725
- ["tsconfig.json", tsconfig()],
726
- ["package.json", packageJson(name)],
727
- [".gsrc", gsrc(store)],
1081
+ ["package.json", rootPackageJson()],
1082
+ ["build.mjs", buildScript()],
1083
+ ["vite.config.ts", viteEditorConfig()],
1084
+ ["tsconfig.json", rootTsconfig()],
1085
+ [".gsrc", gsrc(opts.store)],
728
1086
  [".gitignore", gitignore()],
729
- ["README.md", readme(name)]
1087
+ ["README.md", rootReadme()]
1088
+ ];
1089
+ }
1090
+ function componentFiles(name) {
1091
+ return [
1092
+ ["manifest.json", manifest(name)],
1093
+ ["component.tsx", component(name)]
730
1094
  ];
731
1095
  }
732
1096
  function manifest(name) {
@@ -767,51 +1131,86 @@ export default function ${pascal(name)}(_props: Props): React.ReactElement {
767
1131
  }
768
1132
  `;
769
1133
  }
770
- function viteConfig() {
771
- return `import { defineConfig } from "vite";
1134
+ function buildScript() {
1135
+ return `import { build } from "vite";
772
1136
  import react from "@vitejs/plugin-react";
1137
+ import * as fs from "node:fs";
1138
+ import * as path from "node:path";
773
1139
 
774
- // Builds a single ESM bundle suitable for \`gs push\`.
775
- //
776
- // React / react-dom / react/jsx-runtime are EXTERNALIZED and rewritten
777
- // at build time to point at GreatStore's runtime shim at
778
- // \`/assets/remote-components/_runtime.js\`. The shim re-exports the host
779
- // chat tree's React instance off \`window.__GS_REMOTE_RUNTIME__\`, so
780
- // the component and the host share one React. Bundling React in
781
- // instead would crash on the first hook call because the bundled-in
782
- // React's dispatcher is null when the chat tree drives the render.
783
- //
784
- // Do NOT remove the \`paths\` mapping \u2014 without it the browser tries to
785
- // resolve a bare \`react\` specifier and fails before the component
786
- // renders.
787
- export default defineConfig({
788
- plugins: [react()],
789
- build: {
790
- lib: {
791
- entry: "component.tsx",
792
- formats: ["es"],
793
- fileName: () => "bundle.js",
794
- },
795
- outDir: ".",
796
- emptyOutDir: false,
797
- rollupOptions: {
798
- external: ["react", "react-dom", "react/jsx-runtime"],
799
- output: {
800
- entryFileNames: "bundle.js",
801
- paths: {
802
- react: "/assets/remote-components/_runtime.js",
803
- "react-dom": "/assets/remote-components/_runtime.js",
804
- "react/jsx-runtime": "/assets/remote-components/_runtime.js",
1140
+ const COMPONENTS_DIR = path.resolve("components");
1141
+
1142
+ function listComponents() {
1143
+ if (!fs.existsSync(COMPONENTS_DIR)) return [];
1144
+ return fs
1145
+ .readdirSync(COMPONENTS_DIR)
1146
+ .filter((entry) => {
1147
+ const dir = path.join(COMPONENTS_DIR, entry);
1148
+ return (
1149
+ fs.statSync(dir).isDirectory() &&
1150
+ fs.existsSync(path.join(dir, "component.tsx"))
1151
+ );
1152
+ })
1153
+ .sort();
1154
+ }
1155
+
1156
+ const components = listComponents();
1157
+ if (components.length === 0) {
1158
+ console.log("(no components in ./components \u2014 run \`gs init <name>\` to add one)");
1159
+ process.exit(0);
1160
+ }
1161
+
1162
+ const target = process.argv[2];
1163
+ const queue = target ? components.filter((c) => c === target) : components;
1164
+ if (target && queue.length === 0) {
1165
+ console.error(\`No component named "\${target}" in ./components\`);
1166
+ process.exit(1);
1167
+ }
1168
+
1169
+ for (const name of queue) {
1170
+ const dir = path.join(COMPONENTS_DIR, name);
1171
+ await build({
1172
+ plugins: [react()],
1173
+ logLevel: "warn",
1174
+ build: {
1175
+ lib: {
1176
+ entry: path.join(dir, "component.tsx"),
1177
+ formats: ["es"],
1178
+ fileName: () => "bundle.js",
1179
+ },
1180
+ outDir: dir,
1181
+ emptyOutDir: false,
1182
+ rollupOptions: {
1183
+ external: ["react", "react-dom", "react/jsx-runtime"],
1184
+ output: {
1185
+ entryFileNames: "bundle.js",
1186
+ paths: {
1187
+ react: "/assets/remote-components/_runtime.js",
1188
+ "react-dom": "/assets/remote-components/_runtime.js",
1189
+ "react/jsx-runtime": "/assets/remote-components/_runtime.js",
1190
+ },
805
1191
  },
806
1192
  },
1193
+ minify: true,
1194
+ sourcemap: false,
807
1195
  },
808
- minify: true,
809
- sourcemap: false,
810
- },
1196
+ });
1197
+ console.log(\`built components/\${name}/bundle.js\`);
1198
+ }
1199
+ `;
1200
+ }
1201
+ function viteEditorConfig() {
1202
+ return `import { defineConfig } from "vite";
1203
+ import react from "@vitejs/plugin-react";
1204
+
1205
+ // Real builds happen in build.mjs (one Vite invocation per component).
1206
+ // This file exists only so editors / language servers can resolve the
1207
+ // React plugin when inspecting components/*/component.tsx.
1208
+ export default defineConfig({
1209
+ plugins: [react()],
811
1210
  });
812
1211
  `;
813
1212
  }
814
- function tsconfig() {
1213
+ function rootTsconfig() {
815
1214
  return JSON.stringify(
816
1215
  {
817
1216
  compilerOptions: {
@@ -826,22 +1225,22 @@ function tsconfig() {
826
1225
  isolatedModules: true,
827
1226
  noEmit: true
828
1227
  },
829
- include: ["component.tsx", "vite.config.ts"]
1228
+ include: ["components/**/component.tsx", "vite.config.ts", "build.mjs"]
830
1229
  },
831
1230
  null,
832
1231
  2
833
1232
  ) + "\n";
834
1233
  }
835
- function packageJson(name) {
1234
+ function rootPackageJson() {
836
1235
  return JSON.stringify(
837
1236
  {
838
- name,
1237
+ name: "greatstore-components",
839
1238
  version: "0.0.1",
840
1239
  private: true,
841
1240
  type: "module",
842
1241
  scripts: {
843
- build: "vite build",
844
- push: "vite build && gs push"
1242
+ build: "node build.mjs",
1243
+ push: "node build.mjs && gs push"
845
1244
  },
846
1245
  dependencies: {
847
1246
  react: "^19.0.0",
@@ -860,25 +1259,37 @@ function packageJson(name) {
860
1259
  ) + "\n";
861
1260
  }
862
1261
  function gsrc(store) {
863
- return JSON.stringify({ store: store ?? "<your-store-slug>" }, null, 2) + "\n";
1262
+ return JSON.stringify({ store }, null, 2) + "\n";
864
1263
  }
865
1264
  function gitignore() {
866
- return ["node_modules/", "bundle.js", "*.tsbuildinfo", ".DS_Store", ""].join("\n");
1265
+ return [
1266
+ "node_modules/",
1267
+ "components/*/bundle.js",
1268
+ "components/*/.gssync.json",
1269
+ "*.tsbuildinfo",
1270
+ ".DS_Store",
1271
+ ""
1272
+ ].join("\n");
867
1273
  }
868
- function readme(name) {
869
- return `# ${name}
1274
+ function rootReadme() {
1275
+ return `# GreatStore components
870
1276
 
871
- A GreatStore custom component scaffolded with \`gs init\`.
1277
+ Custom React components published to your GreatStore tenant. Each
1278
+ component lives in its own folder under \`components/\`.
872
1279
 
873
1280
  \`\`\`
874
1281
  npm install
875
- npm run build # produces bundle.js
876
- gs push # uploads as a draft revision
877
- gs publish ${name}
1282
+ gs init <component_name> # add a new component
1283
+ npm run build # builds every component/<name>/bundle.js
1284
+ gs push # uploads every changed component as a draft
1285
+ gs publish <component_name> # promote a specific component to live
878
1286
  \`\`\`
879
1287
 
880
- Edit \`component.tsx\` for the UI and \`manifest.json\` for the metadata
881
- that the LLM sees (especially \`description\` and \`inputSchema\`).
1288
+ - \`gs push\` (no args) hashes each component and only uploads the ones
1289
+ that have changed since the last sync.
1290
+ - \`gs pull\` (no args) refreshes every remote component into
1291
+ \`components/<name>/\`. Components with unsaved local edits are skipped
1292
+ with a warning; pass \`--force\` to overwrite.
882
1293
  `;
883
1294
  }
884
1295
  function pascal(name) {
@@ -886,7 +1297,7 @@ function pascal(name) {
886
1297
  }
887
1298
 
888
1299
  // src/index.ts
889
- var VERSION = true ? "0.0.10" : "0.0.0-dev";
1300
+ var VERSION = true ? "0.0.11-beta.1" : "0.0.0-dev";
890
1301
  var HELP = `gs \u2014 GreatStore CLI (v${VERSION})
891
1302
 
892
1303
  Usage:
@@ -896,28 +1307,27 @@ Commands:
896
1307
  login Open browser, capture nav token, persist credentials.
897
1308
  logout Wipe ~/.greatstore/credentials.json.
898
1309
  whoami Print the identity stored locally.
899
- init <name> Scaffold a component project.
1310
+ init [<name>] Scaffold project root; add \`components/<name>/\` if name given.
900
1311
  list List components in the current store.
901
- pull <name> Download manifest.json + bundle.js.
902
- push [<name>] Upload manifest.json + bundle.js as a new draft.
1312
+ pull [<name>|*] Download remote component(s) into \`components/<name>/\`. No args = all.
1313
+ push [<name>] Upload changed components from \`components/\`. No args = all (skips unchanged).
903
1314
  publish <name> Promote the draft (or --version N) to live.
904
1315
  unpublish <name> Clear the live pointer.
905
1316
  delete <name> Soft-delete the component.
906
1317
 
907
1318
  Common flags:
908
- --store <slug> Target store (else .gsrc or GS_STORE).
1319
+ --store <slug> \`init\` only \u2014 writes the slug into .gsrc. Other commands read .gsrc.
909
1320
  --json Machine-readable output for \`list\`.
910
1321
  --draft | --live | --version N Revision selector for \`pull\`.
911
1322
  -o, --out <dir> Output directory for \`pull\` / \`init\`.
912
1323
  --manifest <path> Path to manifest for \`push\`.
913
1324
  --bundle <path> Path to bundle for \`push\`.
914
- --force Overwrite for \`init\`; skip confirmation for \`delete\`.
1325
+ --force Overwrite for \`init\` / \`pull\`; skip confirmation for \`delete\`.
915
1326
  --yes Skip confirmation for \`delete\`.
916
1327
  -h, --help Show this help.
917
1328
  -v, --version Print version.
918
1329
 
919
1330
  Environment:
920
- GS_STORE Fallback for --store.
921
1331
  GS_API_BASE Override API base (full URL, no trailing slash).
922
1332
  GS_NAV_BASE Override nav OAuth base.
923
1333
  GS_BROWSER_CMD Override the browser-open command.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@greatstore/cli",
3
- "version": "0.0.10",
3
+ "version": "0.0.11-beta.1",
4
4
  "description": "CLI for authoring and shipping GreatStore custom components.",
5
5
  "license": "UNLICENSED",
6
6
  "type": "module",