@forgecart/cli 2.202610141814.0 → 2.202610142335.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/dist/src/admin-api/admin-api.answer.d.ts +26 -0
- package/dist/src/admin-api/admin-api.answer.js +43 -0
- package/dist/src/admin-api/admin-api.answer.js.map +1 -0
- package/dist/src/admin-api/admin-api.client.d.ts +6 -4
- package/dist/src/admin-api/admin-api.client.js +19 -15
- package/dist/src/admin-api/admin-api.client.js.map +1 -1
- package/dist/src/admin-api/admin-api.result.d.ts +15 -9
- package/dist/src/admin-api/admin-api.result.js +45 -30
- package/dist/src/admin-api/admin-api.result.js.map +1 -1
- package/dist/src/authorization/cli-authorization.client.d.ts +4 -3
- package/dist/src/authorization/cli-authorization.client.js +18 -13
- package/dist/src/authorization/cli-authorization.client.js.map +1 -1
- package/dist/src/commands/sync.js +1 -1
- package/dist/src/commands/sync.js.map +1 -1
- package/dist/src/sync/sync-bundle-push.d.ts +15 -3
- package/dist/src/sync/sync-bundle-push.js +45 -2
- package/dist/src/sync/sync-bundle-push.js.map +1 -1
- package/dist/src/sync/sync-record.file.d.ts +27 -3
- package/dist/src/sync/sync-record.file.js +77 -25
- package/dist/src/sync/sync-record.file.js.map +1 -1
- package/dist/src/sync/sync-rejection.d.ts +24 -0
- package/dist/src/sync/sync-rejection.js +58 -0
- package/dist/src/sync/sync-rejection.js.map +1 -0
- package/dist/src/sync/sync-repository.d.ts +9 -1
- package/dist/src/sync/sync-repository.js +23 -1
- package/dist/src/sync/sync-repository.js.map +1 -1
- package/dist/src/sync/sync-round-follow.d.ts +42 -0
- package/dist/src/sync/sync-round-follow.js +128 -0
- package/dist/src/sync/sync-round-follow.js.map +1 -0
- package/dist/src/sync/sync-round.d.ts +49 -8
- package/dist/src/sync/sync-round.js +134 -27
- package/dist/src/sync/sync-round.js.map +1 -1
- package/dist/src/sync/sync.runner.d.ts +4 -2
- package/dist/src/sync/sync.runner.js +6 -3
- package/dist/src/sync/sync.runner.js.map +1 -1
- package/dist/src/sync/type/sync.types.d.ts +78 -2
- package/dist/src/sync/type/sync.types.js +31 -0
- package/dist/src/sync/type/sync.types.js.map +1 -1
- package/dist/src/sync/workspace-sync.client.d.ts +29 -6
- package/dist/src/sync/workspace-sync.client.js +141 -21
- package/dist/src/sync/workspace-sync.client.js.map +1 -1
- package/package.json +1 -1
- package/templates/storefront/README.md +52 -5
- package/templates/storefront/src/app/account/page.tsx +51 -0
- package/templates/storefront/src/app/layout.tsx +4 -5
- package/templates/storefront/src/app/verify/page.tsx +16 -4
- package/templates/storefront/src/components/Header.tsx +13 -1
- package/templates/storefront/src/components/account/AccountPanel.tsx +289 -0
- package/templates/storefront/src/components/account/RegisterForm.tsx +15 -66
- package/templates/storefront/src/components/account/ResetPasswordForm.tsx +2 -2
- package/templates/storefront/src/components/account/VerificationResend.tsx +96 -0
- package/templates/storefront/src/lib/account/account-state.ts +152 -0
- package/templates/storefront/src/lib/account-session.ts +83 -16
- package/templates/storefront/src/lib/error-messages.ts +102 -8
- package/templates/storefront/src/lib/seo/noindex.ts +5 -0
- package/templates/storefront/src/lib/seo/sitemap-entries.ts +2 -0
- package/templates/storefront/src/lib/shop-session.ts +71 -18
- package/templates/storefront-shadcn/README.md +52 -5
- package/templates/storefront-shadcn/src/app/account/page.tsx +51 -0
- package/templates/storefront-shadcn/src/app/layout.tsx +4 -5
- package/templates/storefront-shadcn/src/app/verify/page.tsx +16 -4
- package/templates/storefront-shadcn/src/components/Header.tsx +17 -1
- package/templates/storefront-shadcn/src/components/account/AccountPanel.tsx +290 -0
- package/templates/storefront-shadcn/src/components/account/RegisterForm.tsx +19 -61
- package/templates/storefront-shadcn/src/components/account/ResetPasswordForm.tsx +2 -2
- package/templates/storefront-shadcn/src/components/account/VerificationResend.tsx +92 -0
- package/templates/storefront-shadcn/src/lib/account/account-state.ts +152 -0
- package/templates/storefront-shadcn/src/lib/account-session.ts +83 -16
- package/templates/storefront-shadcn/src/lib/error-messages.ts +102 -8
- package/templates/storefront-shadcn/src/lib/seo/noindex.ts +5 -0
- package/templates/storefront-shadcn/src/lib/seo/sitemap-entries.ts +2 -0
- package/templates/storefront-shadcn/src/lib/shop-session.ts +71 -18
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"sync.runner.js","sourceRoot":"","sources":["../../../src/sync/sync.runner.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,EAAE,IAAI,EAAE,MAAM,kBAAkB,CAAC;AAC/C,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAEpC,OAAO,EAAE,cAAc,EAAE,MAAM,kCAAkC,CAAC;AAClE,OAAO,EAAE,iBAAiB,EAAE,MAAM,yCAAyC,CAAC;AAK5E,OAAO,EAAE,qBAAqB,EAAE,MAAM,wCAAwC,CAAC;AAE/E,OAAO,EAAE,cAAc,EAAE,MAAM,uBAAuB,CAAC;AACvD,OAAO,EAAE,uBAAuB,EAAE,MAAM,sBAAsB,CAAC;AAC/D,OAAO,EAAE,cAAc,EAAE,MAAM,uBAAuB,CAAC;AACvD,OAAO,EAAE,cAAc,EAAE,MAAM,uBAAuB,CAAC;AACvD,OAAO,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAC;AACtD,OAAO,EAAE,SAAS,EAAE,MAAM,iBAAiB,CAAC;AAC5C,OAAO,EAAE,SAAS,EAAE,MAAM,iBAAiB,CAAC;AAE5C,OAAO,EAAE,mBAAmB,EAAE,MAAM,4BAA4B,CAAC;AAEjE
|
|
1
|
+
{"version":3,"file":"sync.runner.js","sourceRoot":"","sources":["../../../src/sync/sync.runner.ts"],"names":[],"mappings":"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"}
|
|
@@ -85,16 +85,92 @@ export interface SyncUpload {
|
|
|
85
85
|
partCount: number;
|
|
86
86
|
heldParts: number[];
|
|
87
87
|
}
|
|
88
|
-
/**
|
|
88
|
+
/**
|
|
89
|
+
* One finding of the SEO gate (factory task 2481): its code, its severity —
|
|
90
|
+
* {@link SyncSeoSeverity.BLOCKING} blocks the landing, any other never does —
|
|
91
|
+
* the route it was crawled at, and what was observed there.
|
|
92
|
+
*/
|
|
93
|
+
export interface SyncSeoFinding {
|
|
94
|
+
code: string;
|
|
95
|
+
severity: string;
|
|
96
|
+
url: string;
|
|
97
|
+
detail: string;
|
|
98
|
+
}
|
|
99
|
+
/** The severity of an SEO finding that blocks the landing; every other severity is a warning. */
|
|
100
|
+
export declare enum SyncSeoSeverity {
|
|
101
|
+
BLOCKING = "RED"
|
|
102
|
+
}
|
|
103
|
+
/** A push's verdict: what the round it landed as ended on. */
|
|
89
104
|
export interface SyncPushResult {
|
|
90
105
|
accepted: boolean;
|
|
91
106
|
servingSha: string | null;
|
|
92
107
|
gateMessages: string[];
|
|
93
108
|
runtimeError: string | null;
|
|
94
|
-
seoFindings:
|
|
109
|
+
seoFindings: SyncSeoFinding[];
|
|
95
110
|
/** How many merchant keys the push merged into each env file it carried. */
|
|
96
111
|
envFiles: SyncEnvFileMerge[];
|
|
97
112
|
}
|
|
113
|
+
/** How a push's round stands (factory task 2481): the workspace lands it, or it has its verdict. */
|
|
114
|
+
export declare enum SyncPushRoundState {
|
|
115
|
+
RUNNING = "RUNNING",
|
|
116
|
+
ENDED = "ENDED"
|
|
117
|
+
}
|
|
118
|
+
/** The steps of a push's round, in the order a round that runs every one of them goes through them. */
|
|
119
|
+
export declare enum SyncPushRoundPhase {
|
|
120
|
+
BUNDLE_ASSEMBLED = "BUNDLE_ASSEMBLED",
|
|
121
|
+
IMPORT = "IMPORT",
|
|
122
|
+
COMPILE_GATE = "COMPILE_GATE",
|
|
123
|
+
DEPENDENCY_INSTALL = "DEPENDENCY_INSTALL",
|
|
124
|
+
DEV_SERVER_RESTART = "DEV_SERVER_RESTART",
|
|
125
|
+
RUNTIME_SETTLE = "RUNTIME_SETTLE",
|
|
126
|
+
SEO_GATE = "SEO_GATE",
|
|
127
|
+
APPLY = "APPLY",
|
|
128
|
+
ENV_MERGE = "ENV_MERGE"
|
|
129
|
+
}
|
|
130
|
+
/** How one step of a push's round stands. */
|
|
131
|
+
export declare enum SyncPushRoundStepStatus {
|
|
132
|
+
STARTED = "STARTED",
|
|
133
|
+
ENDED = "ENDED",
|
|
134
|
+
SKIPPED = "SKIPPED"
|
|
135
|
+
}
|
|
136
|
+
/**
|
|
137
|
+
* One step of a push's round: its phase — a {@link SyncPushRoundPhase}, or one
|
|
138
|
+
* a later admin API added, printed by its own name — how it stands, and the
|
|
139
|
+
* milliseconds it took once it ended.
|
|
140
|
+
*/
|
|
141
|
+
export interface SyncPushRoundStep {
|
|
142
|
+
phase: string;
|
|
143
|
+
status: SyncPushRoundStepStatus;
|
|
144
|
+
elapsedMs: number | null;
|
|
145
|
+
}
|
|
146
|
+
/**
|
|
147
|
+
* The round a push lands as (factory task 2481), as the admin API answers the
|
|
148
|
+
* push and every read of it: its id, how it stands, every step it went through
|
|
149
|
+
* so far — each as it last stood — and its verdict once it ended.
|
|
150
|
+
*/
|
|
151
|
+
export interface SyncPushRound {
|
|
152
|
+
id: string;
|
|
153
|
+
state: SyncPushRoundState;
|
|
154
|
+
steps: SyncPushRoundStep[];
|
|
155
|
+
verdict: SyncPushResult | null;
|
|
156
|
+
}
|
|
157
|
+
/**
|
|
158
|
+
* A push's round this laptop has not seen end (factory task 2481), recorded the
|
|
159
|
+
* moment the push answered it: the round, the serving commit the push was cut
|
|
160
|
+
* on, and the env digests the record takes once it landed. The next run
|
|
161
|
+
* follows it before it pushes anything.
|
|
162
|
+
*/
|
|
163
|
+
export interface SyncPendingRound {
|
|
164
|
+
channel: string;
|
|
165
|
+
roundId: string;
|
|
166
|
+
cutOn: string;
|
|
167
|
+
env: SyncEnvDigests;
|
|
168
|
+
}
|
|
169
|
+
/** One top-level entry of a pushed tree and the bytes its files hold — a directory carries a trailing `/`. */
|
|
170
|
+
export interface SyncTopLevelEntry {
|
|
171
|
+
name: string;
|
|
172
|
+
bytes: number;
|
|
173
|
+
}
|
|
98
174
|
/**
|
|
99
175
|
* The last sync, recorded under `.forgecart/sync/`: the channel it was against,
|
|
100
176
|
* the serving commit the laptop's tree last matched, and a digest of every
|
|
@@ -1,3 +1,34 @@
|
|
|
1
|
+
/** The severity of an SEO finding that blocks the landing; every other severity is a warning. */
|
|
2
|
+
export var SyncSeoSeverity;
|
|
3
|
+
(function (SyncSeoSeverity) {
|
|
4
|
+
SyncSeoSeverity["BLOCKING"] = "RED";
|
|
5
|
+
})(SyncSeoSeverity || (SyncSeoSeverity = {}));
|
|
6
|
+
/** How a push's round stands (factory task 2481): the workspace lands it, or it has its verdict. */
|
|
7
|
+
export var SyncPushRoundState;
|
|
8
|
+
(function (SyncPushRoundState) {
|
|
9
|
+
SyncPushRoundState["RUNNING"] = "RUNNING";
|
|
10
|
+
SyncPushRoundState["ENDED"] = "ENDED";
|
|
11
|
+
})(SyncPushRoundState || (SyncPushRoundState = {}));
|
|
12
|
+
/** The steps of a push's round, in the order a round that runs every one of them goes through them. */
|
|
13
|
+
export var SyncPushRoundPhase;
|
|
14
|
+
(function (SyncPushRoundPhase) {
|
|
15
|
+
SyncPushRoundPhase["BUNDLE_ASSEMBLED"] = "BUNDLE_ASSEMBLED";
|
|
16
|
+
SyncPushRoundPhase["IMPORT"] = "IMPORT";
|
|
17
|
+
SyncPushRoundPhase["COMPILE_GATE"] = "COMPILE_GATE";
|
|
18
|
+
SyncPushRoundPhase["DEPENDENCY_INSTALL"] = "DEPENDENCY_INSTALL";
|
|
19
|
+
SyncPushRoundPhase["DEV_SERVER_RESTART"] = "DEV_SERVER_RESTART";
|
|
20
|
+
SyncPushRoundPhase["RUNTIME_SETTLE"] = "RUNTIME_SETTLE";
|
|
21
|
+
SyncPushRoundPhase["SEO_GATE"] = "SEO_GATE";
|
|
22
|
+
SyncPushRoundPhase["APPLY"] = "APPLY";
|
|
23
|
+
SyncPushRoundPhase["ENV_MERGE"] = "ENV_MERGE";
|
|
24
|
+
})(SyncPushRoundPhase || (SyncPushRoundPhase = {}));
|
|
25
|
+
/** How one step of a push's round stands. */
|
|
26
|
+
export var SyncPushRoundStepStatus;
|
|
27
|
+
(function (SyncPushRoundStepStatus) {
|
|
28
|
+
SyncPushRoundStepStatus["STARTED"] = "STARTED";
|
|
29
|
+
SyncPushRoundStepStatus["ENDED"] = "ENDED";
|
|
30
|
+
SyncPushRoundStepStatus["SKIPPED"] = "SKIPPED";
|
|
31
|
+
})(SyncPushRoundStepStatus || (SyncPushRoundStepStatus = {}));
|
|
1
32
|
/** How a path differs between two trees (`git diff-tree`'s status letters). */
|
|
2
33
|
export var SyncChangeKind;
|
|
3
34
|
(function (SyncChangeKind) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"sync.types.js","sourceRoot":"","sources":["../../../../src/sync/type/sync.types.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"sync.types.js","sourceRoot":"","sources":["../../../../src/sync/type/sync.types.ts"],"names":[],"mappings":"AAgHA,iGAAiG;AACjG,MAAM,CAAN,IAAY,eAEX;AAFD,WAAY,eAAe;IACzB,mCAAgB,CAAA;AAClB,CAAC,EAFW,eAAe,KAAf,eAAe,QAE1B;AAaD,oGAAoG;AACpG,MAAM,CAAN,IAAY,kBAGX;AAHD,WAAY,kBAAkB;IAC5B,yCAAmB,CAAA;IACnB,qCAAe,CAAA;AACjB,CAAC,EAHW,kBAAkB,KAAlB,kBAAkB,QAG7B;AAED,uGAAuG;AACvG,MAAM,CAAN,IAAY,kBAUX;AAVD,WAAY,kBAAkB;IAC5B,2DAAqC,CAAA;IACrC,uCAAiB,CAAA;IACjB,mDAA6B,CAAA;IAC7B,+DAAyC,CAAA;IACzC,+DAAyC,CAAA;IACzC,uDAAiC,CAAA;IACjC,2CAAqB,CAAA;IACrB,qCAAe,CAAA;IACf,6CAAuB,CAAA;AACzB,CAAC,EAVW,kBAAkB,KAAlB,kBAAkB,QAU7B;AAED,6CAA6C;AAC7C,MAAM,CAAN,IAAY,uBAIX;AAJD,WAAY,uBAAuB;IACjC,8CAAmB,CAAA;IACnB,0CAAe,CAAA;IACf,8CAAmB,CAAA;AACrB,CAAC,EAJW,uBAAuB,KAAvB,uBAAuB,QAIlC;AAuDD,+EAA+E;AAC/E,MAAM,CAAN,IAAY,cAKX;AALD,WAAY,cAAc;IACxB,6BAAW,CAAA;IACX,+BAAa,CAAA;IACb,gCAAc,CAAA;IACd,oCAAkB,CAAA;AACpB,CAAC,EALW,cAAc,KAAd,cAAc,QAKzB;AA4CD,0CAA0C;AAC1C,MAAM,CAAN,IAAY,WAWX;AAXD,WAAY,WAAW;IACrB,6CAA6C;IAC7C,kCAAmB,CAAA;IACnB,uEAAuE;IACvE,4BAAa,CAAA;IACb,kEAAkE;IAClE,4BAAa,CAAA;IACb,oEAAoE;IACpE,wCAAyB,CAAA;IACzB,2DAA2D;IAC3D,oCAAqB,CAAA;AACvB,CAAC,EAXW,WAAW,KAAX,WAAW,QAWtB;AAmDD,mFAAmF;AACnF,MAAM,CAAN,IAAY,oBAIX;AAJD,WAAY,oBAAoB;IAC9B,uCAAe,CAAA;IACf,+CAAuB,CAAA;IACvB,uCAAe,CAAA;AACjB,CAAC,EAJW,oBAAoB,KAApB,oBAAoB,QAI/B"}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { AdminApiClient } from '../admin-api/admin-api.client.js';
|
|
2
|
-
import type
|
|
2
|
+
import { type SyncBundleDeclaration, type SyncBundlePart, type SyncEnvFileLines, type SyncPulledBundle, type SyncPushRound, type SyncServingState, type SyncUpload } from './type/sync.types.js';
|
|
3
3
|
/**
|
|
4
4
|
* The admin API's workspace-sync operations (#2781) — the reads of the serving
|
|
5
5
|
* branch, the push, and the begin and the parts of a chunked push (factory task
|
|
@@ -10,6 +10,12 @@ import type { SyncBundleDeclaration, SyncBundlePart, SyncEnvFileLines, SyncPulle
|
|
|
10
10
|
* credential step answers by authorizing. A workspace updating to the current
|
|
11
11
|
* version (#2371) is no refusal: every operation waits it out and asks again
|
|
12
12
|
* ({@link SyncImageUpdateWait}).
|
|
13
|
+
*
|
|
14
|
+
* A push answers the round it lands as, and the round is read until it ended
|
|
15
|
+
* (factory task 2481): no request waits on the landing. An admin API from
|
|
16
|
+
* before the round answers the push's selection GRAPHQL_VALIDATION_FAILED, and
|
|
17
|
+
* the run is refused naming this CLI's version and the operation it misses —
|
|
18
|
+
* nothing lands on it.
|
|
13
19
|
*/
|
|
14
20
|
export declare class WorkspaceSyncClient {
|
|
15
21
|
private readonly api;
|
|
@@ -31,8 +37,11 @@ export declare class WorkspaceSyncClient {
|
|
|
31
37
|
* Push `bundle` — the laptop's tree as one commit cut on the serving tip — as
|
|
32
38
|
* an authenticated upload, with the merchant lines of the env files
|
|
33
39
|
* `envFiles`, file by file; a push without a bundle carries the lines alone.
|
|
40
|
+
* Answers the round the push lands as.
|
|
34
41
|
*/
|
|
35
|
-
push(bundle: Buffer | null, envFiles: readonly SyncEnvFileLines[]): Promise<
|
|
42
|
+
push(bundle: Buffer | null, envFiles: readonly SyncEnvFileLines[]): Promise<SyncPushRound>;
|
|
43
|
+
/** The round `roundId` a push answered, as it stands. */
|
|
44
|
+
round(roundId: string): Promise<SyncPushRound>;
|
|
36
45
|
/**
|
|
37
46
|
* Begin the chunked push of the bundle `declaration` declares — or, for a
|
|
38
47
|
* bundle the workspace already holds parts of, answer that same upload
|
|
@@ -44,11 +53,25 @@ export declare class WorkspaceSyncClient {
|
|
|
44
53
|
/**
|
|
45
54
|
* Land the parts the upload `uploadId` holds as ONE bundle, with the
|
|
46
55
|
* merchant lines of the env files `envFiles`, file by file — a push naming
|
|
47
|
-
* its upload, carrying no bundle.
|
|
56
|
+
* its upload, carrying no bundle. Answers the round the push lands as.
|
|
48
57
|
*/
|
|
49
|
-
land(uploadId: string, envFiles: readonly SyncEnvFileLines[]): Promise<
|
|
50
|
-
/**
|
|
51
|
-
|
|
58
|
+
land(uploadId: string, envFiles: readonly SyncEnvFileLines[]): Promise<SyncPushRound>;
|
|
59
|
+
/**
|
|
60
|
+
* Send a push or a read of its round, and read the round it answers. An
|
|
61
|
+
* admin API that offers no round refuses the run, naming what it misses.
|
|
62
|
+
*/
|
|
63
|
+
private roundOf;
|
|
64
|
+
/** The refusal of an admin API from before the round: this CLI's version, and the operation it misses. */
|
|
65
|
+
private roundless;
|
|
66
|
+
/** A push's round, read field by field: its verdict once it ended, null while it runs. */
|
|
67
|
+
private readRound;
|
|
68
|
+
/** One step of a round: its phase by name, how it stands, and its elapsed milliseconds once it ended. */
|
|
69
|
+
private readStep;
|
|
70
|
+
private isRoundState;
|
|
71
|
+
private isStepStatus;
|
|
72
|
+
/** What a push did, read off the verdict its round ended on. */
|
|
73
|
+
private readVerdict;
|
|
74
|
+
private readFinding;
|
|
52
75
|
/** A push with a bundle is an upload; one with env lines alone is plain JSON. */
|
|
53
76
|
private pushRequest;
|
|
54
77
|
/** The push carrying its bundle whole, declared by its byte length and sha256. */
|
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
import { createHash } from 'node:crypto';
|
|
2
|
+
import { AdminApiRefusal } from '../admin-api/admin-api-refusal.error.js';
|
|
3
|
+
import { CliExitLadder, CliRefusal } from '../exit-ladder.js';
|
|
2
4
|
import { isStorefrontEnvFile } from '../util/dotenv.util.js';
|
|
5
|
+
import { readPackageVersion } from '../version.js';
|
|
3
6
|
import { SyncImageUpdateWait } from './sync-image-update.js';
|
|
7
|
+
import { SyncPushRoundState, SyncPushRoundStepStatus, } from './type/sync.types.js';
|
|
4
8
|
/** The SDK's `WorkspaceSyncRetrieve` operation, verbatim. */
|
|
5
9
|
const STATE_DOCUMENT = `query WorkspaceSyncRetrieve {
|
|
6
10
|
workspaceSyncState {
|
|
@@ -22,21 +26,75 @@ const BUNDLE_DOCUMENT = `query WorkspaceSyncPull($input: WorkspaceSyncBundleInpu
|
|
|
22
26
|
}
|
|
23
27
|
}
|
|
24
28
|
}`;
|
|
25
|
-
/**
|
|
29
|
+
/**
|
|
30
|
+
* The SDK's `WorkspaceSyncPush` operation, verbatim: a push answers the round
|
|
31
|
+
* it lands as (factory task 2481).
|
|
32
|
+
*/
|
|
26
33
|
const PUSH_DOCUMENT = `mutation WorkspaceSyncPush($input: PushWorkspaceSyncInput!) {
|
|
27
34
|
pushWorkspaceSync(input: $input) {
|
|
28
35
|
id
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
36
|
+
state
|
|
37
|
+
steps {
|
|
38
|
+
phase
|
|
39
|
+
status
|
|
40
|
+
elapsedMs
|
|
41
|
+
}
|
|
42
|
+
verdict {
|
|
43
|
+
id
|
|
44
|
+
accepted
|
|
45
|
+
servingSha
|
|
46
|
+
gateMessages
|
|
47
|
+
runtimeError
|
|
48
|
+
seoFindings {
|
|
49
|
+
code
|
|
50
|
+
severity
|
|
51
|
+
url
|
|
52
|
+
detail
|
|
53
|
+
}
|
|
54
|
+
envFiles {
|
|
55
|
+
file
|
|
56
|
+
merged
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}`;
|
|
61
|
+
/** The SDK's `WorkspaceSyncRetrieveRound` operation, verbatim: the round a push answered, as it stands. */
|
|
62
|
+
const ROUND_DOCUMENT = `query WorkspaceSyncRetrieveRound($id: ID!) {
|
|
63
|
+
workspaceSyncRound(id: $id) {
|
|
64
|
+
id
|
|
65
|
+
state
|
|
66
|
+
steps {
|
|
67
|
+
phase
|
|
68
|
+
status
|
|
69
|
+
elapsedMs
|
|
70
|
+
}
|
|
71
|
+
verdict {
|
|
72
|
+
id
|
|
73
|
+
accepted
|
|
74
|
+
servingSha
|
|
75
|
+
gateMessages
|
|
76
|
+
runtimeError
|
|
77
|
+
seoFindings {
|
|
78
|
+
code
|
|
79
|
+
severity
|
|
80
|
+
url
|
|
81
|
+
detail
|
|
82
|
+
}
|
|
83
|
+
envFiles {
|
|
84
|
+
file
|
|
85
|
+
merged
|
|
86
|
+
}
|
|
37
87
|
}
|
|
38
88
|
}
|
|
39
89
|
}`;
|
|
90
|
+
/** The root fields a push and a read of its round select. */
|
|
91
|
+
const PUSH_FIELD = 'pushWorkspaceSync';
|
|
92
|
+
const ROUND_FIELD = 'workspaceSyncRound';
|
|
93
|
+
/**
|
|
94
|
+
* The code GraphQL answers a document selecting what the schema lacks with: an
|
|
95
|
+
* admin API from before the round (factory task 2481) answers it to a push.
|
|
96
|
+
*/
|
|
97
|
+
const VALIDATION_FAILED_CODE = 'GRAPHQL_VALIDATION_FAILED';
|
|
40
98
|
/** The SDK's `WorkspaceSyncBeginUpload` operation, verbatim. */
|
|
41
99
|
const BEGIN_DOCUMENT = `mutation WorkspaceSyncBeginUpload($input: BeginWorkspaceSyncUploadInput!) {
|
|
42
100
|
beginWorkspaceSyncUpload(input: $input) {
|
|
@@ -79,6 +137,12 @@ const PART_FILE_NAME = 'serving.bundle.part';
|
|
|
79
137
|
* credential step answers by authorizing. A workspace updating to the current
|
|
80
138
|
* version (#2371) is no refusal: every operation waits it out and asks again
|
|
81
139
|
* ({@link SyncImageUpdateWait}).
|
|
140
|
+
*
|
|
141
|
+
* A push answers the round it lands as, and the round is read until it ended
|
|
142
|
+
* (factory task 2481): no request waits on the landing. An admin API from
|
|
143
|
+
* before the round answers the push's selection GRAPHQL_VALIDATION_FAILED, and
|
|
144
|
+
* the run is refused naming this CLI's version and the operation it misses —
|
|
145
|
+
* nothing lands on it.
|
|
82
146
|
*/
|
|
83
147
|
export class WorkspaceSyncClient {
|
|
84
148
|
api;
|
|
@@ -115,9 +179,14 @@ export class WorkspaceSyncClient {
|
|
|
115
179
|
* Push `bundle` — the laptop's tree as one commit cut on the serving tip — as
|
|
116
180
|
* an authenticated upload, with the merchant lines of the env files
|
|
117
181
|
* `envFiles`, file by file; a push without a bundle carries the lines alone.
|
|
182
|
+
* Answers the round the push lands as.
|
|
118
183
|
*/
|
|
119
184
|
async push(bundle, envFiles) {
|
|
120
|
-
return this.
|
|
185
|
+
return this.roundOf(this.pushRequest(bundle, envFiles), PUSH_FIELD);
|
|
186
|
+
}
|
|
187
|
+
/** The round `roundId` a push answered, as it stands. */
|
|
188
|
+
async round(roundId) {
|
|
189
|
+
return this.roundOf(this.api.jsonRequest(ROUND_DOCUMENT, { id: roundId }), ROUND_FIELD);
|
|
121
190
|
}
|
|
122
191
|
/**
|
|
123
192
|
* Begin the chunked push of the bundle `declaration` declares — or, for a
|
|
@@ -137,21 +206,72 @@ export class WorkspaceSyncClient {
|
|
|
137
206
|
/**
|
|
138
207
|
* Land the parts the upload `uploadId` holds as ONE bundle, with the
|
|
139
208
|
* merchant lines of the env files `envFiles`, file by file — a push naming
|
|
140
|
-
* its upload, carrying no bundle.
|
|
209
|
+
* its upload, carrying no bundle. Answers the round the push lands as.
|
|
141
210
|
*/
|
|
142
211
|
async land(uploadId, envFiles) {
|
|
143
|
-
return this.
|
|
212
|
+
return this.roundOf(this.api.jsonRequest(PUSH_DOCUMENT, { input: { uploadId, envFiles } }), PUSH_FIELD);
|
|
213
|
+
}
|
|
214
|
+
/**
|
|
215
|
+
* Send a push or a read of its round, and read the round it answers. An
|
|
216
|
+
* admin API that offers no round refuses the run, naming what it misses.
|
|
217
|
+
*/
|
|
218
|
+
async roundOf(request, field) {
|
|
219
|
+
try {
|
|
220
|
+
return this.readRound(await this.exchange(request, field));
|
|
221
|
+
}
|
|
222
|
+
catch (error) {
|
|
223
|
+
if (!(error instanceof AdminApiRefusal) || error.code !== VALIDATION_FAILED_CODE)
|
|
224
|
+
throw error;
|
|
225
|
+
throw this.roundless(error);
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
/** The refusal of an admin API from before the round: this CLI's version, and the operation it misses. */
|
|
229
|
+
roundless(refusal) {
|
|
230
|
+
return new CliRefusal(CliExitLadder.error, [
|
|
231
|
+
`forgecart ${readPackageVersion()} needs the admin API's ${ROUND_FIELD}, which every push lands through; the admin API at ${refusal.endpoint} does not offer it (${refusal.code} on ${refusal.field}). Update the shop, then sync again — nothing landed.`,
|
|
232
|
+
]);
|
|
233
|
+
}
|
|
234
|
+
/** A push's round, read field by field: its verdict once it ended, null while it runs. */
|
|
235
|
+
readRound(result) {
|
|
236
|
+
const verdict = result.readNullableObject('verdict');
|
|
237
|
+
return {
|
|
238
|
+
id: result.readString('id'),
|
|
239
|
+
state: result.readMember('state', (value) => this.isRoundState(value)),
|
|
240
|
+
steps: result.readObjectList('steps').map((step) => this.readStep(step)),
|
|
241
|
+
verdict: verdict === null ? null : this.readVerdict(verdict),
|
|
242
|
+
};
|
|
243
|
+
}
|
|
244
|
+
/** One step of a round: its phase by name, how it stands, and its elapsed milliseconds once it ended. */
|
|
245
|
+
readStep(step) {
|
|
246
|
+
return {
|
|
247
|
+
phase: step.readString('phase'),
|
|
248
|
+
status: step.readMember('status', (value) => this.isStepStatus(value)),
|
|
249
|
+
elapsedMs: step.readNullableCount('elapsedMs'),
|
|
250
|
+
};
|
|
251
|
+
}
|
|
252
|
+
isRoundState(value) {
|
|
253
|
+
return Object.values(SyncPushRoundState).includes(value);
|
|
254
|
+
}
|
|
255
|
+
isStepStatus(value) {
|
|
256
|
+
return Object.values(SyncPushRoundStepStatus).includes(value);
|
|
257
|
+
}
|
|
258
|
+
/** What a push did, read off the verdict its round ended on. */
|
|
259
|
+
readVerdict(verdict) {
|
|
260
|
+
return {
|
|
261
|
+
accepted: verdict.readBoolean('accepted'),
|
|
262
|
+
servingSha: verdict.readNullableString('servingSha'),
|
|
263
|
+
gateMessages: verdict.readStringList('gateMessages'),
|
|
264
|
+
runtimeError: verdict.readNullableString('runtimeError'),
|
|
265
|
+
seoFindings: verdict.readObjectList('seoFindings').map((finding) => this.readFinding(finding)),
|
|
266
|
+
envFiles: this.readEnvMerges(verdict),
|
|
267
|
+
};
|
|
144
268
|
}
|
|
145
|
-
|
|
146
|
-
async pushed(request) {
|
|
147
|
-
const result = await this.exchange(request, 'pushWorkspaceSync');
|
|
269
|
+
readFinding(finding) {
|
|
148
270
|
return {
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
seoFindings: result.readStringList('seoFindings'),
|
|
154
|
-
envFiles: this.readEnvMerges(result),
|
|
271
|
+
code: finding.readString('code'),
|
|
272
|
+
severity: finding.readString('severity'),
|
|
273
|
+
url: finding.readText('url'),
|
|
274
|
+
detail: finding.readText('detail'),
|
|
155
275
|
};
|
|
156
276
|
}
|
|
157
277
|
/** A push with a bundle is an upload; one with env lines alone is plain JSON. */
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"workspace-sync.client.js","sourceRoot":"","sources":["../../../src/sync/workspace-sync.client.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;
|
|
1
|
+
{"version":3,"file":"workspace-sync.client.js","sourceRoot":"","sources":["../../../src/sync/workspace-sync.client.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -140,10 +140,49 @@ defers its first flush so a fast add-to-cart wins the first-touch race. The
|
|
|
140
140
|
mirror is what the browser's shop socket boots its auth from: written without
|
|
141
141
|
it, a relay-established session would never reach the client at all.
|
|
142
142
|
|
|
143
|
+
## Customer account
|
|
144
|
+
|
|
145
|
+
The scaffold ships the shopper's account surface, so a store starts with
|
|
146
|
+
sign-in, sign-out and registration working — extend it rather than rebuild it:
|
|
147
|
+
|
|
148
|
+
| Route | What it does |
|
|
149
|
+
| ----------- | ------------------------------------------------------------ |
|
|
150
|
+
| `/account` | signs a shopper in, greets them by name, signs them out |
|
|
151
|
+
| `/register` | creates an account; its confirmation mail links to `/verify` |
|
|
152
|
+
|
|
153
|
+
The header links `/account`, the footer `/register`. Five rules keep it
|
|
154
|
+
working — each one was learned from a store that broke it:
|
|
155
|
+
|
|
156
|
+
- **Account pages are static shells.**
|
|
157
|
+
[`src/app/account/page.tsx`](src/app/account/page.tsx) reads no session on
|
|
158
|
+
the server — no `next/headers`, no `connection()`, no session module. The
|
|
159
|
+
session lives on the shopper's own socket, in the `AccountPanel` client
|
|
160
|
+
island, which waits for the `CartProvider` to arm it (`useCart().hydrated`).
|
|
161
|
+
A session read on the server path throws during the render and 500s the page.
|
|
162
|
+
- **Sign in and out only through the session helpers.** `loginCustomer` and
|
|
163
|
+
`logoutCustomer` ([`src/lib/account-session.ts`](src/lib/account-session.ts))
|
|
164
|
+
replace the socket with one carrying the new session (or none) and write both
|
|
165
|
+
session cookies before they resolve: a socket left on the session it opened
|
|
166
|
+
with refuses the very next call. `findSignedInCustomer` says who is signed in.
|
|
167
|
+
- **Every refusal renders through `shopperMessage`**
|
|
168
|
+
([`src/lib/error-messages.ts`](src/lib/error-messages.ts)): it has copy for
|
|
169
|
+
every account refusal the shop API answers, and appends the raw code to its
|
|
170
|
+
generic line for any code it does not know. A new refusal's copy goes there,
|
|
171
|
+
once.
|
|
172
|
+
- **Registration requires the password.** The API treats it as optional, and an
|
|
173
|
+
account registered without one can never sign in.
|
|
174
|
+
- **`lib/shop-config` is server-only**, so no client component imports it, and
|
|
175
|
+
**the header links only routes the template ships**.
|
|
176
|
+
|
|
177
|
+
A store scaffolded before the surface shipped adopts it with `forgecart
|
|
178
|
+
refresh`: the account files it never had are rendered, the files it never
|
|
179
|
+
edited are replaced, and a file it edited — its `Header.tsx`, say — is reported
|
|
180
|
+
as a conflict and left as it is.
|
|
181
|
+
|
|
143
182
|
## Account links
|
|
144
183
|
|
|
145
184
|
Two routes exist because the PLATFORM links to them from the e-mails it sends
|
|
146
|
-
a shopper
|
|
185
|
+
a shopper:
|
|
147
186
|
|
|
148
187
|
| Route | Mail | What it does |
|
|
149
188
|
| ---------------------------- | ------------------- | ------------------------------------------------------------ |
|
|
@@ -178,10 +217,13 @@ src/
|
|
|
178
217
|
cart-actions.ts # cart Server Actions (incl. coupons)
|
|
179
218
|
checkout-actions.ts # checkout Server Actions
|
|
180
219
|
cart-context.tsx # client cart state + typed error channel
|
|
181
|
-
|
|
182
|
-
account
|
|
220
|
+
shop-session.ts # the shopper's own socket + session cookie sync
|
|
221
|
+
account-session.ts # register, sign-in, sign-out on that socket
|
|
222
|
+
account-actions.ts # password-reset + verification-resend Server Actions
|
|
223
|
+
account/ # pure account rules (token, states, ErrorCode map)
|
|
224
|
+
error-messages.ts # shopper copy for every typed error code
|
|
183
225
|
components/
|
|
184
|
-
Header.tsx # top nav
|
|
226
|
+
Header.tsx # top nav (Home / Products / Cart / Account) + cart count
|
|
185
227
|
ProductCard.tsx # product tile
|
|
186
228
|
ProductPurchase.tsx # variant picker + purchase options + add-to-cart
|
|
187
229
|
CartView.tsx # cart lines, coupons, totals, checkout CTA
|
|
@@ -191,7 +233,10 @@ src/
|
|
|
191
233
|
RatesStep.tsx # grouped radio-card rate selection
|
|
192
234
|
PaymentFormEmbed.tsx # origin-checked hosted-form host
|
|
193
235
|
account/
|
|
194
|
-
|
|
236
|
+
AccountPanel.tsx # sign-in form, greeting, sign-out (/account)
|
|
237
|
+
RegisterForm.tsx # create an account (/register)
|
|
238
|
+
VerificationResend.tsx # send the confirmation mail again
|
|
239
|
+
AccountMessage.tsx # the one outcome block the account pages use
|
|
195
240
|
ResetPasswordForm.tsx # set a new password from a reset token
|
|
196
241
|
RequestPasswordResetForm.tsx # ask for a reset link
|
|
197
242
|
app/
|
|
@@ -202,6 +247,8 @@ src/
|
|
|
202
247
|
products/ # grid + [slug] detail
|
|
203
248
|
cart/page.tsx # cart
|
|
204
249
|
checkout/page.tsx # the stepped checkout
|
|
250
|
+
account/page.tsx # sign-in + greeting: a static shell around AccountPanel
|
|
251
|
+
register/page.tsx # create an account
|
|
205
252
|
verify/page.tsx # e-mail verification link target
|
|
206
253
|
reset-password/page.tsx # password-reset link target + request form
|
|
207
254
|
```
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import type { Metadata } from 'next';
|
|
2
|
+
|
|
3
|
+
import { AccountPanel } from '../../components/account/AccountPanel';
|
|
4
|
+
import { getRequestLocale } from '../../lib/locale/request-binding';
|
|
5
|
+
import { routeMetadata } from '../../lib/seo/metadata';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* `/account` — where a shopper signs in, is greeted, and signs out (#1093).
|
|
9
|
+
*
|
|
10
|
+
* A STATIC SHELL, and that is the law this file exists to keep: the server
|
|
11
|
+
* reads no session here — no cookie, no `connection()`, no session module. A
|
|
12
|
+
* storefront agent once read the session on the server path; the render threw
|
|
13
|
+
* `EmptyError: no elements in sequence`, and the 500 made the promote gate roll
|
|
14
|
+
* back the whole changeset. The session is the shopper's, on their own socket:
|
|
15
|
+
* `AccountPanel` asks it in the browser, and the server paints the skeleton it
|
|
16
|
+
* starts from. `tool/storefront-template-spec` holds this file to that.
|
|
17
|
+
*
|
|
18
|
+
* Locale-prefixable like `/register` — a shopper addresses it from a link on
|
|
19
|
+
* this storefront, never out of a mail — so `account` is absent from the
|
|
20
|
+
* grammar's unprefixed set and `/de/account` serves the German render.
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
const PAGE_TITLE = 'Your account';
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Noindex via `NOINDEX_PATHS`, and no alternates — for the reasons `/register`'s
|
|
27
|
+
* metadata states: one shopper's own page has nothing for a search result to be
|
|
28
|
+
* about, and a page that declines indexing has no preferred address to claim.
|
|
29
|
+
*/
|
|
30
|
+
export async function generateMetadata(): Promise<Metadata> {
|
|
31
|
+
const { binding, shopName, channelResolved } = await getRequestLocale();
|
|
32
|
+
return routeMetadata({
|
|
33
|
+
binding,
|
|
34
|
+
shopName,
|
|
35
|
+
channelResolved,
|
|
36
|
+
pathname: '/account',
|
|
37
|
+
pathsByLocale: {},
|
|
38
|
+
title: PAGE_TITLE,
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export default async function AccountPage() {
|
|
43
|
+
const { binding } = await getRequestLocale();
|
|
44
|
+
|
|
45
|
+
return (
|
|
46
|
+
<div className="mx-auto max-w-xl space-y-6">
|
|
47
|
+
<h1 className="text-2xl font-bold tracking-tight">{PAGE_TITLE}</h1>
|
|
48
|
+
<AccountPanel locale={binding} />
|
|
49
|
+
</div>
|
|
50
|
+
);
|
|
51
|
+
}
|
|
@@ -136,11 +136,10 @@ export default async function RootLayout({ children }: { children: ReactNode })
|
|
|
136
136
|
<LocaleLink href="/cart" locale={binding} className="link link-hover">
|
|
137
137
|
Cart
|
|
138
138
|
</LocaleLink>
|
|
139
|
-
{/* The
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
exist. A merchant who adds sign-in moves it up. */}
|
|
139
|
+
{/* The direct way to `/register` (#1471). The header's Account
|
|
140
|
+
link (#1093) is the account entry — sign-in, with
|
|
141
|
+
registration one link away — and this is the shortcut for
|
|
142
|
+
a shopper who already knows they are new. */}
|
|
144
143
|
<LocaleLink href="/register" locale={binding} className="link link-hover">
|
|
145
144
|
Create account
|
|
146
145
|
</LocaleLink>
|