@greatstore/cli 0.0.10 → 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.
- package/README.md +7 -0
- package/dist/cli.js +552 -157
- 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((
|
|
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((
|
|
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(() =>
|
|
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
|
|
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
|
-
|
|
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 =
|
|
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
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
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
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
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(
|
|
534
|
-
JSON.stringify(
|
|
558
|
+
path3.join(componentDir, SYNC_FILE),
|
|
559
|
+
JSON.stringify(state, null, 2) + "\n"
|
|
535
560
|
);
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
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);
|
|
541
627
|
}
|
|
542
|
-
const
|
|
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
|
-
`
|
|
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
|
|
558
|
-
import * as
|
|
722
|
+
import * as fs5 from "fs";
|
|
723
|
+
import * as path5 from "path";
|
|
559
724
|
async function pushCommand(args) {
|
|
560
|
-
const slug =
|
|
561
|
-
const
|
|
562
|
-
|
|
563
|
-
const
|
|
564
|
-
if (!
|
|
565
|
-
throw new Error(
|
|
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}` };
|
|
566
793
|
}
|
|
567
|
-
if (!
|
|
568
|
-
|
|
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
|
+
};
|
|
569
800
|
}
|
|
570
|
-
const manifestText =
|
|
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
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
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
|
-
|
|
584
|
-
|
|
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 =
|
|
588
|
-
const sourceText =
|
|
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(
|
|
591
|
-
|
|
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
|
-
|
|
845
|
+
"component.tsx"
|
|
597
846
|
);
|
|
598
847
|
}
|
|
599
848
|
const url = `${apiBaseFor(slug)}/api/builder/components/${encodeURIComponent(name)}`;
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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,116 @@ async function deleteCommand(args) {
|
|
|
662
966
|
`);
|
|
663
967
|
}
|
|
664
968
|
function prompt(question) {
|
|
665
|
-
return new Promise((
|
|
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
|
-
|
|
973
|
+
resolve4(answer);
|
|
670
974
|
});
|
|
671
975
|
});
|
|
672
976
|
}
|
|
673
977
|
|
|
674
978
|
// src/commands/init.ts
|
|
675
|
-
import * as
|
|
676
|
-
import * as
|
|
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
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
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;
|
|
696
1007
|
}
|
|
1008
|
+
const componentDir = path6.join(root, "components", name);
|
|
1009
|
+
ensureComponent(componentDir, name, force);
|
|
1010
|
+
const projectLabel = path6.relative(process.cwd(), root) || ".";
|
|
697
1011
|
process.stdout.write(
|
|
698
1012
|
[
|
|
699
|
-
`
|
|
1013
|
+
`Added component "${name}" at components/${name}/.`,
|
|
700
1014
|
"",
|
|
701
1015
|
"Next steps:",
|
|
702
|
-
` cd ${
|
|
703
|
-
|
|
1016
|
+
...rootExisted ? [] : [` cd ${projectLabel}`, " npm install"],
|
|
1017
|
+
` # edit components/${name}/component.tsx and manifest.json`,
|
|
704
1018
|
" npm run build",
|
|
705
1019
|
" gs push",
|
|
706
1020
|
""
|
|
707
1021
|
].join("\n")
|
|
708
1022
|
);
|
|
709
1023
|
}
|
|
710
|
-
function
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
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 });
|
|
718
1049
|
}
|
|
719
|
-
function
|
|
720
|
-
|
|
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) {
|
|
721
1065
|
return [
|
|
722
|
-
["
|
|
723
|
-
["
|
|
724
|
-
["vite.config.ts",
|
|
725
|
-
["tsconfig.json",
|
|
726
|
-
["
|
|
727
|
-
[".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)],
|
|
728
1071
|
[".gitignore", gitignore()],
|
|
729
|
-
["README.md",
|
|
1072
|
+
["README.md", rootReadme()]
|
|
1073
|
+
];
|
|
1074
|
+
}
|
|
1075
|
+
function componentFiles(name) {
|
|
1076
|
+
return [
|
|
1077
|
+
["manifest.json", manifest(name)],
|
|
1078
|
+
["component.tsx", component(name)]
|
|
730
1079
|
];
|
|
731
1080
|
}
|
|
732
1081
|
function manifest(name) {
|
|
@@ -767,51 +1116,86 @@ export default function ${pascal(name)}(_props: Props): React.ReactElement {
|
|
|
767
1116
|
}
|
|
768
1117
|
`;
|
|
769
1118
|
}
|
|
770
|
-
function
|
|
771
|
-
return `import {
|
|
1119
|
+
function buildScript() {
|
|
1120
|
+
return `import { build } from "vite";
|
|
772
1121
|
import react from "@vitejs/plugin-react";
|
|
1122
|
+
import * as fs from "node:fs";
|
|
1123
|
+
import * as path from "node:path";
|
|
773
1124
|
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
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
|
+
},
|
|
805
1176
|
},
|
|
806
1177
|
},
|
|
1178
|
+
minify: true,
|
|
1179
|
+
sourcemap: false,
|
|
807
1180
|
},
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
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()],
|
|
811
1195
|
});
|
|
812
1196
|
`;
|
|
813
1197
|
}
|
|
814
|
-
function
|
|
1198
|
+
function rootTsconfig() {
|
|
815
1199
|
return JSON.stringify(
|
|
816
1200
|
{
|
|
817
1201
|
compilerOptions: {
|
|
@@ -826,22 +1210,22 @@ function tsconfig() {
|
|
|
826
1210
|
isolatedModules: true,
|
|
827
1211
|
noEmit: true
|
|
828
1212
|
},
|
|
829
|
-
include: ["component.tsx", "vite.config.ts"]
|
|
1213
|
+
include: ["components/**/component.tsx", "vite.config.ts", "build.mjs"]
|
|
830
1214
|
},
|
|
831
1215
|
null,
|
|
832
1216
|
2
|
|
833
1217
|
) + "\n";
|
|
834
1218
|
}
|
|
835
|
-
function
|
|
1219
|
+
function rootPackageJson() {
|
|
836
1220
|
return JSON.stringify(
|
|
837
1221
|
{
|
|
838
|
-
name,
|
|
1222
|
+
name: "greatstore-components",
|
|
839
1223
|
version: "0.0.1",
|
|
840
1224
|
private: true,
|
|
841
1225
|
type: "module",
|
|
842
1226
|
scripts: {
|
|
843
|
-
build: "
|
|
844
|
-
push: "
|
|
1227
|
+
build: "node build.mjs",
|
|
1228
|
+
push: "node build.mjs && gs push"
|
|
845
1229
|
},
|
|
846
1230
|
dependencies: {
|
|
847
1231
|
react: "^19.0.0",
|
|
@@ -863,22 +1247,34 @@ function gsrc(store) {
|
|
|
863
1247
|
return JSON.stringify({ store: store ?? "<your-store-slug>" }, null, 2) + "\n";
|
|
864
1248
|
}
|
|
865
1249
|
function gitignore() {
|
|
866
|
-
return [
|
|
1250
|
+
return [
|
|
1251
|
+
"node_modules/",
|
|
1252
|
+
"components/*/bundle.js",
|
|
1253
|
+
"components/*/.gssync.json",
|
|
1254
|
+
"*.tsbuildinfo",
|
|
1255
|
+
".DS_Store",
|
|
1256
|
+
""
|
|
1257
|
+
].join("\n");
|
|
867
1258
|
}
|
|
868
|
-
function
|
|
869
|
-
return `#
|
|
1259
|
+
function rootReadme() {
|
|
1260
|
+
return `# GreatStore components
|
|
870
1261
|
|
|
871
|
-
|
|
1262
|
+
Custom React components published to your GreatStore tenant. Each
|
|
1263
|
+
component lives in its own folder under \`components/\`.
|
|
872
1264
|
|
|
873
1265
|
\`\`\`
|
|
874
1266
|
npm install
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
gs
|
|
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
|
|
878
1271
|
\`\`\`
|
|
879
1272
|
|
|
880
|
-
|
|
881
|
-
that
|
|
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.
|
|
882
1278
|
`;
|
|
883
1279
|
}
|
|
884
1280
|
function pascal(name) {
|
|
@@ -886,7 +1282,7 @@ function pascal(name) {
|
|
|
886
1282
|
}
|
|
887
1283
|
|
|
888
1284
|
// src/index.ts
|
|
889
|
-
var VERSION = true ? "0.0.
|
|
1285
|
+
var VERSION = true ? "0.0.11-beta.0" : "0.0.0-dev";
|
|
890
1286
|
var HELP = `gs \u2014 GreatStore CLI (v${VERSION})
|
|
891
1287
|
|
|
892
1288
|
Usage:
|
|
@@ -896,28 +1292,27 @@ Commands:
|
|
|
896
1292
|
login Open browser, capture nav token, persist credentials.
|
|
897
1293
|
logout Wipe ~/.greatstore/credentials.json.
|
|
898
1294
|
whoami Print the identity stored locally.
|
|
899
|
-
init <name>
|
|
1295
|
+
init [<name>] Scaffold project root; add \`components/<name>/\` if name given.
|
|
900
1296
|
list List components in the current store.
|
|
901
|
-
pull <name
|
|
902
|
-
push [<name>] Upload
|
|
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).
|
|
903
1299
|
publish <name> Promote the draft (or --version N) to live.
|
|
904
1300
|
unpublish <name> Clear the live pointer.
|
|
905
1301
|
delete <name> Soft-delete the component.
|
|
906
1302
|
|
|
907
1303
|
Common flags:
|
|
908
|
-
--store <slug>
|
|
1304
|
+
--store <slug> \`init\` only \u2014 writes the slug into .gsrc. Other commands read .gsrc.
|
|
909
1305
|
--json Machine-readable output for \`list\`.
|
|
910
1306
|
--draft | --live | --version N Revision selector for \`pull\`.
|
|
911
1307
|
-o, --out <dir> Output directory for \`pull\` / \`init\`.
|
|
912
1308
|
--manifest <path> Path to manifest for \`push\`.
|
|
913
1309
|
--bundle <path> Path to bundle for \`push\`.
|
|
914
|
-
--force Overwrite for \`init\`; skip confirmation for \`delete\`.
|
|
1310
|
+
--force Overwrite for \`init\` / \`pull\`; skip confirmation for \`delete\`.
|
|
915
1311
|
--yes Skip confirmation for \`delete\`.
|
|
916
1312
|
-h, --help Show this help.
|
|
917
1313
|
-v, --version Print version.
|
|
918
1314
|
|
|
919
1315
|
Environment:
|
|
920
|
-
GS_STORE Fallback for --store.
|
|
921
1316
|
GS_API_BASE Override API base (full URL, no trailing slash).
|
|
922
1317
|
GS_NAV_BASE Override nav OAuth base.
|
|
923
1318
|
GS_BROWSER_CMD Override the browser-open command.
|