@forgecart/cli 2.202610102045.0 → 2.202610110825.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/authorization/cli-unauthorized-refusal.d.ts +10 -4
- package/dist/src/authorization/cli-unauthorized-refusal.js +11 -3
- package/dist/src/authorization/cli-unauthorized-refusal.js.map +1 -1
- package/dist/src/cli.js +4 -0
- package/dist/src/cli.js.map +1 -1
- package/dist/src/commands/init.js +5 -47
- package/dist/src/commands/init.js.map +1 -1
- package/dist/src/commands/sync.d.ts +14 -0
- package/dist/src/commands/sync.js +49 -0
- package/dist/src/commands/sync.js.map +1 -0
- package/dist/src/exit-ladder.d.ts +8 -1
- package/dist/src/exit-ladder.js +7 -0
- package/dist/src/exit-ladder.js.map +1 -1
- package/dist/src/sync/sync-api.error.d.ts +15 -0
- package/dist/src/sync/sync-api.error.js +26 -0
- package/dist/src/sync/sync-api.error.js.map +1 -0
- package/dist/src/sync/sync-divergence.d.ts +43 -0
- package/dist/src/sync/sync-divergence.js +104 -0
- package/dist/src/sync/sync-divergence.js.map +1 -0
- package/dist/src/sync/sync-dotenv.file.d.ts +36 -0
- package/dist/src/sync/sync-dotenv.file.js +95 -0
- package/dist/src/sync/sync-dotenv.file.js.map +1 -0
- package/dist/src/sync/sync-env.runner.d.ts +29 -0
- package/dist/src/sync/sync-env.runner.js +95 -0
- package/dist/src/sync/sync-env.runner.js.map +1 -0
- package/dist/src/sync/sync-record.file.d.ts +28 -0
- package/dist/src/sync/sync-record.file.js +86 -0
- package/dist/src/sync/sync-record.file.js.map +1 -0
- package/dist/src/sync/sync-repository.d.ts +110 -0
- package/dist/src/sync/sync-repository.js +396 -0
- package/dist/src/sync/sync-repository.js.map +1 -0
- package/dist/src/sync/sync-round.d.ts +95 -0
- package/dist/src/sync/sync-round.js +355 -0
- package/dist/src/sync/sync-round.js.map +1 -0
- package/dist/src/sync/sync-watch.d.ts +48 -0
- package/dist/src/sync/sync-watch.js +144 -0
- package/dist/src/sync/sync-watch.js.map +1 -0
- package/dist/src/sync/sync.runner.d.ts +37 -0
- package/dist/src/sync/sync.runner.js +121 -0
- package/dist/src/sync/sync.runner.js.map +1 -0
- package/dist/src/sync/type/sync.types.d.ts +159 -0
- package/dist/src/sync/type/sync.types.js +30 -0
- package/dist/src/sync/type/sync.types.js.map +1 -0
- package/dist/src/sync/workspace-sync.client.d.ts +85 -0
- package/dist/src/sync/workspace-sync.client.js +317 -0
- package/dist/src/sync/workspace-sync.client.js.map +1 -0
- package/dist/src/util/dotenv.util.d.ts +28 -0
- package/dist/src/util/dotenv.util.js +81 -0
- package/dist/src/util/dotenv.util.js.map +1 -0
- package/package.json +1 -1
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { mkdir, stat } from 'node:fs/promises';
|
|
3
|
+
import { resolve } from 'node:path';
|
|
4
|
+
import { CliCredentialCheck } from '../authorization/cli-credential-check.js';
|
|
5
|
+
import { CliUnauthorizedReason, CliUnauthorizedRefusal, } from '../authorization/cli-unauthorized-refusal.js';
|
|
6
|
+
import { print } from '../output.js';
|
|
7
|
+
import { QuestionPromptAdapter } from '../question/question-prompt.adapter.js';
|
|
8
|
+
import { SyncDivergenceSelection } from './sync-divergence.js';
|
|
9
|
+
import { SyncDotenvFile } from './sync-dotenv.file.js';
|
|
10
|
+
import { SyncRecordFile } from './sync-record.file.js';
|
|
11
|
+
import { SyncRepository } from './sync-repository.js';
|
|
12
|
+
import { SyncRound } from './sync-round.js';
|
|
13
|
+
import { SyncWatch } from './sync-watch.js';
|
|
14
|
+
import { WorkspaceSyncClient } from './workspace-sync.client.js';
|
|
15
|
+
/**
|
|
16
|
+
* `forgecart sync [name]` (#2781): keep the storefront's tree and the board's
|
|
17
|
+
* serving tree the same tree. Before anything leaves the machine the run
|
|
18
|
+
* settles its four credentials — the environment, then the storefront's `.env`,
|
|
19
|
+
* then its `.forgecart/config.json`, exactly as `init` reads them; a key in no
|
|
20
|
+
* source exits 3 with nothing sent — and needs `git` on the PATH. Then it runs
|
|
21
|
+
* ONE round, or, with `--watch`, a round whenever a side moves until Ctrl-C.
|
|
22
|
+
*/
|
|
23
|
+
export class SyncRunner {
|
|
24
|
+
name;
|
|
25
|
+
options;
|
|
26
|
+
/**
|
|
27
|
+
* @param name the storefront directory as given — the current directory when absent.
|
|
28
|
+
* @param options the run's flags.
|
|
29
|
+
*/
|
|
30
|
+
constructor(name, options) {
|
|
31
|
+
this.name = name;
|
|
32
|
+
this.options = options;
|
|
33
|
+
}
|
|
34
|
+
async run() {
|
|
35
|
+
this.assertFlags();
|
|
36
|
+
const storefront = this.name ? resolve(process.cwd(), this.name) : process.cwd();
|
|
37
|
+
const credentials = await this.settleCredentials(storefront);
|
|
38
|
+
await SyncRepository.assertGitOnPath();
|
|
39
|
+
await this.assertStorefront(storefront);
|
|
40
|
+
if (this.options.watch === true) {
|
|
41
|
+
await this.watch(storefront, credentials);
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
const { round } = this.assemble(storefront, credentials, new AbortController().signal);
|
|
45
|
+
await round.run();
|
|
46
|
+
}
|
|
47
|
+
/** `--dry-run` plans one round and moves nothing; it cannot watch. */
|
|
48
|
+
assertFlags() {
|
|
49
|
+
if (this.options.dryRun !== true || this.options.watch !== true)
|
|
50
|
+
return;
|
|
51
|
+
throw new Error('--dry-run plans one round and moves nothing, so it cannot --watch.');
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* The four credentials, from the first source carrying each. With one in no
|
|
55
|
+
* source the run prints where it looked — key names, never values — and exits
|
|
56
|
+
* 3 before any request.
|
|
57
|
+
*/
|
|
58
|
+
async settleCredentials(storefront) {
|
|
59
|
+
const check = await CliCredentialCheck.read(storefront);
|
|
60
|
+
const credentials = check.credentials();
|
|
61
|
+
if (credentials !== undefined)
|
|
62
|
+
return credentials;
|
|
63
|
+
for (const line of check.report())
|
|
64
|
+
print(line);
|
|
65
|
+
throw new CliUnauthorizedRefusal(CliUnauthorizedReason.MISSING);
|
|
66
|
+
}
|
|
67
|
+
/** The storefront must be a directory — `--pull` makes a new one to take the workspace's tree into. */
|
|
68
|
+
async assertStorefront(storefront) {
|
|
69
|
+
if (await this.isDirectory(storefront))
|
|
70
|
+
return;
|
|
71
|
+
if (this.options.pull === true) {
|
|
72
|
+
await mkdir(storefront, { recursive: true });
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
throw new Error(`No storefront at ${storefront}: name an existing directory, or pass --pull to take the workspace's tree into a new one.`);
|
|
76
|
+
}
|
|
77
|
+
/** Watch until Ctrl-C, which ends the run in flight and prints where it stood. */
|
|
78
|
+
async watch(storefront, credentials) {
|
|
79
|
+
const interrupt = new AbortController();
|
|
80
|
+
const { client, repository, round } = this.assemble(storefront, credentials, interrupt.signal);
|
|
81
|
+
const watch = new SyncWatch(this.displayName(), round, repository, client);
|
|
82
|
+
const stop = () => {
|
|
83
|
+
interrupt.abort();
|
|
84
|
+
};
|
|
85
|
+
process.once('SIGINT', stop);
|
|
86
|
+
try {
|
|
87
|
+
await watch.run(interrupt.signal);
|
|
88
|
+
}
|
|
89
|
+
finally {
|
|
90
|
+
process.off('SIGINT', stop);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
assemble(storefront, credentials, interrupt) {
|
|
94
|
+
const pullOnly = this.options.pull === true;
|
|
95
|
+
const client = new WorkspaceSyncClient(credentials, this.options.timeout, interrupt);
|
|
96
|
+
const repository = new SyncRepository(storefront);
|
|
97
|
+
const round = new SyncRound({ storefront: this.displayName(), pullOnly, dryRun: this.options.dryRun === true }, credentials, client, repository, new SyncRecordFile(storefront, this.channelOf(credentials)), new SyncDotenvFile(storefront), new SyncDivergenceSelection(new QuestionPromptAdapter(this.options), pullOnly));
|
|
98
|
+
return { client, repository, round };
|
|
99
|
+
}
|
|
100
|
+
/** The channel a record belongs to: a digest of the admin API and the channel token — never the token. */
|
|
101
|
+
channelOf(credentials) {
|
|
102
|
+
return createHash('sha256')
|
|
103
|
+
.update(`${credentials.adminApiUrl}\n${credentials.channelToken}`)
|
|
104
|
+
.digest('hex');
|
|
105
|
+
}
|
|
106
|
+
/** The storefront as the merchant named it. */
|
|
107
|
+
displayName() {
|
|
108
|
+
return this.name ?? '.';
|
|
109
|
+
}
|
|
110
|
+
async isDirectory(path) {
|
|
111
|
+
try {
|
|
112
|
+
return (await stat(path)).isDirectory();
|
|
113
|
+
}
|
|
114
|
+
catch (error) {
|
|
115
|
+
if (error.code === 'ENOENT')
|
|
116
|
+
return false;
|
|
117
|
+
throw error;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
//# sourceMappingURL=sync.runner.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sync.runner.js","sourceRoot":"","sources":["../../../src/sync/sync.runner.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
import type { QuestionPromptInput } from '../../question/type/question-prompt.input.js';
|
|
2
|
+
/**
|
|
3
|
+
* `forgecart sync`'s options as commander parses them (#2781), beside the three
|
|
4
|
+
* flags every questioning command carries — they answer the divergence
|
|
5
|
+
* selection. No option carries a token, a secret or a URL: the credentials are
|
|
6
|
+
* the four `FORGECART_*` keys the credential check settles.
|
|
7
|
+
*/
|
|
8
|
+
export interface SyncOptions extends QuestionPromptInput {
|
|
9
|
+
/** `--pull`: take the workspace's tree onto this laptop; never push. */
|
|
10
|
+
pull?: boolean;
|
|
11
|
+
/** `--watch`: keep syncing — push local edits, pull workspace landings — until interrupted. */
|
|
12
|
+
watch?: boolean;
|
|
13
|
+
/** `--dry-run`: print the plan and the paths a push would send; move nothing. */
|
|
14
|
+
dryRun?: boolean;
|
|
15
|
+
/** `--timeout <seconds>`: the longest one exchange with the admin API may take. */
|
|
16
|
+
timeout: number;
|
|
17
|
+
}
|
|
18
|
+
/** `forgecart sync-env`'s option: merge instead of read. */
|
|
19
|
+
export interface SyncEnvOptions {
|
|
20
|
+
merge?: boolean;
|
|
21
|
+
}
|
|
22
|
+
/** What `forgecart sync-env` answers — the report the admin API parses. */
|
|
23
|
+
export interface SyncEnvReport {
|
|
24
|
+
/** The merchant lines of the storefront's `.env` once the run is done. */
|
|
25
|
+
lines: string[];
|
|
26
|
+
/** How many merchant keys a merge carried in; 0 for a read. */
|
|
27
|
+
merged: number;
|
|
28
|
+
}
|
|
29
|
+
/** The board's serving branch, as the admin API reads it from the pod. */
|
|
30
|
+
export interface SyncServingState {
|
|
31
|
+
servingSha: string;
|
|
32
|
+
}
|
|
33
|
+
/** A pulled bundle whose bytes matched their declared length and sha256. */
|
|
34
|
+
export interface SyncPulledBundle {
|
|
35
|
+
/** The commit it is cut from; empty for the whole board. */
|
|
36
|
+
sinceSha: string;
|
|
37
|
+
bytes: Buffer;
|
|
38
|
+
/** The merchant lines of the workspace's `.env`. */
|
|
39
|
+
envLines: string[];
|
|
40
|
+
}
|
|
41
|
+
/** What the admin API answered a push. */
|
|
42
|
+
export interface SyncPushResult {
|
|
43
|
+
accepted: boolean;
|
|
44
|
+
servingSha: string | null;
|
|
45
|
+
gateMessages: string[];
|
|
46
|
+
runtimeError: string | null;
|
|
47
|
+
seoFindings: string[];
|
|
48
|
+
envKeys: number;
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* The last sync, recorded under `.forgecart/sync/`: the channel it was against,
|
|
52
|
+
* the serving commit the laptop's tree last matched, and a digest of every
|
|
53
|
+
* merchant `.env` line as it last travelled.
|
|
54
|
+
*/
|
|
55
|
+
export interface SyncRecord {
|
|
56
|
+
channel: string;
|
|
57
|
+
servingSha: string;
|
|
58
|
+
env: Record<string, string>;
|
|
59
|
+
}
|
|
60
|
+
/** How a path differs between two trees (`git diff-tree`'s status letters). */
|
|
61
|
+
export declare enum SyncChangeKind {
|
|
62
|
+
ADDED = "A",
|
|
63
|
+
DELETED = "D",
|
|
64
|
+
MODIFIED = "M",
|
|
65
|
+
TYPE_CHANGED = "T"
|
|
66
|
+
}
|
|
67
|
+
/** One path a change between two trees touches. */
|
|
68
|
+
export interface SyncTreeChange {
|
|
69
|
+
kind: SyncChangeKind;
|
|
70
|
+
path: string;
|
|
71
|
+
}
|
|
72
|
+
/** What writing a tree onto the laptop did. */
|
|
73
|
+
export interface SyncMaterialized {
|
|
74
|
+
written: number;
|
|
75
|
+
removed: number;
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* The serving branch, held in the private repository: its commit, the tree of
|
|
79
|
+
* it that travels, and the merchant `.env` lines its bundle carried — null when
|
|
80
|
+
* the commit was already held and no bundle was read.
|
|
81
|
+
*/
|
|
82
|
+
export interface SyncServing {
|
|
83
|
+
sha: string;
|
|
84
|
+
tree: string;
|
|
85
|
+
envLines: string[] | null;
|
|
86
|
+
}
|
|
87
|
+
/** A round's view: the record, and the three trees it compares. */
|
|
88
|
+
export interface SyncView {
|
|
89
|
+
record: SyncRecord | null;
|
|
90
|
+
/** The travelling tree of the recorded base — the empty tree for a first sync. */
|
|
91
|
+
base: string;
|
|
92
|
+
local: string;
|
|
93
|
+
serving: SyncServing;
|
|
94
|
+
/** The paths each side changed since the base. */
|
|
95
|
+
localChanges: SyncTreeChange[];
|
|
96
|
+
workspaceChanges: SyncTreeChange[];
|
|
97
|
+
}
|
|
98
|
+
/** Where a round left the laptop: its tree, the serving commit it stands on, and the round in words. */
|
|
99
|
+
export interface SyncRoundEnd {
|
|
100
|
+
localTree: string;
|
|
101
|
+
servingSha: string;
|
|
102
|
+
summary: string;
|
|
103
|
+
}
|
|
104
|
+
/** What a round decides from its view. */
|
|
105
|
+
export declare enum SyncVerdict {
|
|
106
|
+
/** The laptop's tree is the serving tree. */
|
|
107
|
+
IN_SYNC = "in-sync",
|
|
108
|
+
/** Only the workspace changed: its tree is written onto the laptop. */
|
|
109
|
+
PULL = "pull",
|
|
110
|
+
/** Only the laptop changed: its tree lands on the serving tip. */
|
|
111
|
+
PUSH = "push",
|
|
112
|
+
/** Only the laptop changed, and the run never pushes (`--pull`). */
|
|
113
|
+
KEEP_LOCAL = "keep-local",
|
|
114
|
+
/** Both changed: the selection decides which tree wins. */
|
|
115
|
+
DIVERGED = "diverged"
|
|
116
|
+
}
|
|
117
|
+
/** One attempt of a round: settled, or a push the board moved past while it landed. */
|
|
118
|
+
export type SyncAttempt = {
|
|
119
|
+
kind: 'settled';
|
|
120
|
+
end: SyncRoundEnd;
|
|
121
|
+
} | {
|
|
122
|
+
kind: 'moved';
|
|
123
|
+
point: SyncDivergencePoint;
|
|
124
|
+
};
|
|
125
|
+
/** The `.env` lines a round sent alone: how many keys the workspace merged, and the digests to record. */
|
|
126
|
+
export interface SyncSentEnv {
|
|
127
|
+
merged: number;
|
|
128
|
+
digests: Record<string, string>;
|
|
129
|
+
}
|
|
130
|
+
/** What one round needs to know of its run. */
|
|
131
|
+
export interface SyncRoundOptions {
|
|
132
|
+
/** The storefront directory, as the run names it. */
|
|
133
|
+
storefront: string;
|
|
134
|
+
/** `--pull`: the round never pushes. */
|
|
135
|
+
pullOnly: boolean;
|
|
136
|
+
/** `--dry-run`: the round plans and moves nothing. */
|
|
137
|
+
dryRun: boolean;
|
|
138
|
+
}
|
|
139
|
+
/** The merchant `.env` lines a push carries, and the digests the record takes once they landed. */
|
|
140
|
+
export interface SyncOutgoingEnv {
|
|
141
|
+
lines: string[];
|
|
142
|
+
digests: Record<string, string>;
|
|
143
|
+
}
|
|
144
|
+
/** What taking the workspace's `.env` lines did: the keys taken, and the digests the record takes. */
|
|
145
|
+
export interface SyncTakenEnv {
|
|
146
|
+
taken: number;
|
|
147
|
+
digests: Record<string, string>;
|
|
148
|
+
}
|
|
149
|
+
/** The selection a divergence asks: which tree overrides the other, or neither. */
|
|
150
|
+
export declare enum SyncDivergenceChoice {
|
|
151
|
+
LOCAL = "local",
|
|
152
|
+
WORKSPACE = "workspace",
|
|
153
|
+
ABORT = "abort"
|
|
154
|
+
}
|
|
155
|
+
/** Where a divergence left both sides: the laptop's tree and the serving commit it diverged from. */
|
|
156
|
+
export interface SyncDivergencePoint {
|
|
157
|
+
localTree: string;
|
|
158
|
+
servingSha: string;
|
|
159
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/** How a path differs between two trees (`git diff-tree`'s status letters). */
|
|
2
|
+
export var SyncChangeKind;
|
|
3
|
+
(function (SyncChangeKind) {
|
|
4
|
+
SyncChangeKind["ADDED"] = "A";
|
|
5
|
+
SyncChangeKind["DELETED"] = "D";
|
|
6
|
+
SyncChangeKind["MODIFIED"] = "M";
|
|
7
|
+
SyncChangeKind["TYPE_CHANGED"] = "T";
|
|
8
|
+
})(SyncChangeKind || (SyncChangeKind = {}));
|
|
9
|
+
/** What a round decides from its view. */
|
|
10
|
+
export var SyncVerdict;
|
|
11
|
+
(function (SyncVerdict) {
|
|
12
|
+
/** The laptop's tree is the serving tree. */
|
|
13
|
+
SyncVerdict["IN_SYNC"] = "in-sync";
|
|
14
|
+
/** Only the workspace changed: its tree is written onto the laptop. */
|
|
15
|
+
SyncVerdict["PULL"] = "pull";
|
|
16
|
+
/** Only the laptop changed: its tree lands on the serving tip. */
|
|
17
|
+
SyncVerdict["PUSH"] = "push";
|
|
18
|
+
/** Only the laptop changed, and the run never pushes (`--pull`). */
|
|
19
|
+
SyncVerdict["KEEP_LOCAL"] = "keep-local";
|
|
20
|
+
/** Both changed: the selection decides which tree wins. */
|
|
21
|
+
SyncVerdict["DIVERGED"] = "diverged";
|
|
22
|
+
})(SyncVerdict || (SyncVerdict = {}));
|
|
23
|
+
/** The selection a divergence asks: which tree overrides the other, or neither. */
|
|
24
|
+
export var SyncDivergenceChoice;
|
|
25
|
+
(function (SyncDivergenceChoice) {
|
|
26
|
+
SyncDivergenceChoice["LOCAL"] = "local";
|
|
27
|
+
SyncDivergenceChoice["WORKSPACE"] = "workspace";
|
|
28
|
+
SyncDivergenceChoice["ABORT"] = "abort";
|
|
29
|
+
})(SyncDivergenceChoice || (SyncDivergenceChoice = {}));
|
|
30
|
+
//# sourceMappingURL=sync.types.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sync.types.js","sourceRoot":"","sources":["../../../../src/sync/type/sync.types.ts"],"names":[],"mappings":"AAmEA,+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;AAmCD,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"}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import type { CliCredentials } from '../authorization/type/cli-authorization.types.js';
|
|
2
|
+
import type { SyncPulledBundle, SyncPushResult, SyncServingState } from './type/sync.types.js';
|
|
3
|
+
/**
|
|
4
|
+
* The admin API's three workspace-sync operations (#2781), over GraphQL on
|
|
5
|
+
* HTTP with the channel token and the admin secret as the `forgecart-token` and
|
|
6
|
+
* `forgecart-secret` headers — the one surface `forgecart sync` reaches the
|
|
7
|
+
* shop through. Every request goes to FORGECART_ADMIN_API_URL as given, and
|
|
8
|
+
* every exchange is bounded by `--timeout` and ends when the run is
|
|
9
|
+
* interrupted. Answers are read field by field: one the CLI cannot read is an
|
|
10
|
+
* error naming the endpoint, never a guess. Credentials the API refuses end the
|
|
11
|
+
* run on exit 3; any other refusal is a {@link SyncApiError} carrying its code.
|
|
12
|
+
*/
|
|
13
|
+
export declare class WorkspaceSyncClient {
|
|
14
|
+
private readonly credentials;
|
|
15
|
+
private readonly timeoutSeconds;
|
|
16
|
+
private readonly interrupt;
|
|
17
|
+
/**
|
|
18
|
+
* @param credentials the four credentials the run settled.
|
|
19
|
+
* @param timeoutSeconds the bound on one exchange — `--timeout`.
|
|
20
|
+
* @param interrupt ends every exchange in flight when the run is interrupted.
|
|
21
|
+
*/
|
|
22
|
+
constructor(credentials: CliCredentials, timeoutSeconds: number, interrupt: AbortSignal);
|
|
23
|
+
/** The board's serving commit, read from the pod. */
|
|
24
|
+
state(): Promise<SyncServingState>;
|
|
25
|
+
/**
|
|
26
|
+
* The serving branch since `sinceSha` — all of it for `''` — as ONE git
|
|
27
|
+
* bundle, checked against its declared byte length and sha256 before anything
|
|
28
|
+
* reads it: a short or mismatched bundle is an error, and nothing is written.
|
|
29
|
+
*/
|
|
30
|
+
bundle(sinceSha: string): Promise<SyncPulledBundle>;
|
|
31
|
+
/**
|
|
32
|
+
* Push `bundle` — the laptop's tree as one commit cut on the serving tip — as
|
|
33
|
+
* an authenticated upload, with the merchant `.env` lines `envLines`; a push
|
|
34
|
+
* without a bundle carries the lines alone.
|
|
35
|
+
*/
|
|
36
|
+
push(bundle: Buffer | null, envLines: readonly string[]): Promise<SyncPushResult>;
|
|
37
|
+
/** A push with a bundle is an upload; one with `.env` lines alone is plain JSON. */
|
|
38
|
+
private pushRequest;
|
|
39
|
+
/** A GraphQL request as JSON. */
|
|
40
|
+
private jsonRequest;
|
|
41
|
+
/**
|
|
42
|
+
* The push as a GraphQL multipart request: the operation, the map naming the
|
|
43
|
+
* bundle's variable, and the bundle itself, declared by its byte length and
|
|
44
|
+
* sha256. The admin API reads a multipart request only with the preflight
|
|
45
|
+
* header; the boundary is the form's own.
|
|
46
|
+
*/
|
|
47
|
+
private uploadRequest;
|
|
48
|
+
private authHeaders;
|
|
49
|
+
/**
|
|
50
|
+
* POST one operation and answer its root field's object. The exchange — the
|
|
51
|
+
* response and its whole body — is bounded by `--timeout` and ends with the
|
|
52
|
+
* run's interrupt.
|
|
53
|
+
*/
|
|
54
|
+
private exchange;
|
|
55
|
+
private post;
|
|
56
|
+
/** Why an exchange ended without an answer: the run's interrupt, `--timeout`, or the network. */
|
|
57
|
+
private exchangeFailure;
|
|
58
|
+
/** The response's GraphQL body — an answer that is not one names its HTTP status. */
|
|
59
|
+
private parseBody;
|
|
60
|
+
/** The root field's object, once the body carries no error. */
|
|
61
|
+
private rootField;
|
|
62
|
+
/**
|
|
63
|
+
* A GraphQL error ends the call: credentials refused as UNAUTHORIZED or
|
|
64
|
+
* FORBIDDEN on exit 3, any other code as the typed {@link SyncApiError}.
|
|
65
|
+
*/
|
|
66
|
+
private assertNoErrors;
|
|
67
|
+
private extensionsOf;
|
|
68
|
+
private messageOf;
|
|
69
|
+
/** The variables a typed error carries, as text — none when it carries none. */
|
|
70
|
+
private errorVariables;
|
|
71
|
+
/** A pulled bundle's bytes against what it declares: its length, then its sha256. */
|
|
72
|
+
private assertDeclared;
|
|
73
|
+
/** A string field that must carry text. */
|
|
74
|
+
private readString;
|
|
75
|
+
/** A string field that may be empty. */
|
|
76
|
+
private readText;
|
|
77
|
+
private readNullableString;
|
|
78
|
+
private readStringList;
|
|
79
|
+
private readCount;
|
|
80
|
+
private readBoolean;
|
|
81
|
+
private unreadable;
|
|
82
|
+
private notGraphQlMessage;
|
|
83
|
+
/** What failed to connect: fetch's own message names nothing, its cause names the socket error. */
|
|
84
|
+
private failureDetail;
|
|
85
|
+
}
|