@greatstore/cli 0.0.9 → 0.0.11-beta.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 (3) hide show
  1. package/README.md +7 -0
  2. package/dist/cli.js +555 -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
@@ -71,7 +71,9 @@ var SUCCESS_HTML = `<!doctype html>
71
71
  <style>body{font:14px/1.5 system-ui;margin:4rem auto;max-width:32rem;color:#222;text-align:center}
72
72
  h1{font-size:1.4rem}code{background:#f4f4f4;padding:.1em .3em;border-radius:.2em}</style></head>
73
73
  <body><h1>You're signed in.</h1>
74
- <p>You can close this window and return to your terminal.</p></body></html>`;
74
+ <p>This window will close automatically. If it doesn't, you can close it and return to your terminal.</p>
75
+ <script>setTimeout(function(){window.close();},250);</script>
76
+ </body></html>`;
75
77
  var FAILURE_HTML = `<!doctype html>
76
78
  <html lang="en"><head><meta charset="utf-8"><title>GreatStore CLI</title>
77
79
  <style>body{font:14px/1.5 system-ui;margin:4rem auto;max-width:32rem;color:#222;text-align:center}
@@ -90,7 +92,7 @@ async function captureLoopbackToken(options) {
90
92
  const open = options.openBrowser ?? ((url) => openInBrowser(url, options.browserCmdEnv));
91
93
  const server = http.createServer();
92
94
  try {
93
- 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));
94
96
  const address = server.address();
95
97
  const redirectUri = `http://127.0.0.1:${address.port}${CALLBACK_PATH}`;
96
98
  const authUrl = `${options.navBaseUrl}/connect_oauth_done?redirect_uri=${encodeURIComponent(redirectUri)}&state=${encodeURIComponent(state)}`;
@@ -102,7 +104,7 @@ async function captureLoopbackToken(options) {
102
104
  }
103
105
  }
104
106
  function waitForCallback(server, expectedState, timeoutMs) {
105
- return new Promise((resolve5, reject) => {
107
+ return new Promise((resolve4, reject) => {
106
108
  let settled = false;
107
109
  const settle = (fn) => {
108
110
  if (settled) return;
@@ -148,7 +150,7 @@ function waitForCallback(server, expectedState, timeoutMs) {
148
150
  res.writeHead(200, { "content-type": "text/html" });
149
151
  res.end(SUCCESS_HTML);
150
152
  clearTimeout(timer);
151
- settle(() => resolve5({ token }));
153
+ settle(() => resolve4({ token }));
152
154
  });
153
155
  });
154
156
  }
@@ -271,16 +273,11 @@ var StoreResolutionError = class extends Error {
271
273
  this.name = "StoreResolutionError";
272
274
  }
273
275
  };
274
- function resolveStore(input = {}) {
275
- const flag = input.flag?.trim();
276
- if (flag) return flag;
276
+ function requireProjectStore(input = {}) {
277
277
  const fromRc = findGsrc(input.cwd ?? process.cwd());
278
278
  if (fromRc) return fromRc;
279
- const env = input.env ?? process.env;
280
- const fromEnv = env.GS_STORE?.trim();
281
- if (fromEnv) return fromEnv;
282
279
  throw new StoreResolutionError(
283
- '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."
284
281
  );
285
282
  }
286
283
  function findGsrc(cwd) {
@@ -477,7 +474,7 @@ function whoamiCommand() {
477
474
 
478
475
  // src/commands/list.ts
479
476
  async function listCommand(args) {
480
- const slug = resolveStore({ flag: flagString(args.flags, "store") });
477
+ const slug = requireProjectStore();
481
478
  const url = `${apiBaseFor(slug)}/api/builder/components`;
482
479
  const data = await request(url);
483
480
  if (flagBool(args.flags, "json")) {
@@ -514,34 +511,181 @@ function pad(s, width) {
514
511
  }
515
512
 
516
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";
517
519
  import * as fs3 from "fs";
518
520
  import * as path3 from "path";
519
- async function pullCommand(args) {
520
- const name = args.positional[0];
521
- if (!name) {
522
- 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}`);
523
533
  }
524
- const slug = resolveStore({ flag: flagString(args.flags, "store") });
525
- const out = flagString(args.flags, "out") ?? ".";
526
- const query = buildRevisionQuery(args);
527
- const url = `${apiBaseFor(slug)}/api/builder/components/${encodeURIComponent(name)}${query}`;
528
- const data = await request(url);
529
- 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) {
530
557
  fs3.writeFileSync(
531
- path3.join(out, "manifest.json"),
532
- JSON.stringify(data.manifest, null, 2) + "\n"
558
+ path3.join(componentDir, SYNC_FILE),
559
+ JSON.stringify(state, null, 2) + "\n"
533
560
  );
534
- fs3.writeFileSync(path3.join(out, "bundle.js"), data.bundle);
535
- const wrote = ["manifest.json", "bundle.js"];
536
- if (data.source !== null) {
537
- fs3.writeFileSync(path3.join(out, "component.tsx"), data.source);
538
- 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 });
587
+ }
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);
539
627
  }
540
- const sourceNote = data.source === null ? " (no source \u2014 this version was pushed without component.tsx)" : "";
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`);
541
633
  process.stdout.write(
542
- `Pulled ${name} v${data.version} \u2192 ${path3.resolve(out)}/{${wrote.join(", ")}}${sourceNote}
634
+ `
635
+ ${summary.join(", ")} (of ${outcomes.length}) \u2190 ${slug}
543
636
  `
544
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 });
545
689
  }
546
690
  function buildRevisionQuery(args) {
547
691
  const version = flagString(args.flags, "version");
@@ -550,65 +694,227 @@ function buildRevisionQuery(args) {
550
694
  if (flagBool(args.flags, "live")) return `?revision=live`;
551
695
  return "";
552
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
+ }
553
720
 
554
721
  // src/commands/push.ts
555
- import * as fs4 from "fs";
556
- import * as path4 from "path";
722
+ import * as fs5 from "fs";
723
+ import * as path5 from "path";
557
724
  async function pushCommand(args) {
558
- const slug = resolveStore({ flag: flagString(args.flags, "store") });
559
- const manifestPath = path4.resolve(flagString(args.flags, "manifest") ?? "manifest.json");
560
- const bundlePath = path4.resolve(flagString(args.flags, "bundle") ?? "bundle.js");
561
- const sourcePath = path4.resolve(flagString(args.flags, "source") ?? "component.tsx");
562
- if (!fs4.existsSync(manifestPath)) {
563
- 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;
754
+ }
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;
761
+ }
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}` };
564
793
  }
565
- if (!fs4.existsSync(bundlePath)) {
566
- throw new Error(`Bundle not found: ${bundlePath} (did you run \`npm run build\`?)`);
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
+ };
567
800
  }
568
- const manifestText = fs4.readFileSync(manifestPath, "utf8");
801
+ const manifestText = fs5.readFileSync(manifestPath, "utf8");
569
802
  let manifestName;
570
803
  try {
571
804
  const parsed = JSON.parse(manifestText);
572
805
  if (typeof parsed.name === "string") manifestName = parsed.name;
573
806
  } catch (err) {
574
- throw new Error(`Manifest is not valid JSON: ${err.message}`);
575
- }
576
- const name = args.positional[0] ?? manifestName;
577
- if (!name) {
578
- 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
+ };
579
812
  }
580
813
  if (manifestName && manifestName !== name) {
581
- throw new Error(
582
- `Manifest name "${manifestName}" does not match argument "${name}". The server will reject this.`
583
- );
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
+ }
584
827
  }
585
- const bundleText = fs4.readFileSync(bundlePath, "utf8");
586
- 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;
587
830
  const form = new FormData();
588
- form.append("manifest", new Blob([manifestText], { type: "application/json" }), "manifest.json");
589
- 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
+ );
590
841
  if (sourceText !== null) {
591
842
  form.append(
592
843
  "source",
593
844
  new Blob([sourceText], { type: "text/plain; charset=utf-8" }),
594
- path4.basename(sourcePath)
845
+ "component.tsx"
595
846
  );
596
847
  }
597
848
  const url = `${apiBaseFor(slug)}/api/builder/components/${encodeURIComponent(name)}`;
598
- const data = await request(url, { method: "POST", multipart: form });
599
- const sourceNote = sourceText === null ? " (no source uploaded \u2014 `component.tsx` not found in cwd)" : "";
600
- process.stdout.write(
601
- `Pushed ${name} v${data.version} (draft) to ${slug}${sourceNote}. View: ${data.permalink}
602
- 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 ?? ""}
603
897
  `
604
- );
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
+ }
605
911
  }
606
912
 
607
913
  // src/commands/publish.ts
608
914
  async function publishCommand(args) {
609
915
  const name = args.positional[0];
610
916
  if (!name) throw new Error("Usage: gs publish <name> [--version N]");
611
- const slug = resolveStore({ flag: flagString(args.flags, "store") });
917
+ const slug = requireProjectStore();
612
918
  const url = `${apiBaseFor(slug)}/api/builder/components/${encodeURIComponent(name)}/publish`;
613
919
  const versionFlag = flagString(args.flags, "version");
614
920
  let body = void 0;
@@ -632,7 +938,7 @@ async function publishCommand(args) {
632
938
  async function unpublishCommand(args) {
633
939
  const name = args.positional[0];
634
940
  if (!name) throw new Error("Usage: gs unpublish <name>");
635
- const slug = resolveStore({ flag: flagString(args.flags, "store") });
941
+ const slug = requireProjectStore();
636
942
  const url = `${apiBaseFor(slug)}/api/builder/components/${encodeURIComponent(name)}/unpublish`;
637
943
  await request(url, { method: "POST", body: {} });
638
944
  process.stdout.write(`Unpublished ${name}. The draft and version history are retained.
@@ -644,7 +950,7 @@ import * as readline from "readline";
644
950
  async function deleteCommand(args) {
645
951
  const name = args.positional[0];
646
952
  if (!name) throw new Error("Usage: gs delete <name> [--yes]");
647
- const slug = resolveStore({ flag: flagString(args.flags, "store") });
953
+ const slug = requireProjectStore();
648
954
  if (!flagBool(args.flags, "yes")) {
649
955
  const confirmed = await prompt(
650
956
  `Soft-delete component "${name}" in store "${slug}"? Type "yes" to confirm: `
@@ -660,71 +966,116 @@ async function deleteCommand(args) {
660
966
  `);
661
967
  }
662
968
  function prompt(question) {
663
- return new Promise((resolve5) => {
969
+ return new Promise((resolve4) => {
664
970
  const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
665
971
  rl.question(question, (answer) => {
666
972
  rl.close();
667
- resolve5(answer);
973
+ resolve4(answer);
668
974
  });
669
975
  });
670
976
  }
671
977
 
672
978
  // src/commands/init.ts
673
- import * as fs5 from "fs";
674
- import * as path5 from "path";
979
+ import * as fs6 from "fs";
980
+ import * as path6 from "path";
675
981
  var NAME_REGEX = /^[a-z][a-z0-9_]*$/;
676
982
  function initCommand(args) {
677
983
  const name = args.positional[0];
678
- if (!name) {
679
- throw new Error("Usage: gs init <name> [--out <dir>] [--store <slug>] [--force]");
680
- }
681
- if (!NAME_REGEX.test(name)) {
984
+ if (name !== void 0 && !NAME_REGEX.test(name)) {
682
985
  throw new Error(`Invalid component name: "${name}" (must match ${NAME_REGEX}).`);
683
986
  }
684
- const outRel = flagString(args.flags, "out") ?? name;
685
- const out = path5.resolve(outRel);
686
987
  const force = flagBool(args.flags, "force");
687
988
  const storeFlag = flagString(args.flags, "store");
688
- ensureWritable(out, force);
689
- fs5.mkdirSync(out, { recursive: true });
690
- for (const [relPath, content] of files({ name, store: storeFlag })) {
691
- const full = path5.join(out, relPath);
692
- fs5.mkdirSync(path5.dirname(full), { recursive: true });
693
- fs5.writeFileSync(full, content);
989
+ const outRel = flagString(args.flags, "out") ?? ".";
990
+ const root = path6.resolve(outRel);
991
+ const rootExisted = hasRootScaffold(root);
992
+ ensureRoot(root, rootExisted, force, { store: storeFlag });
993
+ if (!name) {
994
+ const created = rootExisted ? "Updated" : "Scaffolded";
995
+ process.stdout.write(
996
+ [
997
+ `${created} GreatStore project root in ${root}.`,
998
+ "",
999
+ "Next steps:",
1000
+ ` cd ${path6.relative(process.cwd(), root) || "."}`,
1001
+ " npm install",
1002
+ " gs init <component_name> # add your first component",
1003
+ ""
1004
+ ].join("\n")
1005
+ );
1006
+ return;
694
1007
  }
1008
+ const componentDir = path6.join(root, "components", name);
1009
+ ensureComponent(componentDir, name, force);
1010
+ const projectLabel = path6.relative(process.cwd(), root) || ".";
695
1011
  process.stdout.write(
696
1012
  [
697
- `Scaffolded "${name}" in ${out}.`,
1013
+ `Added component "${name}" at components/${name}/.`,
698
1014
  "",
699
1015
  "Next steps:",
700
- ` cd ${path5.relative(process.cwd(), out) || "."}`,
701
- " npm install",
1016
+ ...rootExisted ? [] : [` cd ${projectLabel}`, " npm install"],
1017
+ ` # edit components/${name}/component.tsx and manifest.json`,
702
1018
  " npm run build",
703
1019
  " gs push",
704
1020
  ""
705
1021
  ].join("\n")
706
1022
  );
707
1023
  }
708
- function ensureWritable(dir, force) {
709
- if (!fs5.existsSync(dir)) return;
710
- const entries = fs5.readdirSync(dir);
711
- if (entries.length === 0) return;
712
- if (force) return;
713
- throw new Error(
714
- `Refusing to scaffold into non-empty directory ${dir}. Pass --force to override.`
715
- );
1024
+ function hasRootScaffold(dir) {
1025
+ return fs6.existsSync(path6.join(dir, "package.json")) && fs6.existsSync(path6.join(dir, "build.mjs"));
1026
+ }
1027
+ function ensureRoot(root, rootExisted, force, opts) {
1028
+ fs6.mkdirSync(root, { recursive: true });
1029
+ if (rootExisted) {
1030
+ fs6.mkdirSync(path6.join(root, "components"), { recursive: true });
1031
+ return;
1032
+ }
1033
+ if (!force) {
1034
+ const entries = fs6.readdirSync(root).filter((e) => e !== ".gsrc");
1035
+ if (entries.length > 0) {
1036
+ throw new Error(
1037
+ `Refusing to scaffold project root into non-empty directory ${root}. Pass --force to override.`
1038
+ );
1039
+ }
1040
+ }
1041
+ for (const [relPath, content] of rootFiles({ store: opts.store })) {
1042
+ const full = path6.join(root, relPath);
1043
+ fs6.mkdirSync(path6.dirname(full), { recursive: true });
1044
+ if (force || !fs6.existsSync(full)) {
1045
+ fs6.writeFileSync(full, content);
1046
+ }
1047
+ }
1048
+ fs6.mkdirSync(path6.join(root, "components"), { recursive: true });
716
1049
  }
717
- function files(opts) {
718
- const { name, store } = opts;
1050
+ function ensureComponent(componentDir, name, force) {
1051
+ if (fs6.existsSync(componentDir) && !force) {
1052
+ const entries = fs6.readdirSync(componentDir);
1053
+ if (entries.length > 0) {
1054
+ throw new Error(
1055
+ `Refusing to overwrite existing components/${name}/. Pass --force to override.`
1056
+ );
1057
+ }
1058
+ }
1059
+ fs6.mkdirSync(componentDir, { recursive: true });
1060
+ for (const [relPath, content] of componentFiles(name)) {
1061
+ fs6.writeFileSync(path6.join(componentDir, relPath), content);
1062
+ }
1063
+ }
1064
+ function rootFiles(opts) {
719
1065
  return [
720
- ["manifest.json", manifest(name)],
721
- ["component.tsx", component(name)],
722
- ["vite.config.ts", viteConfig()],
723
- ["tsconfig.json", tsconfig()],
724
- ["package.json", packageJson(name)],
725
- [".gsrc", gsrc(store)],
1066
+ ["package.json", rootPackageJson()],
1067
+ ["build.mjs", buildScript()],
1068
+ ["vite.config.ts", viteEditorConfig()],
1069
+ ["tsconfig.json", rootTsconfig()],
1070
+ [".gsrc", gsrc(opts.store)],
726
1071
  [".gitignore", gitignore()],
727
- ["README.md", readme(name)]
1072
+ ["README.md", rootReadme()]
1073
+ ];
1074
+ }
1075
+ function componentFiles(name) {
1076
+ return [
1077
+ ["manifest.json", manifest(name)],
1078
+ ["component.tsx", component(name)]
728
1079
  ];
729
1080
  }
730
1081
  function manifest(name) {
@@ -765,51 +1116,86 @@ export default function ${pascal(name)}(_props: Props): React.ReactElement {
765
1116
  }
766
1117
  `;
767
1118
  }
768
- function viteConfig() {
769
- return `import { defineConfig } from "vite";
1119
+ function buildScript() {
1120
+ return `import { build } from "vite";
770
1121
  import react from "@vitejs/plugin-react";
1122
+ import * as fs from "node:fs";
1123
+ import * as path from "node:path";
771
1124
 
772
- // Builds a single ESM bundle suitable for \`gs push\`.
773
- //
774
- // React / react-dom / react/jsx-runtime are EXTERNALIZED and rewritten
775
- // at build time to point at GreatStore's runtime shim at
776
- // \`/assets/remote-components/_runtime.js\`. The shim re-exports the host
777
- // chat tree's React instance off \`window.__GS_REMOTE_RUNTIME__\`, so
778
- // the component and the host share one React. Bundling React in
779
- // instead would crash on the first hook call because the bundled-in
780
- // React's dispatcher is null when the chat tree drives the render.
781
- //
782
- // Do NOT remove the \`paths\` mapping \u2014 without it the browser tries to
783
- // resolve a bare \`react\` specifier and fails before the component
784
- // renders.
785
- export default defineConfig({
786
- plugins: [react()],
787
- build: {
788
- lib: {
789
- entry: "component.tsx",
790
- formats: ["es"],
791
- fileName: () => "bundle.js",
792
- },
793
- outDir: ".",
794
- emptyOutDir: false,
795
- rollupOptions: {
796
- external: ["react", "react-dom", "react/jsx-runtime"],
797
- output: {
798
- entryFileNames: "bundle.js",
799
- paths: {
800
- react: "/assets/remote-components/_runtime.js",
801
- "react-dom": "/assets/remote-components/_runtime.js",
802
- "react/jsx-runtime": "/assets/remote-components/_runtime.js",
1125
+ const COMPONENTS_DIR = path.resolve("components");
1126
+
1127
+ function listComponents() {
1128
+ if (!fs.existsSync(COMPONENTS_DIR)) return [];
1129
+ return fs
1130
+ .readdirSync(COMPONENTS_DIR)
1131
+ .filter((entry) => {
1132
+ const dir = path.join(COMPONENTS_DIR, entry);
1133
+ return (
1134
+ fs.statSync(dir).isDirectory() &&
1135
+ fs.existsSync(path.join(dir, "component.tsx"))
1136
+ );
1137
+ })
1138
+ .sort();
1139
+ }
1140
+
1141
+ const components = listComponents();
1142
+ if (components.length === 0) {
1143
+ console.log("(no components in ./components \u2014 run \`gs init <name>\` to add one)");
1144
+ process.exit(0);
1145
+ }
1146
+
1147
+ const target = process.argv[2];
1148
+ const queue = target ? components.filter((c) => c === target) : components;
1149
+ if (target && queue.length === 0) {
1150
+ console.error(\`No component named "\${target}" in ./components\`);
1151
+ process.exit(1);
1152
+ }
1153
+
1154
+ for (const name of queue) {
1155
+ const dir = path.join(COMPONENTS_DIR, name);
1156
+ await build({
1157
+ plugins: [react()],
1158
+ logLevel: "warn",
1159
+ build: {
1160
+ lib: {
1161
+ entry: path.join(dir, "component.tsx"),
1162
+ formats: ["es"],
1163
+ fileName: () => "bundle.js",
1164
+ },
1165
+ outDir: dir,
1166
+ emptyOutDir: false,
1167
+ rollupOptions: {
1168
+ external: ["react", "react-dom", "react/jsx-runtime"],
1169
+ output: {
1170
+ entryFileNames: "bundle.js",
1171
+ paths: {
1172
+ react: "/assets/remote-components/_runtime.js",
1173
+ "react-dom": "/assets/remote-components/_runtime.js",
1174
+ "react/jsx-runtime": "/assets/remote-components/_runtime.js",
1175
+ },
803
1176
  },
804
1177
  },
1178
+ minify: true,
1179
+ sourcemap: false,
805
1180
  },
806
- minify: true,
807
- sourcemap: false,
808
- },
1181
+ });
1182
+ console.log(\`built components/\${name}/bundle.js\`);
1183
+ }
1184
+ `;
1185
+ }
1186
+ function viteEditorConfig() {
1187
+ return `import { defineConfig } from "vite";
1188
+ import react from "@vitejs/plugin-react";
1189
+
1190
+ // Real builds happen in build.mjs (one Vite invocation per component).
1191
+ // This file exists only so editors / language servers can resolve the
1192
+ // React plugin when inspecting components/*/component.tsx.
1193
+ export default defineConfig({
1194
+ plugins: [react()],
809
1195
  });
810
1196
  `;
811
1197
  }
812
- function tsconfig() {
1198
+ function rootTsconfig() {
813
1199
  return JSON.stringify(
814
1200
  {
815
1201
  compilerOptions: {
@@ -824,22 +1210,22 @@ function tsconfig() {
824
1210
  isolatedModules: true,
825
1211
  noEmit: true
826
1212
  },
827
- include: ["component.tsx", "vite.config.ts"]
1213
+ include: ["components/**/component.tsx", "vite.config.ts", "build.mjs"]
828
1214
  },
829
1215
  null,
830
1216
  2
831
1217
  ) + "\n";
832
1218
  }
833
- function packageJson(name) {
1219
+ function rootPackageJson() {
834
1220
  return JSON.stringify(
835
1221
  {
836
- name,
1222
+ name: "greatstore-components",
837
1223
  version: "0.0.1",
838
1224
  private: true,
839
1225
  type: "module",
840
1226
  scripts: {
841
- build: "vite build",
842
- push: "vite build && gs push"
1227
+ build: "node build.mjs",
1228
+ push: "node build.mjs && gs push"
843
1229
  },
844
1230
  dependencies: {
845
1231
  react: "^19.0.0",
@@ -861,22 +1247,34 @@ function gsrc(store) {
861
1247
  return JSON.stringify({ store: store ?? "<your-store-slug>" }, null, 2) + "\n";
862
1248
  }
863
1249
  function gitignore() {
864
- return ["node_modules/", "bundle.js", "*.tsbuildinfo", ".DS_Store", ""].join("\n");
1250
+ return [
1251
+ "node_modules/",
1252
+ "components/*/bundle.js",
1253
+ "components/*/.gssync.json",
1254
+ "*.tsbuildinfo",
1255
+ ".DS_Store",
1256
+ ""
1257
+ ].join("\n");
865
1258
  }
866
- function readme(name) {
867
- return `# ${name}
1259
+ function rootReadme() {
1260
+ return `# GreatStore components
868
1261
 
869
- A GreatStore custom component scaffolded with \`gs init\`.
1262
+ Custom React components published to your GreatStore tenant. Each
1263
+ component lives in its own folder under \`components/\`.
870
1264
 
871
1265
  \`\`\`
872
1266
  npm install
873
- npm run build # produces bundle.js
874
- gs push # uploads as a draft revision
875
- gs publish ${name}
1267
+ gs init <component_name> # add a new component
1268
+ npm run build # builds every component/<name>/bundle.js
1269
+ gs push # uploads every changed component as a draft
1270
+ gs publish <component_name> # promote a specific component to live
876
1271
  \`\`\`
877
1272
 
878
- Edit \`component.tsx\` for the UI and \`manifest.json\` for the metadata
879
- that the LLM sees (especially \`description\` and \`inputSchema\`).
1273
+ - \`gs push\` (no args) hashes each component and only uploads the ones
1274
+ that have changed since the last sync.
1275
+ - \`gs pull\` (no args) refreshes every remote component into
1276
+ \`components/<name>/\`. Components with unsaved local edits are skipped
1277
+ with a warning; pass \`--force\` to overwrite.
880
1278
  `;
881
1279
  }
882
1280
  function pascal(name) {
@@ -884,7 +1282,7 @@ function pascal(name) {
884
1282
  }
885
1283
 
886
1284
  // src/index.ts
887
- var VERSION = true ? "0.0.9" : "0.0.0-dev";
1285
+ var VERSION = true ? "0.0.11-beta.0" : "0.0.0-dev";
888
1286
  var HELP = `gs \u2014 GreatStore CLI (v${VERSION})
889
1287
 
890
1288
  Usage:
@@ -894,28 +1292,27 @@ Commands:
894
1292
  login Open browser, capture nav token, persist credentials.
895
1293
  logout Wipe ~/.greatstore/credentials.json.
896
1294
  whoami Print the identity stored locally.
897
- init <name> Scaffold a component project.
1295
+ init [<name>] Scaffold project root; add \`components/<name>/\` if name given.
898
1296
  list List components in the current store.
899
- pull <name> Download manifest.json + bundle.js.
900
- push [<name>] Upload manifest.json + bundle.js as a new draft.
1297
+ pull [<name>|*] Download remote component(s) into \`components/<name>/\`. No args = all.
1298
+ push [<name>] Upload changed components from \`components/\`. No args = all (skips unchanged).
901
1299
  publish <name> Promote the draft (or --version N) to live.
902
1300
  unpublish <name> Clear the live pointer.
903
1301
  delete <name> Soft-delete the component.
904
1302
 
905
1303
  Common flags:
906
- --store <slug> Target store (else .gsrc or GS_STORE).
1304
+ --store <slug> \`init\` only \u2014 writes the slug into .gsrc. Other commands read .gsrc.
907
1305
  --json Machine-readable output for \`list\`.
908
1306
  --draft | --live | --version N Revision selector for \`pull\`.
909
1307
  -o, --out <dir> Output directory for \`pull\` / \`init\`.
910
1308
  --manifest <path> Path to manifest for \`push\`.
911
1309
  --bundle <path> Path to bundle for \`push\`.
912
- --force Overwrite for \`init\`; skip confirmation for \`delete\`.
1310
+ --force Overwrite for \`init\` / \`pull\`; skip confirmation for \`delete\`.
913
1311
  --yes Skip confirmation for \`delete\`.
914
1312
  -h, --help Show this help.
915
1313
  -v, --version Print version.
916
1314
 
917
1315
  Environment:
918
- GS_STORE Fallback for --store.
919
1316
  GS_API_BASE Override API base (full URL, no trailing slash).
920
1317
  GS_NAV_BASE Override nav OAuth base.
921
1318
  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.9",
3
+ "version": "0.0.11-beta.0",
4
4
  "description": "CLI for authoring and shipping GreatStore custom components.",
5
5
  "license": "UNLICENSED",
6
6
  "type": "module",