create-egregore 0.18.17 → 0.18.18
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/lib/local.js +219 -140
- package/lib/ui.js +115 -11
- package/package.json +2 -2
- package/runtime/codex/bin/lib/identity.sh +10 -0
- package/runtime/codex/bin/telemetry.sh +54 -13
package/lib/local.js
CHANGED
|
@@ -653,105 +653,138 @@ async function localFounderFlow(ui, ctx = {}) {
|
|
|
653
653
|
// 2. Get user info
|
|
654
654
|
const user = await getUser(githubToken);
|
|
655
655
|
|
|
656
|
-
// 3.
|
|
657
|
-
//
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
s
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
656
|
+
// 3–6. Home → new/existing → naming → repo picker → repo names, as a
|
|
657
|
+
// step machine so esc walks BACKWARDS through the flow (field request:
|
|
658
|
+
// a wrong org pick shouldn't mean Ctrl-C and start over). Every answer
|
|
659
|
+
// lives in `st`, so a revisited step re-offers the previous choice.
|
|
660
|
+
// Creation (step 7 below) is the point of no return — no back from there.
|
|
661
|
+
const slugifyName = (s) => (s || "")
|
|
662
|
+
.toLowerCase()
|
|
663
|
+
.replace(/[^a-z0-9-]/g, "-")
|
|
664
|
+
.replace(/-+/g, "-")
|
|
665
|
+
.replace(/^-|-$/g, "");
|
|
666
|
+
const BACK = ui.BACK;
|
|
667
|
+
const SKIP = Symbol("step-skip"); // deterministic step — passes through in both directions
|
|
668
|
+
|
|
669
|
+
const st = {
|
|
670
|
+
orgs: null, // fetched once
|
|
671
|
+
githubOrg: null, isOrg: false,
|
|
672
|
+
isNewProject: null,
|
|
673
|
+
orgName: null, description: "", newProjectRepo: null,
|
|
674
|
+
selectedRepoNames: null, // raw picker selection (preserved across back)
|
|
675
|
+
selectedRepos: [], // enriched {name, base_branch}
|
|
676
|
+
repoName: null, memoryRepoName: null,
|
|
677
|
+
reposCache: null, // {org, repos, teamRepoCount}
|
|
678
|
+
};
|
|
679
|
+
|
|
680
|
+
const stepHome = async () => {
|
|
681
|
+
if (!st.orgs) {
|
|
682
|
+
ui.section("Pick a home", "where should this Egregore live?");
|
|
683
|
+
const s = ui.spinner("Checking your GitHub accounts...");
|
|
684
|
+
st.orgs = await listOrgs(githubToken);
|
|
685
|
+
s.stop("Accounts loaded");
|
|
686
|
+
}
|
|
687
|
+
if (st.orgs.length === 0) {
|
|
688
|
+
if (!st.githubOrg) {
|
|
689
|
+
st.githubOrg = user.login;
|
|
690
|
+
st.isOrg = false;
|
|
691
|
+
ui.info(`Using your personal account: ${ui.bold(st.githubOrg)}`);
|
|
692
|
+
}
|
|
693
|
+
return SKIP;
|
|
672
694
|
}
|
|
673
695
|
const orgChoice = await ui.choose(
|
|
674
696
|
"Where should Egregore live?",
|
|
675
|
-
|
|
697
|
+
[
|
|
698
|
+
{ label: user.login, description: "Personal account" },
|
|
699
|
+
...st.orgs.map((o) => ({ label: o.login, description: "Organization" })),
|
|
700
|
+
],
|
|
676
701
|
"Don't see your org? Authorize it for your account on GitHub, then re-run.",
|
|
702
|
+
{ escAction: "back" },
|
|
677
703
|
);
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
704
|
+
if (orgChoice === BACK) return BACK; // first step — driver clamps to a re-ask
|
|
705
|
+
st.githubOrg = orgChoice.label;
|
|
706
|
+
st.isOrg = st.githubOrg !== user.login;
|
|
707
|
+
return true;
|
|
708
|
+
};
|
|
681
709
|
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
710
|
+
const stepWorkType = async () => {
|
|
711
|
+
// In-repo detection already answered this — pass through both ways.
|
|
712
|
+
if (ctx.prefilledRepo) {
|
|
713
|
+
if (st.isNewProject === null) {
|
|
714
|
+
st.isNewProject = false;
|
|
715
|
+
ui.info(`${ui.green("→")} existing project — we'll manage ${ui.cyan(ctx.prefilledRepo.localName)} plus any others you pick.`);
|
|
716
|
+
}
|
|
717
|
+
return SKIP;
|
|
718
|
+
}
|
|
691
719
|
const action = await ui.choose("What are you working on?", [
|
|
692
720
|
{ label: "Starting something new", description: "Create a project from scratch" },
|
|
693
721
|
{ label: "Existing project", description: "Connect repos I already have" },
|
|
694
|
-
]);
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
ui.section("Name it", "what's this Egregore called?");
|
|
700
|
-
let orgName, description, selectedRepos = [], newProjectRepo = null;
|
|
701
|
-
|
|
702
|
-
if (isNewProject) {
|
|
703
|
-
orgName = await ui.prompt("name your Egregore (team or group name)", { default: githubOrg });
|
|
704
|
-
description = await ui.prompt("what are you building? (one line)");
|
|
705
|
-
|
|
706
|
-
// Derive the egregore + memory repo names now so we can validate the
|
|
707
|
-
// optional project repo name against them before the user commits to it.
|
|
708
|
-
// Matches the final derivation at line ~395 below.
|
|
709
|
-
const slugifyRepo = (s) => s.toLowerCase()
|
|
710
|
-
.replace(/[^a-z0-9-]/g, "-")
|
|
711
|
-
.replace(/-+/g, "-")
|
|
712
|
-
.replace(/^-|-$/g, "") || "egregore";
|
|
713
|
-
const predictedRepoName = slugifyRepo(orgName);
|
|
714
|
-
const predictedMemoryRepoName = `${predictedRepoName}-memory`;
|
|
715
|
-
|
|
716
|
-
// Ask about project repo
|
|
717
|
-
ui.note([
|
|
718
|
-
"Your Egregore gets its own repo (memory + config) in your account.",
|
|
719
|
-
"Have a code project too? Egregore can add a repo for it in your account.",
|
|
720
|
-
]);
|
|
722
|
+
], undefined, { escAction: "back" });
|
|
723
|
+
if (action === BACK) return BACK;
|
|
724
|
+
st.isNewProject = action.label.startsWith("Starting");
|
|
725
|
+
return true;
|
|
726
|
+
};
|
|
721
727
|
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
728
|
+
const stepNameProject = async () => {
|
|
729
|
+
ui.section("Name it", "what's this Egregore called?");
|
|
730
|
+
if (st.isNewProject) {
|
|
731
|
+
const name = await ui.promptOrBack("name your Egregore (team or group name)", { default: st.orgName || st.githubOrg });
|
|
732
|
+
if (name === BACK) return BACK;
|
|
733
|
+
st.orgName = name;
|
|
734
|
+
const desc = await ui.promptOrBack("what are you building? (one line)", { default: st.description || undefined });
|
|
735
|
+
if (desc === BACK) return BACK;
|
|
736
|
+
st.description = desc;
|
|
737
|
+
|
|
738
|
+
// Validate the optional project repo name against the names the
|
|
739
|
+
// Egregore/memory repos WILL get (editable later, derived for now).
|
|
740
|
+
const predictedRepoName = slugifyName(st.orgName) || "egregore";
|
|
741
|
+
const predictedMemoryRepoName = `${predictedRepoName}-memory`;
|
|
742
|
+
ui.note([
|
|
743
|
+
"Your Egregore gets its own repo (memory + config) in your account.",
|
|
744
|
+
"Have a code project too? Egregore can add a repo for it in your account.",
|
|
745
|
+
]);
|
|
746
|
+
for (;;) {
|
|
747
|
+
const projectName = await ui.promptOrBack("code project repo name (Enter to skip)", { default: st.newProjectRepo || undefined });
|
|
748
|
+
if (projectName === BACK) return BACK;
|
|
749
|
+
if (!projectName) { st.newProjectRepo = null; break; }
|
|
750
|
+
const slug = slugifyName(projectName);
|
|
751
|
+
if (!slug) {
|
|
752
|
+
ui.warn("That doesn't produce a valid repo name. Try letters, numbers, and dashes.");
|
|
753
|
+
continue;
|
|
754
|
+
}
|
|
755
|
+
if (slug === predictedRepoName) {
|
|
756
|
+
ui.warn(`"${slug}" is already the name of your Egregore repo. Pick a different name.`);
|
|
757
|
+
ui.info(` ${ui.dim("suggestion:")} ${ui.cyan(`${slug}-app`)}`);
|
|
758
|
+
continue;
|
|
759
|
+
}
|
|
760
|
+
if (slug === predictedMemoryRepoName) {
|
|
761
|
+
ui.warn(`"${slug}" collides with your memory repo. Pick a different name.`);
|
|
762
|
+
continue;
|
|
763
|
+
}
|
|
764
|
+
st.newProjectRepo = slug;
|
|
726
765
|
break;
|
|
727
766
|
}
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
ui.warn(`"${slug}" is already the name of your Egregore repo. Pick a different name.`);
|
|
735
|
-
ui.info(` ${ui.dim("suggestion:")} ${ui.cyan(`${slug}-app`)}`);
|
|
736
|
-
continue;
|
|
737
|
-
}
|
|
738
|
-
if (slug === predictedMemoryRepoName) {
|
|
739
|
-
ui.warn(`"${slug}" collides with your memory repo. Pick a different name.`);
|
|
740
|
-
continue;
|
|
741
|
-
}
|
|
742
|
-
newProjectRepo = slug;
|
|
743
|
-
break;
|
|
767
|
+
} else {
|
|
768
|
+
const name = await ui.promptOrBack("what should we call this Egregore?", { default: st.orgName || st.githubOrg });
|
|
769
|
+
if (name === BACK) return BACK;
|
|
770
|
+
st.orgName = name;
|
|
771
|
+
st.description = "";
|
|
772
|
+
st.newProjectRepo = null;
|
|
744
773
|
}
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
orgName = await ui.prompt("what should we call this Egregore?", { default: githubOrg });
|
|
748
|
-
description = "";
|
|
774
|
+
return true;
|
|
775
|
+
};
|
|
749
776
|
|
|
777
|
+
const stepRepos = async () => {
|
|
778
|
+
if (st.isNewProject) {
|
|
779
|
+
st.selectedRepoNames = null;
|
|
780
|
+
st.selectedRepos = [];
|
|
781
|
+
return SKIP;
|
|
782
|
+
}
|
|
750
783
|
// Auto-adopt the user's current repo if it's under the chosen GitHub owner.
|
|
751
784
|
const autoAdopt =
|
|
752
785
|
ctx.prefilledRepo &&
|
|
753
786
|
ctx.prefilledRepo.owner &&
|
|
754
|
-
ctx.prefilledRepo.owner.toLowerCase() === githubOrg.toLowerCase()
|
|
787
|
+
ctx.prefilledRepo.owner.toLowerCase() === st.githubOrg.toLowerCase()
|
|
755
788
|
? ctx.prefilledRepo.name
|
|
756
789
|
: null;
|
|
757
790
|
|
|
@@ -759,47 +792,58 @@ async function localFounderFlow(ui, ctx = {}) {
|
|
|
759
792
|
// 500 most recently updated repos, which buries a team's repos in a big
|
|
760
793
|
// org — so the user's team repos are loaded too and boosted to the top,
|
|
761
794
|
// and typing in the picker live-searches the WHOLE org (see
|
|
762
|
-
// searchOrgRepos), so no repo is unreachable.
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
795
|
+
// searchOrgRepos), so no repo is unreachable. The load is cached per
|
|
796
|
+
// owner so back-navigation doesn't refetch.
|
|
797
|
+
if (!st.reposCache || st.reposCache.org !== st.githubOrg) {
|
|
798
|
+
const s0 = ui.spinner("Loading repos...");
|
|
799
|
+
const repos = [];
|
|
800
|
+
let teamRepoCount = 0;
|
|
801
|
+
try {
|
|
802
|
+
const [orgRepos, teams] = await Promise.all([
|
|
803
|
+
listOrgRepos(githubToken, st.githubOrg, st.isOrg),
|
|
804
|
+
st.isOrg ? listUserTeams(githubToken, st.githubOrg) : Promise.resolve([]),
|
|
805
|
+
]);
|
|
806
|
+
const teamRepoLists = await Promise.all(
|
|
807
|
+
teams.map((t) => listTeamRepos(githubToken, st.githubOrg, t.slug).catch(() => [])),
|
|
808
|
+
);
|
|
809
|
+
const seen = new Set();
|
|
810
|
+
for (const list of teamRepoLists) {
|
|
811
|
+
for (const r of list) {
|
|
812
|
+
if (seen.has(r.name)) continue;
|
|
813
|
+
seen.add(r.name);
|
|
814
|
+
repos.push(r);
|
|
815
|
+
}
|
|
816
|
+
}
|
|
817
|
+
teamRepoCount = repos.length;
|
|
818
|
+
for (const r of orgRepos) {
|
|
777
819
|
if (seen.has(r.name)) continue;
|
|
778
820
|
seen.add(r.name);
|
|
779
821
|
repos.push(r);
|
|
780
822
|
}
|
|
823
|
+
s0.stop(teamRepoCount > 0 ? `Loaded repos — your teams' ${teamRepoCount} first` : "Loaded repos");
|
|
824
|
+
} catch {
|
|
825
|
+
s0.stop("Could not load repos");
|
|
781
826
|
}
|
|
782
|
-
|
|
783
|
-
for (const r of orgRepos) {
|
|
784
|
-
if (seen.has(r.name)) continue;
|
|
785
|
-
seen.add(r.name);
|
|
786
|
-
repos.push(r);
|
|
787
|
-
}
|
|
788
|
-
s0.stop(teamRepoCount > 0 ? `Loaded repos — your teams' ${teamRepoCount} first` : "Loaded repos");
|
|
789
|
-
} catch {
|
|
790
|
-
s0.stop("Could not load repos");
|
|
827
|
+
st.reposCache = { org: st.githubOrg, repos, teamRepoCount };
|
|
791
828
|
}
|
|
829
|
+
const repos = st.reposCache.repos;
|
|
792
830
|
|
|
793
831
|
const filtered = autoAdopt ? repos.filter((r) => r.name !== autoAdopt) : repos;
|
|
794
832
|
if (autoAdopt) {
|
|
795
833
|
console.log("");
|
|
796
834
|
ui.info(`${ui.cyan(autoAdopt)} will be managed automatically (your current repo)`);
|
|
797
835
|
}
|
|
798
|
-
|
|
836
|
+
const picked = await ui.multiSelect(
|
|
799
837
|
"Which other repos should Egregore manage?",
|
|
800
838
|
filtered,
|
|
801
|
-
{
|
|
839
|
+
{
|
|
840
|
+
remoteSearch: (term) => searchOrgRepos(githubToken, st.githubOrg, st.isOrg, term),
|
|
841
|
+
preselected: st.selectedRepoNames || [],
|
|
842
|
+
escAction: "back",
|
|
843
|
+
},
|
|
802
844
|
);
|
|
845
|
+
if (picked === BACK) return BACK;
|
|
846
|
+
let selectedRepos = picked;
|
|
803
847
|
|
|
804
848
|
// Non-interactive terminals get no live search — keep a typed-names
|
|
805
849
|
// escape hatch there, validated so a typo doesn't become a ghost repo.
|
|
@@ -808,11 +852,11 @@ async function localFounderFlow(ui, ctx = {}) {
|
|
|
808
852
|
for (const raw of (extraRaw || "").split(",")) {
|
|
809
853
|
const name = raw.trim();
|
|
810
854
|
if (!name || selectedRepos.includes(name)) continue;
|
|
811
|
-
if (await repoExists(githubToken, githubOrg, name)) {
|
|
855
|
+
if (await repoExists(githubToken, st.githubOrg, name)) {
|
|
812
856
|
selectedRepos.push(name);
|
|
813
857
|
ui.success(`Added ${name}`);
|
|
814
858
|
} else {
|
|
815
|
-
ui.warn(`No repo ${githubOrg}/${name} — skipping`);
|
|
859
|
+
ui.warn(`No repo ${st.githubOrg}/${name} — skipping`);
|
|
816
860
|
}
|
|
817
861
|
}
|
|
818
862
|
}
|
|
@@ -820,6 +864,7 @@ async function localFounderFlow(ui, ctx = {}) {
|
|
|
820
864
|
selectedRepos.unshift(autoAdopt);
|
|
821
865
|
}
|
|
822
866
|
if (autoAdopt) skipRepos.add(autoAdopt);
|
|
867
|
+
st.selectedRepoNames = [...selectedRepos];
|
|
823
868
|
|
|
824
869
|
// Detect default branch for each selected repo and convert to {name, base_branch} format
|
|
825
870
|
if (selectedRepos.length > 0) {
|
|
@@ -827,14 +872,13 @@ async function localFounderFlow(ui, ctx = {}) {
|
|
|
827
872
|
const reposWithBranch = [];
|
|
828
873
|
for (const repoName of selectedRepos) {
|
|
829
874
|
try {
|
|
830
|
-
const info = await detectRepoBranches(githubToken, githubOrg, repoName);
|
|
875
|
+
const info = await detectRepoBranches(githubToken, st.githubOrg, repoName);
|
|
831
876
|
reposWithBranch.push({ name: repoName, base_branch: info ? info.defaultBranch : "main" });
|
|
832
877
|
} catch {
|
|
833
878
|
reposWithBranch.push({ name: repoName, base_branch: "main" });
|
|
834
879
|
}
|
|
835
880
|
}
|
|
836
881
|
s0b.stop("Checked branches");
|
|
837
|
-
// Replace flat list with enriched objects
|
|
838
882
|
selectedRepos = reposWithBranch;
|
|
839
883
|
console.log("");
|
|
840
884
|
ui.info("Detected branch setup:");
|
|
@@ -843,42 +887,77 @@ async function localFounderFlow(ui, ctx = {}) {
|
|
|
843
887
|
}
|
|
844
888
|
console.log("");
|
|
845
889
|
}
|
|
890
|
+
st.selectedRepos = selectedRepos;
|
|
891
|
+
return true;
|
|
892
|
+
};
|
|
893
|
+
|
|
894
|
+
const stepRepoNames = async () => {
|
|
895
|
+
// Derived names shown as editable prompts — creating repos with silently
|
|
896
|
+
// derived names was a repeated field complaint.
|
|
897
|
+
const derivedSlug = slugifyName(st.orgName) || "egregore";
|
|
898
|
+
console.log("");
|
|
899
|
+
ui.note([
|
|
900
|
+
`Two repos will be created in ${ui.bold(st.githubOrg)}: the Egregore repo`,
|
|
901
|
+
"(config + skills) and its shared-memory repo. Enter keeps the defaults.",
|
|
902
|
+
]);
|
|
903
|
+
for (;;) {
|
|
904
|
+
const answer = await ui.promptOrBack("Egregore repo name", { default: st.repoName || derivedSlug });
|
|
905
|
+
if (answer === BACK) return BACK;
|
|
906
|
+
const candidate = slugifyName(answer);
|
|
907
|
+
if (!candidate) { ui.warn("That doesn't produce a valid repo name. Try letters, numbers, and dashes."); continue; }
|
|
908
|
+
if (st.newProjectRepo && candidate === st.newProjectRepo) { ui.warn(`"${candidate}" is already your project repo. Pick a different name.`); continue; }
|
|
909
|
+
st.repoName = candidate;
|
|
910
|
+
break;
|
|
911
|
+
}
|
|
912
|
+
for (;;) {
|
|
913
|
+
const answer = await ui.promptOrBack("memory repo name", { default: st.memoryRepoName || `${st.repoName}-memory` });
|
|
914
|
+
if (answer === BACK) return BACK;
|
|
915
|
+
const candidate = slugifyName(answer);
|
|
916
|
+
if (!candidate) { ui.warn("That doesn't produce a valid repo name. Try letters, numbers, and dashes."); continue; }
|
|
917
|
+
if (candidate === st.repoName) { ui.warn("Memory repo can't share the Egregore repo's name."); continue; }
|
|
918
|
+
if (st.newProjectRepo && candidate === st.newProjectRepo) { ui.warn(`"${candidate}" is already your project repo. Pick a different name.`); continue; }
|
|
919
|
+
st.memoryRepoName = candidate;
|
|
920
|
+
break;
|
|
921
|
+
}
|
|
922
|
+
return true;
|
|
923
|
+
};
|
|
924
|
+
|
|
925
|
+
// Direction-aware driver: BACK rewinds one step; SKIP (deterministic
|
|
926
|
+
// steps) passes through in whichever direction we're moving, so backing
|
|
927
|
+
// over an auto-answered step doesn't bounce the user forward again.
|
|
928
|
+
const steps = [stepHome, stepWorkType, stepNameProject, stepRepos, stepRepoNames];
|
|
929
|
+
let stepIdx = 0;
|
|
930
|
+
let stepDir = 1;
|
|
931
|
+
while (stepIdx < steps.length) {
|
|
932
|
+
const result = await steps[stepIdx]();
|
|
933
|
+
if (result === BACK) {
|
|
934
|
+
stepDir = -1;
|
|
935
|
+
stepIdx = Math.max(0, stepIdx - 1);
|
|
936
|
+
continue;
|
|
937
|
+
}
|
|
938
|
+
if (result === SKIP) {
|
|
939
|
+
stepIdx += stepDir;
|
|
940
|
+
if (stepIdx < 0) { stepIdx = 0; stepDir = 1; }
|
|
941
|
+
continue;
|
|
942
|
+
}
|
|
943
|
+
stepDir = 1;
|
|
944
|
+
stepIdx++;
|
|
846
945
|
}
|
|
847
946
|
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
const
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
947
|
+
const githubOrg = st.githubOrg;
|
|
948
|
+
const isOrg = st.isOrg;
|
|
949
|
+
const isNewProject = st.isNewProject;
|
|
950
|
+
const orgName = st.orgName;
|
|
951
|
+
const description = st.description;
|
|
952
|
+
let newProjectRepo = st.newProjectRepo;
|
|
953
|
+
let selectedRepos = st.selectedRepos;
|
|
954
|
+
const repoName = st.repoName;
|
|
955
|
+
const memoryRepoName = st.memoryRepoName;
|
|
856
956
|
const nameSlug = slugifyName(orgName) || "egregore";
|
|
857
957
|
// Org slug prefixed with GitHub owner — globally unique across all egregores
|
|
858
958
|
const ghPrefix = slugifyName(githubOrg);
|
|
859
959
|
const slug = ghPrefix === nameSlug ? nameSlug : `${ghPrefix}-${nameSlug}`;
|
|
860
960
|
|
|
861
|
-
console.log("");
|
|
862
|
-
ui.note([
|
|
863
|
-
`Two repos will be created in ${ui.bold(githubOrg)}: the Egregore repo`,
|
|
864
|
-
"(config + skills) and its shared-memory repo. Enter keeps the defaults.",
|
|
865
|
-
]);
|
|
866
|
-
let repoName;
|
|
867
|
-
for (;;) {
|
|
868
|
-
repoName = slugifyName(await ui.prompt("Egregore repo name", { default: nameSlug }));
|
|
869
|
-
if (!repoName) { ui.warn("That doesn't produce a valid repo name. Try letters, numbers, and dashes."); continue; }
|
|
870
|
-
if (newProjectRepo && repoName === newProjectRepo) { ui.warn(`"${repoName}" is already your project repo. Pick a different name.`); continue; }
|
|
871
|
-
break;
|
|
872
|
-
}
|
|
873
|
-
let memoryRepoName;
|
|
874
|
-
for (;;) {
|
|
875
|
-
memoryRepoName = slugifyName(await ui.prompt("memory repo name", { default: `${repoName}-memory` }));
|
|
876
|
-
if (!memoryRepoName) { ui.warn("That doesn't produce a valid repo name. Try letters, numbers, and dashes."); continue; }
|
|
877
|
-
if (memoryRepoName === repoName) { ui.warn("Memory repo can't share the Egregore repo's name."); continue; }
|
|
878
|
-
if (newProjectRepo && memoryRepoName === newProjectRepo) { ui.warn(`"${memoryRepoName}" is already your project repo. Pick a different name.`); continue; }
|
|
879
|
-
break;
|
|
880
|
-
}
|
|
881
|
-
|
|
882
961
|
// Guard: if egregore repo already exists with a config, don't overwrite
|
|
883
962
|
console.log("");
|
|
884
963
|
const s1 = ui.spinner("Checking for existing Egregore...");
|
package/lib/ui.js
CHANGED
|
@@ -226,6 +226,60 @@ function prompt(question, { default: defaultValue } = {}) {
|
|
|
226
226
|
});
|
|
227
227
|
}
|
|
228
228
|
|
|
229
|
+
// Line input that can step backwards: esc resolves BACK, enter resolves the
|
|
230
|
+
// typed value (or the default). Raw-mode single-line editor — append +
|
|
231
|
+
// backspace only, which covers the short names these prompts collect.
|
|
232
|
+
// Falls back to the plain readline prompt when not interactive.
|
|
233
|
+
function promptOrBack(question, { default: defaultValue } = {}) {
|
|
234
|
+
if (!canInteract()) return prompt(question, { default: defaultValue });
|
|
235
|
+
return new Promise((resolve) => {
|
|
236
|
+
let buf = "";
|
|
237
|
+
const label = defaultValue
|
|
238
|
+
? ` ${CYAN}?${RESET} ${question} ${DIM}[${defaultValue}]${RESET} ${DIM}(esc back)${RESET} `
|
|
239
|
+
: ` ${CYAN}?${RESET} ${question} ${DIM}(esc back)${RESET} `;
|
|
240
|
+
const render = () => process.stdout.write(`\r\x1b[2K${label}${buf}`);
|
|
241
|
+
const cleanup = () => {
|
|
242
|
+
process.stdin.setRawMode(false);
|
|
243
|
+
process.stdin.pause();
|
|
244
|
+
process.stdin.removeListener("keypress", onKey);
|
|
245
|
+
};
|
|
246
|
+
const onKey = (_str, key) => {
|
|
247
|
+
if (!key) return;
|
|
248
|
+
if (key.ctrl && key.name === "c") {
|
|
249
|
+
cleanup();
|
|
250
|
+
process.stdout.write(`\n ${DIM}(cancelled)${RESET}\n`);
|
|
251
|
+
process.exit(130);
|
|
252
|
+
}
|
|
253
|
+
if (key.name === "escape") {
|
|
254
|
+
cleanup();
|
|
255
|
+
process.stdout.write(`\r\x1b[2K${label}${DIM}← back${RESET}\n`);
|
|
256
|
+
resolve(BACK);
|
|
257
|
+
return;
|
|
258
|
+
}
|
|
259
|
+
if (key.name === "return") {
|
|
260
|
+
cleanup();
|
|
261
|
+
process.stdout.write("\n");
|
|
262
|
+
resolve(buf.trim() || (defaultValue ?? ""));
|
|
263
|
+
return;
|
|
264
|
+
}
|
|
265
|
+
if (key.name === "backspace") {
|
|
266
|
+
buf = buf.slice(0, -1);
|
|
267
|
+
render();
|
|
268
|
+
return;
|
|
269
|
+
}
|
|
270
|
+
if (_str && _str.length === 1 && !key.ctrl && !key.meta && _str.charCodeAt(0) >= 32) {
|
|
271
|
+
buf += _str;
|
|
272
|
+
render();
|
|
273
|
+
}
|
|
274
|
+
};
|
|
275
|
+
readline.emitKeypressEvents(process.stdin);
|
|
276
|
+
process.stdin.setRawMode(true);
|
|
277
|
+
process.stdin.resume();
|
|
278
|
+
process.stdin.on("keypress", onKey);
|
|
279
|
+
render();
|
|
280
|
+
});
|
|
281
|
+
}
|
|
282
|
+
|
|
229
283
|
async function confirm(question, defaultYes = false) {
|
|
230
284
|
const hint = defaultYes ? `${DIM}[Y/n]${RESET}` : `${DIM}[y/N]${RESET}`;
|
|
231
285
|
const answer = await prompt(`${question} ${hint}`);
|
|
@@ -320,24 +374,33 @@ function filterOptions(options, filterText, getText) {
|
|
|
320
374
|
return result;
|
|
321
375
|
}
|
|
322
376
|
|
|
377
|
+
// ── back navigation ─────────────────────────────────────────────────
|
|
378
|
+
//
|
|
379
|
+
// Multi-step flows (the founder setup) let the user step backwards with
|
|
380
|
+
// esc. Pickers and prompts opt in via `escAction: "back"` — esc then
|
|
381
|
+
// resolves to the BACK sentinel instead of aborting the process, and the
|
|
382
|
+
// caller's step machine rewinds. Ctrl-C always remains the hard abort.
|
|
383
|
+
const BACK = Symbol.for("egregore.ui.back");
|
|
384
|
+
|
|
323
385
|
// ── choose: interactive single-select picker ────────────────────────
|
|
324
386
|
//
|
|
325
387
|
// options: [{ label, description }]
|
|
326
|
-
// returns: the picked option (same object reference as in options[])
|
|
388
|
+
// returns: the picked option (same object reference as in options[]),
|
|
389
|
+
// or BACK when opts.escAction === "back" and the user pressed esc
|
|
327
390
|
// keys:
|
|
328
391
|
// ↑ ↓ navigate
|
|
329
392
|
// enter select
|
|
330
393
|
// type filter (case-insensitive substring match on label + desc)
|
|
331
394
|
// backspace pop filter
|
|
332
|
-
// esc cancel
|
|
395
|
+
// esc cancel — or go back with opts.escAction: "back"
|
|
333
396
|
// ctrl-c abort process
|
|
334
397
|
|
|
335
|
-
async function choose(question, options, hint) {
|
|
336
|
-
if (!canInteract()) return legacyChoose(question, options, hint);
|
|
337
|
-
return interactiveChoose(question, options, hint);
|
|
398
|
+
async function choose(question, options, hint, opts = {}) {
|
|
399
|
+
if (!canInteract()) return legacyChoose(question, options, hint, opts);
|
|
400
|
+
return interactiveChoose(question, options, hint, opts);
|
|
338
401
|
}
|
|
339
402
|
|
|
340
|
-
async function interactiveChoose(question, options, hint) {
|
|
403
|
+
async function interactiveChoose(question, options, hint, opts = {}) {
|
|
341
404
|
return new Promise((resolve) => {
|
|
342
405
|
let filterText = "";
|
|
343
406
|
let visible = filterOptions(options, filterText, (o) => `${o.label} ${o.description || ""}`);
|
|
@@ -394,12 +457,13 @@ async function interactiveChoose(question, options, hint) {
|
|
|
394
457
|
for (let i = start; i < end; i++) lines.push(renderRow(visible[i], i));
|
|
395
458
|
if (end < total) lines.push(` ${DIM}↓ ${total - end} more · type to filter${RESET}`);
|
|
396
459
|
}
|
|
460
|
+
const escHint = opts.escAction === "back" ? `${DIM}esc${RESET} back` : `${DIM}esc${RESET} cancel`;
|
|
397
461
|
if (!first) clearLines(lastLineCount);
|
|
398
462
|
lastLineCount = renderPicker({
|
|
399
463
|
question,
|
|
400
464
|
filterText,
|
|
401
465
|
lines,
|
|
402
|
-
help: hint || `${DIM}↑↓${RESET} navigate · ${DIM}enter${RESET} select · type to filter · ${
|
|
466
|
+
help: hint || `${DIM}↑↓${RESET} navigate · ${DIM}enter${RESET} select · type to filter · ${escHint}`,
|
|
403
467
|
});
|
|
404
468
|
};
|
|
405
469
|
|
|
@@ -416,8 +480,28 @@ async function interactiveChoose(question, options, hint) {
|
|
|
416
480
|
resolve(value);
|
|
417
481
|
};
|
|
418
482
|
|
|
483
|
+
const goBack = () => {
|
|
484
|
+
cleanup();
|
|
485
|
+
clearLines(lastLineCount);
|
|
486
|
+
process.stdout.write(` ${CYAN}◆${RESET} ${BOLD}${question}${RESET} ${DIM}→ ← back${RESET}\n`);
|
|
487
|
+
resolve(BACK);
|
|
488
|
+
};
|
|
489
|
+
|
|
419
490
|
const onKey = (_str, key) => {
|
|
420
491
|
if (!key) return;
|
|
492
|
+
if (key.name === "escape" && filterText) {
|
|
493
|
+
// fzf convention: first esc clears an active filter; only a clean
|
|
494
|
+
// state goes back / cancels.
|
|
495
|
+
filterText = "";
|
|
496
|
+
visible = filterOptions(options, filterText, (o) => `${o.label} ${o.description || ""}`);
|
|
497
|
+
cursor = firstEnabled();
|
|
498
|
+
render();
|
|
499
|
+
return;
|
|
500
|
+
}
|
|
501
|
+
if (key.name === "escape" && opts.escAction === "back") {
|
|
502
|
+
goBack();
|
|
503
|
+
return;
|
|
504
|
+
}
|
|
421
505
|
if ((key.ctrl && key.name === "c") || key.name === "escape") {
|
|
422
506
|
cleanup();
|
|
423
507
|
unparkCursor();
|
|
@@ -462,7 +546,7 @@ async function interactiveChoose(question, options, hint) {
|
|
|
462
546
|
});
|
|
463
547
|
}
|
|
464
548
|
|
|
465
|
-
async function legacyChoose(question, options, hint) {
|
|
549
|
+
async function legacyChoose(question, options, hint, opts = {}) {
|
|
466
550
|
process.stdout.write(`\n ${CYAN}◆${RESET} ${BOLD}${question}${RESET}\n\n`);
|
|
467
551
|
const labelWidth = Math.min(40, Math.max(...options.map((o) => o.label.length)));
|
|
468
552
|
for (let i = 0; i < options.length; i++) {
|
|
@@ -473,8 +557,10 @@ async function legacyChoose(question, options, hint) {
|
|
|
473
557
|
process.stdout.write(` ${DIM}${num})${RESET} ${BOLD}${paddedLabel}${RESET}${desc}\n`);
|
|
474
558
|
}
|
|
475
559
|
if (hint) process.stdout.write(`\n ${DIM}${hint}${RESET}\n`);
|
|
560
|
+
if (opts.escAction === "back") process.stdout.write(` ${DIM}type "back" to go back${RESET}\n`);
|
|
476
561
|
while (true) {
|
|
477
562
|
const answer = await prompt(`pick ${DIM}[1-${options.length}]${RESET}:`);
|
|
563
|
+
if (opts.escAction === "back" && /^(b|back)$/i.test(answer)) return BACK;
|
|
478
564
|
if (!answer) { warn(`Enter a number between 1 and ${options.length}.`); continue; }
|
|
479
565
|
const n = parseInt(answer, 10);
|
|
480
566
|
if (n >= 1 && n <= options.length) {
|
|
@@ -523,7 +609,9 @@ async function interactiveMultiSelect(question, options, opts = {}) {
|
|
|
523
609
|
return new Promise((resolve) => {
|
|
524
610
|
let cursor = 0;
|
|
525
611
|
let filterText = "";
|
|
526
|
-
|
|
612
|
+
// opts.preselected re-arms a previous selection — used when the step
|
|
613
|
+
// machine navigates back to the picker so choices aren't lost.
|
|
614
|
+
const picked = new Set((opts.preselected || []).filter((n) => options.some((o) => o.name === n)));
|
|
527
615
|
// The pool grows as remote results merge in; seed order is preserved so
|
|
528
616
|
// boosted entries (e.g. the user's team repos) stay on top when idle.
|
|
529
617
|
const pool = [...options];
|
|
@@ -584,13 +672,14 @@ async function interactiveMultiSelect(question, options, opts = {}) {
|
|
|
584
672
|
if (searching && total > 0) lines.push(` ${DIM}searching…${RESET}`);
|
|
585
673
|
|
|
586
674
|
const searchHint = opts.remoteSearch ? "type to search" : "type to filter";
|
|
675
|
+
const escHint = opts.escAction === "back" ? ` · ${DIM}esc${RESET} back` : "";
|
|
587
676
|
if (!first) clearLines(lastLineCount);
|
|
588
677
|
lastLineCount = renderPicker({
|
|
589
678
|
question,
|
|
590
679
|
filterText,
|
|
591
680
|
lines,
|
|
592
681
|
input: { label: opts.remoteSearch ? "search" : "filter" },
|
|
593
|
-
help: `${DIM}↑↓${RESET} nav · ${DIM}space${RESET} toggle · ${DIM}ctrl-a${RESET} all · ${DIM}enter${RESET} confirm · ${searchHint}`,
|
|
682
|
+
help: `${DIM}↑↓${RESET} nav · ${DIM}space${RESET} toggle · ${DIM}ctrl-a${RESET} all · ${DIM}enter${RESET} confirm · ${searchHint}${escHint}`,
|
|
594
683
|
});
|
|
595
684
|
};
|
|
596
685
|
|
|
@@ -661,6 +750,21 @@ async function interactiveMultiSelect(question, options, opts = {}) {
|
|
|
661
750
|
|
|
662
751
|
const onKey = (_str, key) => {
|
|
663
752
|
if (!key) return;
|
|
753
|
+
if (key.name === "escape" && filterText) {
|
|
754
|
+
filterText = "";
|
|
755
|
+
refreshVisible();
|
|
756
|
+
cursor = 0;
|
|
757
|
+
searching = false;
|
|
758
|
+
render();
|
|
759
|
+
return;
|
|
760
|
+
}
|
|
761
|
+
if (key.name === "escape" && opts.escAction === "back") {
|
|
762
|
+
cleanup();
|
|
763
|
+
clearLines(lastLineCount);
|
|
764
|
+
process.stdout.write(` ${CYAN}◆${RESET} ${BOLD}${question}${RESET} ${DIM}→ ← back${RESET}\n`);
|
|
765
|
+
resolve(BACK);
|
|
766
|
+
return;
|
|
767
|
+
}
|
|
664
768
|
if ((key.ctrl && key.name === "c") || key.name === "escape") {
|
|
665
769
|
cleanup();
|
|
666
770
|
unparkCursor();
|
|
@@ -1009,7 +1113,7 @@ module.exports = {
|
|
|
1009
1113
|
banner, bannerCompact, section, note, card,
|
|
1010
1114
|
info, success, warn, error, step,
|
|
1011
1115
|
dim, bold, cyan, green, yellow, red,
|
|
1012
|
-
spinner, prompt, choose, multiSelect, confirm, canInteract,
|
|
1116
|
+
spinner, prompt, promptOrBack, choose, multiSelect, confirm, canInteract, BACK,
|
|
1013
1117
|
commandBox,
|
|
1014
1118
|
canShowScreens, welcomeScreen, summaryScreen,
|
|
1015
1119
|
_renderWelcomeBody, _readPkgVersion,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "create-egregore",
|
|
3
|
-
"version": "0.18.
|
|
3
|
+
"version": "0.18.18",
|
|
4
4
|
"description": "Set up Egregore for your team in one command — installs the egregore launcher and your choice of Claude Code or Codex.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"bin": {
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
},
|
|
12
12
|
"scripts": {
|
|
13
13
|
"build:runtime": "node scripts/build-runtime-bundle.mjs",
|
|
14
|
-
"test": "node scripts/build-runtime-bundle.mjs --check && node --test test/context.test.js test/parse-join-target.test.js test/skip-repos.test.js test/onboarding-shortcircuit.test.js test/welcome-screen.test.js test/runtime-selection.test.js test/command-generation.test.js test/auth-gh.test.js test/gauntlet-fixes.test.js test/codex-runtime.test.js test/installer-runtime-source.test.js test/scroll-repro.test.js test/launcher-refocus-redraw.test.js test/multiselect-remote-search.test.js",
|
|
14
|
+
"test": "node scripts/build-runtime-bundle.mjs --check && node --test test/context.test.js test/parse-join-target.test.js test/skip-repos.test.js test/onboarding-shortcircuit.test.js test/welcome-screen.test.js test/runtime-selection.test.js test/command-generation.test.js test/auth-gh.test.js test/gauntlet-fixes.test.js test/codex-runtime.test.js test/installer-runtime-source.test.js test/scroll-repro.test.js test/launcher-refocus-redraw.test.js test/multiselect-remote-search.test.js test/back-navigation.test.js",
|
|
15
15
|
"test:e2e": "node --test test/e2e-local.test.js",
|
|
16
16
|
"test:all": "node scripts/build-runtime-bundle.mjs --check && node --test test/"
|
|
17
17
|
},
|
|
@@ -162,3 +162,13 @@ PROJ_HASH=$(echo -n "$SCRIPT_DIR" | md5 2>/dev/null || echo -n "$SCRIPT_DIR" | m
|
|
|
162
162
|
echo "$EGREGORE_SESSION_ID" > "$SESSION_ID_DIR/session-${PROJ_HASH}.id"
|
|
163
163
|
# Also write project-local file (CLAUDE.md reads this — no glob needed)
|
|
164
164
|
echo "$EGREGORE_SESSION_ID" > "$SCRIPT_DIR/.egregore-session-id"
|
|
165
|
+
|
|
166
|
+
# Model stamp hashes the git common-dir root because telemetry.sh resolves it that way.
|
|
167
|
+
MODEL_REPO_ROOT=$(git -C "$SCRIPT_DIR" rev-parse --path-format=absolute --git-common-dir 2>/dev/null | sed 's|/\.git$||') || MODEL_REPO_ROOT="$SCRIPT_DIR"
|
|
168
|
+
MODEL_PROJ_HASH=$(echo -n "$MODEL_REPO_ROOT" | md5 2>/dev/null || echo -n "$MODEL_REPO_ROOT" | md5sum 2>/dev/null | cut -d' ' -f1)
|
|
169
|
+
MODEL_STAMP_FILE="$SESSION_ID_DIR/session-model-${MODEL_PROJ_HASH}"
|
|
170
|
+
if [ -n "${CLAUDE_MODEL:-}" ]; then
|
|
171
|
+
echo "$CLAUDE_MODEL" > "$MODEL_STAMP_FILE"
|
|
172
|
+
else
|
|
173
|
+
rm -f "$MODEL_STAMP_FILE"
|
|
174
|
+
fi
|
|
@@ -98,6 +98,13 @@ _resolve_org() {
|
|
|
98
98
|
fi
|
|
99
99
|
}
|
|
100
100
|
|
|
101
|
+
_resolve_proj_hash() {
|
|
102
|
+
# Use git common dir so worktrees hash to the same path as the main checkout
|
|
103
|
+
local repo_root
|
|
104
|
+
repo_root=$(git -C "$SCRIPT_DIR" rev-parse --path-format=absolute --git-common-dir 2>/dev/null | sed 's|/\.git$||') || repo_root="$SCRIPT_DIR"
|
|
105
|
+
echo -n "$repo_root" | md5 2>/dev/null || echo -n "$repo_root" | md5sum 2>/dev/null | cut -d' ' -f1
|
|
106
|
+
}
|
|
107
|
+
|
|
101
108
|
_resolve_session_id() {
|
|
102
109
|
if [ -n "${EGREGORE_SESSION_ID:-}" ]; then
|
|
103
110
|
echo "$EGREGORE_SESSION_ID"
|
|
@@ -106,11 +113,8 @@ _resolve_session_id() {
|
|
|
106
113
|
|
|
107
114
|
# Fallback: read from file written by session-start.sh
|
|
108
115
|
# (env vars from hooks don't propagate into the Claude Code agent)
|
|
109
|
-
# Use git common dir so worktrees hash to the same path as the main checkout
|
|
110
|
-
local repo_root
|
|
111
|
-
repo_root=$(git -C "$SCRIPT_DIR" rev-parse --path-format=absolute --git-common-dir 2>/dev/null | sed 's|/\.git$||') || repo_root="$SCRIPT_DIR"
|
|
112
116
|
local proj_hash
|
|
113
|
-
proj_hash=$(
|
|
117
|
+
proj_hash=$(_resolve_proj_hash)
|
|
114
118
|
local sid_file="$HOME/.egregore/session-${proj_hash}.id"
|
|
115
119
|
if [ -f "$sid_file" ]; then
|
|
116
120
|
cat "$sid_file" 2>/dev/null || echo "unknown"
|
|
@@ -119,6 +123,45 @@ _resolve_session_id() {
|
|
|
119
123
|
fi
|
|
120
124
|
}
|
|
121
125
|
|
|
126
|
+
_resolve_model() {
|
|
127
|
+
local proj_hash
|
|
128
|
+
proj_hash=$(_resolve_proj_hash)
|
|
129
|
+
local model_file="$HOME/.egregore/session-model-${proj_hash}"
|
|
130
|
+
if [ ! -s "$model_file" ]; then
|
|
131
|
+
return 0
|
|
132
|
+
fi
|
|
133
|
+
|
|
134
|
+
local model=""
|
|
135
|
+
IFS= read -r model < "$model_file" || true
|
|
136
|
+
if [ -n "$model" ]; then
|
|
137
|
+
printf '%s\n' "$model"
|
|
138
|
+
fi
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
_stamp_model_if_absent() {
|
|
142
|
+
local compact_data="$1"
|
|
143
|
+
local has_model=""
|
|
144
|
+
has_model=$(printf '%s' "$compact_data" | jq -e 'has("model")' 2>/dev/null || true)
|
|
145
|
+
if [ "$has_model" != "false" ]; then
|
|
146
|
+
printf '%s' "$compact_data"
|
|
147
|
+
return 0
|
|
148
|
+
fi
|
|
149
|
+
|
|
150
|
+
local model=""
|
|
151
|
+
model=$(_resolve_model 2>/dev/null || true)
|
|
152
|
+
if [ -z "$model" ]; then
|
|
153
|
+
printf '%s' "$compact_data"
|
|
154
|
+
return 0
|
|
155
|
+
fi
|
|
156
|
+
|
|
157
|
+
local stamped_data=""
|
|
158
|
+
if stamped_data=$(printf '%s' "$compact_data" | jq -c --arg m "$model" 'if has("model") then . else . + {model:$m} end' 2>/dev/null); then
|
|
159
|
+
printf '%s' "$stamped_data"
|
|
160
|
+
else
|
|
161
|
+
printf '%s' "$compact_data"
|
|
162
|
+
fi
|
|
163
|
+
}
|
|
164
|
+
|
|
122
165
|
# --- emit: append event to local JSONL buffer ---
|
|
123
166
|
cmd_emit() {
|
|
124
167
|
local event_type="${1:?Usage: telemetry.sh emit <type> <json>}"
|
|
@@ -129,12 +172,15 @@ cmd_emit() {
|
|
|
129
172
|
return 0
|
|
130
173
|
fi
|
|
131
174
|
|
|
175
|
+
local ts
|
|
176
|
+
ts=$(date -u +%Y-%m-%dT%H:%M:%SZ)
|
|
177
|
+
# Compact data to single line — callers may pass pretty-printed JSON
|
|
178
|
+
local compact_data
|
|
179
|
+
compact_data=$(printf '%s' "$data" | tr -d '\n' | tr -s ' ')
|
|
180
|
+
compact_data=$(_stamp_model_if_absent "$compact_data")
|
|
181
|
+
|
|
132
182
|
# Debug mode: print to stderr instead of writing
|
|
133
183
|
if _is_debug; then
|
|
134
|
-
local ts
|
|
135
|
-
ts=$(date -u +%Y-%m-%dT%H:%M:%SZ)
|
|
136
|
-
local compact_data
|
|
137
|
-
compact_data=$(printf '%s' "$data" | tr -d '\n' | tr -s ' ')
|
|
138
184
|
printf '{"ts":"%s","type":"%s","sid":"%s","org":"%s","user":"%s","data":%s}\n' \
|
|
139
185
|
"$ts" "$event_type" "$(_resolve_session_id)" "$(_resolve_org)" "$(_resolve_user)" "$compact_data" >&2
|
|
140
186
|
return 0
|
|
@@ -144,11 +190,6 @@ cmd_emit() {
|
|
|
144
190
|
mkdir -p "$BUFFER_DIR"
|
|
145
191
|
|
|
146
192
|
# Build event line (outside lock for minimal hold time)
|
|
147
|
-
local ts
|
|
148
|
-
ts=$(date -u +%Y-%m-%dT%H:%M:%SZ)
|
|
149
|
-
# Compact data to single line — callers may pass pretty-printed JSON
|
|
150
|
-
local compact_data
|
|
151
|
-
compact_data=$(printf '%s' "$data" | tr -d '\n' | tr -s ' ')
|
|
152
193
|
local line
|
|
153
194
|
line=$(printf '{"ts":"%s","type":"%s","sid":"%s","org":"%s","user":"%s","data":%s}' \
|
|
154
195
|
"$ts" "$event_type" "$(_resolve_session_id)" "$(_resolve_org)" "$(_resolve_user)" "$compact_data")
|