@forgecart/cli 2.202610130038.0 → 2.202610130322.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-refusal.error.d.ts +2 -2
- package/dist/src/admin-api/admin-api-refusal.error.js +2 -2
- package/dist/src/admin-api/admin-api.client.d.ts +8 -4
- package/dist/src/admin-api/admin-api.client.js +11 -9
- package/dist/src/admin-api/admin-api.client.js.map +1 -1
- package/dist/src/admin-api/admin-api.result.d.ts +4 -0
- package/dist/src/admin-api/admin-api.result.js +17 -0
- package/dist/src/admin-api/admin-api.result.js.map +1 -1
- package/dist/src/authorization/cli-authorization-flow.d.ts +21 -14
- package/dist/src/authorization/cli-authorization-flow.js +40 -31
- package/dist/src/authorization/cli-authorization-flow.js.map +1 -1
- package/dist/src/authorization/cli-credential-check.d.ts +6 -13
- package/dist/src/authorization/cli-credential-check.js +6 -23
- package/dist/src/authorization/cli-credential-check.js.map +1 -1
- package/dist/src/authorization/cli-credential-step.d.ts +74 -0
- package/dist/src/authorization/cli-credential-step.js +154 -0
- package/dist/src/authorization/cli-credential-step.js.map +1 -0
- package/dist/src/authorization/cli-unauthorized-refusal.d.ts +18 -8
- package/dist/src/authorization/cli-unauthorized-refusal.js +26 -11
- package/dist/src/authorization/cli-unauthorized-refusal.js.map +1 -1
- package/dist/src/authorization/type/cli-authorization.types.d.ts +36 -4
- package/dist/src/authorization/type/cli-authorization.types.js +16 -3
- package/dist/src/authorization/type/cli-authorization.types.js.map +1 -1
- package/dist/src/commands/init.d.ts +0 -10
- package/dist/src/commands/init.js +17 -42
- package/dist/src/commands/init.js.map +1 -1
- package/dist/src/commands/sync.js +2 -2
- package/dist/src/commands/sync.js.map +1 -1
- package/dist/src/deploy/deploy.runner.d.ts +15 -8
- package/dist/src/deploy/deploy.runner.js +19 -10
- package/dist/src/deploy/deploy.runner.js.map +1 -1
- package/dist/src/sync/sync-dotenv.file.d.ts +30 -22
- package/dist/src/sync/sync-dotenv.file.js +49 -54
- package/dist/src/sync/sync-dotenv.file.js.map +1 -1
- package/dist/src/sync/sync-env-channel.d.ts +47 -0
- package/dist/src/sync/sync-env-channel.js +106 -0
- package/dist/src/sync/sync-env-channel.js.map +1 -0
- package/dist/src/sync/sync-env.runner.d.ts +26 -10
- package/dist/src/sync/sync-env.runner.js +73 -40
- package/dist/src/sync/sync-env.runner.js.map +1 -1
- package/dist/src/sync/sync-record.file.d.ts +8 -6
- package/dist/src/sync/sync-record.file.js +18 -5
- package/dist/src/sync/sync-record.file.js.map +1 -1
- package/dist/src/sync/sync-round.d.ts +23 -13
- package/dist/src/sync/sync-round.js +73 -37
- package/dist/src/sync/sync-round.js.map +1 -1
- package/dist/src/sync/sync-watch.d.ts +2 -1
- package/dist/src/sync/sync-watch.js +2 -1
- package/dist/src/sync/sync-watch.js.map +1 -1
- package/dist/src/sync/sync.runner.d.ts +16 -6
- package/dist/src/sync/sync.runner.js +29 -19
- package/dist/src/sync/sync.runner.js.map +1 -1
- package/dist/src/sync/type/sync.types.d.ts +49 -16
- package/dist/src/sync/type/sync.types.js.map +1 -1
- package/dist/src/sync/workspace-sync.client.d.ts +14 -8
- package/dist/src/sync/workspace-sync.client.js +40 -17
- package/dist/src/sync/workspace-sync.client.js.map +1 -1
- package/dist/src/util/dotenv.util.d.ts +19 -5
- package/dist/src/util/dotenv.util.js +42 -5
- package/dist/src/util/dotenv.util.js.map +1 -1
- package/package.json +1 -1
|
@@ -1,20 +1,25 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { writeFile } from 'node:fs/promises';
|
|
2
2
|
import { join, resolve } from 'node:path';
|
|
3
|
+
import { isJsonObject } from '../json.js';
|
|
3
4
|
import { print } from '../output.js';
|
|
4
|
-
import { dotenvValue, mergeDotenv, merchantAssignments } from '../util/dotenv.util.js';
|
|
5
|
+
import { STOREFRONT_ENV_FILES, dotenvValue, isStorefrontEnvFile, mergeDotenv, merchantAssignments, readDotenvFile, } from '../util/dotenv.util.js';
|
|
5
6
|
/**
|
|
6
7
|
* The variable a merge's lines arrive in — the pod records a command's line,
|
|
7
8
|
* never its environment, so the lines never land in a record.
|
|
8
9
|
*/
|
|
9
|
-
const
|
|
10
|
+
const SYNC_ENV_FILES_VARIABLE = 'FORGECART_SYNC_ENV_FILES';
|
|
10
11
|
/**
|
|
11
12
|
* `forgecart sync-env [name]` (#2781) — the workspace's side of `forgecart
|
|
12
|
-
* sync`, which the admin API runs in the pod
|
|
13
|
-
* storefront
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
13
|
+
* sync`, which the admin API runs in the pod. It reads EVERY env file the
|
|
14
|
+
* storefront holds — `.env`, `.env.local`, `.env.development`,
|
|
15
|
+
* `.env.development.local`, `.env.production`, `.env.production.local` — or,
|
|
16
|
+
* with `--merge`, merges the lines FORGECART_SYNC_ENV_FILES carries for each
|
|
17
|
+
* file into the storefront file of that name under init's merge rule, creating
|
|
18
|
+
* one the storefront lacks. The files stay apart, so the dev-server resolves
|
|
19
|
+
* every key as the laptop does. It prints ONE JSON report: one entry per env
|
|
20
|
+
* file the storefront holds — its merchant lines as they stand, and how many
|
|
21
|
+
* keys the merge carried in. The platform's own `FORGECART_*` keys are never
|
|
22
|
+
* read out and never written: a laptop's never land, the pod's never leave.
|
|
18
23
|
*/
|
|
19
24
|
export class SyncEnvRunner {
|
|
20
25
|
name;
|
|
@@ -28,31 +33,60 @@ export class SyncEnvRunner {
|
|
|
28
33
|
this.options = options;
|
|
29
34
|
}
|
|
30
35
|
async run() {
|
|
31
|
-
const
|
|
32
|
-
const
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
+
const storefront = this.name ? resolve(process.cwd(), this.name) : process.cwd();
|
|
37
|
+
const incoming = this.incoming();
|
|
38
|
+
const files = [];
|
|
39
|
+
for (const file of STOREFRONT_ENV_FILES) {
|
|
40
|
+
const settled = await this.settle(storefront, file, incoming.get(file));
|
|
41
|
+
if (settled !== null)
|
|
42
|
+
files.push(settled);
|
|
36
43
|
}
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
44
|
+
this.report({ files });
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* One env file once the run is done: the merchant keys carried for it merged
|
|
48
|
+
* in — the file created when the storefront lacks it — and its merchant lines
|
|
49
|
+
* as they then stand; null for a file the storefront does not hold.
|
|
50
|
+
*/
|
|
51
|
+
async settle(storefront, file, managed) {
|
|
52
|
+
const path = join(storefront, file);
|
|
53
|
+
const existing = await readDotenvFile(path);
|
|
54
|
+
if (managed === undefined || managed.size === 0)
|
|
55
|
+
return this.held(file, existing);
|
|
56
|
+
const next = mergeDotenv(existing ?? '', managed);
|
|
43
57
|
if (next !== existing)
|
|
44
|
-
await writeFile(
|
|
45
|
-
|
|
58
|
+
await writeFile(path, next, 'utf8');
|
|
59
|
+
return { file, lines: this.merchantLines(next), merged: managed.size };
|
|
60
|
+
}
|
|
61
|
+
/** A file nothing was merged into, as it stands — null when the storefront does not hold it. */
|
|
62
|
+
held(file, content) {
|
|
63
|
+
if (content === null)
|
|
64
|
+
return null;
|
|
65
|
+
return { file, lines: this.merchantLines(content), merged: 0 };
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* The merchant keys a merge carries for each env file, keyed to their values
|
|
69
|
+
* — platform keys and other lines dropped; none for a read.
|
|
70
|
+
*/
|
|
71
|
+
incoming() {
|
|
72
|
+
const incoming = new Map();
|
|
73
|
+
if (this.options.merge !== true)
|
|
74
|
+
return incoming;
|
|
75
|
+
for (const entry of this.parseEntries(process.env[SYNC_ENV_FILES_VARIABLE])) {
|
|
76
|
+
const { file, lines } = this.parseEntry(entry);
|
|
77
|
+
if (incoming.has(file))
|
|
78
|
+
throw new Error(this.malformedMessage());
|
|
79
|
+
incoming.set(file, this.managedOf(lines));
|
|
80
|
+
}
|
|
81
|
+
return incoming;
|
|
46
82
|
}
|
|
47
|
-
|
|
48
|
-
incoming(raw) {
|
|
49
|
-
const lines = this.parseLines(raw);
|
|
83
|
+
managedOf(lines) {
|
|
50
84
|
return new Map([...merchantAssignments(lines)].map(([key, line]) => [
|
|
51
85
|
key,
|
|
52
86
|
dotenvValue(line, key),
|
|
53
87
|
]));
|
|
54
88
|
}
|
|
55
|
-
|
|
89
|
+
parseEntries(raw) {
|
|
56
90
|
if (raw === undefined)
|
|
57
91
|
throw new Error(this.malformedMessage());
|
|
58
92
|
let parsed;
|
|
@@ -64,14 +98,24 @@ export class SyncEnvRunner {
|
|
|
64
98
|
}
|
|
65
99
|
if (!Array.isArray(parsed))
|
|
66
100
|
throw new Error(this.malformedMessage());
|
|
67
|
-
|
|
101
|
+
return parsed;
|
|
102
|
+
}
|
|
103
|
+
/** One entry: an env file the storefront reads — never a path outside them — and its lines. */
|
|
104
|
+
parseEntry(entry) {
|
|
105
|
+
if (!isJsonObject(entry))
|
|
106
|
+
throw new Error(this.malformedMessage());
|
|
107
|
+
const { file, lines } = entry;
|
|
108
|
+
if (typeof file !== 'string' || !isStorefrontEnvFile(file) || !Array.isArray(lines)) {
|
|
109
|
+
throw new Error(this.malformedMessage());
|
|
110
|
+
}
|
|
111
|
+
const items = lines;
|
|
68
112
|
if (!items.every((item) => typeof item === 'string')) {
|
|
69
113
|
throw new Error(this.malformedMessage());
|
|
70
114
|
}
|
|
71
|
-
return items;
|
|
115
|
+
return { file, lines: items };
|
|
72
116
|
}
|
|
73
117
|
malformedMessage() {
|
|
74
|
-
return `--merge reads ${
|
|
118
|
+
return `--merge reads ${SYNC_ENV_FILES_VARIABLE}, which must hold a JSON list of { "file", "lines" } entries — each file one of ${STOREFRONT_ENV_FILES.join(', ')}, named once, and its lines KEY=value lines.`;
|
|
75
119
|
}
|
|
76
120
|
merchantLines(content) {
|
|
77
121
|
return [...merchantAssignments(content.split('\n')).values()];
|
|
@@ -80,16 +124,5 @@ export class SyncEnvRunner {
|
|
|
80
124
|
report(report) {
|
|
81
125
|
print(JSON.stringify(report));
|
|
82
126
|
}
|
|
83
|
-
/** The `.env` text — empty when the storefront has none; any other failure propagates. */
|
|
84
|
-
async read(path) {
|
|
85
|
-
try {
|
|
86
|
-
return await readFile(path, 'utf8');
|
|
87
|
-
}
|
|
88
|
-
catch (error) {
|
|
89
|
-
if (error.code === 'ENOENT')
|
|
90
|
-
return '';
|
|
91
|
-
throw error;
|
|
92
|
-
}
|
|
93
|
-
}
|
|
94
127
|
}
|
|
95
128
|
//# sourceMappingURL=sync-env.runner.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"sync-env.runner.js","sourceRoot":"","sources":["../../../src/sync/sync-env.runner.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,
|
|
1
|
+
{"version":3,"file":"sync-env.runner.js","sourceRoot":"","sources":["../../../src/sync/sync-env.runner.ts"],"names":[],"mappings":"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"}
|
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
import type { SyncRecord } from './type/sync.types.js';
|
|
1
|
+
import type { SyncEnvDigests, SyncRecord } from './type/sync.types.js';
|
|
2
2
|
/**
|
|
3
3
|
* The last sync of one storefront (#2781), kept in `.forgecart/sync/state.json`
|
|
4
4
|
* beside the private repository: the channel it ran against — a digest of the
|
|
5
5
|
* admin API and the channel token, never the token — the serving commit the
|
|
6
|
-
* laptop's tree last matched, and a digest of every merchant
|
|
7
|
-
* last travelled. A record of another channel is no
|
|
8
|
-
* at another channel syncs as a first sync.
|
|
6
|
+
* laptop's tree last matched, and a digest of every merchant line of every env
|
|
7
|
+
* file as it last travelled, file by file. A record of another channel is no
|
|
8
|
+
* record: a storefront pointed at another channel syncs as a first sync.
|
|
9
9
|
*/
|
|
10
10
|
export declare class SyncRecordFile {
|
|
11
11
|
private readonly channel;
|
|
@@ -17,11 +17,13 @@ export declare class SyncRecordFile {
|
|
|
17
17
|
constructor(storefront: string, channel: string);
|
|
18
18
|
/** This channel's last sync — null before the first, or after a sync against another channel. */
|
|
19
19
|
read(): Promise<SyncRecord | null>;
|
|
20
|
-
/** Record a sync that left the laptop on `servingSha`, with the
|
|
21
|
-
write(servingSha: string, env: Readonly<
|
|
20
|
+
/** Record a sync that left the laptop on `servingSha`, with the env files' lines as `env` digests them. */
|
|
21
|
+
write(servingSha: string, env: Readonly<SyncEnvDigests>): Promise<void>;
|
|
22
22
|
/** The record's fields, checked one by one: a file sync cannot read is an error naming it. */
|
|
23
23
|
private parse;
|
|
24
|
+
/** The env digests, file by file: every file one a storefront reads, every digest a string. */
|
|
24
25
|
private parseEnv;
|
|
26
|
+
private parseFileDigests;
|
|
25
27
|
private unreadableMessage;
|
|
26
28
|
/** The record's text — null before the first sync; any other failure propagates. */
|
|
27
29
|
private readIfPresent;
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { readFile, writeFile } from 'node:fs/promises';
|
|
2
2
|
import { join } from 'node:path';
|
|
3
3
|
import { isJsonObject } from '../json.js';
|
|
4
|
+
import { isStorefrontEnvFile } from '../util/dotenv.util.js';
|
|
4
5
|
import { SYNC_STATE_PATH } from './sync-repository.js';
|
|
5
6
|
/** The record's file, in the private state directory. */
|
|
6
7
|
const RECORD_FILE = 'state.json';
|
|
@@ -11,9 +12,9 @@ const COMMIT_ID_LENGTHS = new Set([40, 64]);
|
|
|
11
12
|
* The last sync of one storefront (#2781), kept in `.forgecart/sync/state.json`
|
|
12
13
|
* beside the private repository: the channel it ran against — a digest of the
|
|
13
14
|
* admin API and the channel token, never the token — the serving commit the
|
|
14
|
-
* laptop's tree last matched, and a digest of every merchant
|
|
15
|
-
* last travelled. A record of another channel is no
|
|
16
|
-
* at another channel syncs as a first sync.
|
|
15
|
+
* laptop's tree last matched, and a digest of every merchant line of every env
|
|
16
|
+
* file as it last travelled, file by file. A record of another channel is no
|
|
17
|
+
* record: a storefront pointed at another channel syncs as a first sync.
|
|
17
18
|
*/
|
|
18
19
|
export class SyncRecordFile {
|
|
19
20
|
channel;
|
|
@@ -34,7 +35,7 @@ export class SyncRecordFile {
|
|
|
34
35
|
const record = this.parse(raw);
|
|
35
36
|
return record.channel === this.channel ? record : null;
|
|
36
37
|
}
|
|
37
|
-
/** Record a sync that left the laptop on `servingSha`, with the
|
|
38
|
+
/** Record a sync that left the laptop on `servingSha`, with the env files' lines as `env` digests them. */
|
|
38
39
|
async write(servingSha, env) {
|
|
39
40
|
const record = { channel: this.channel, servingSha, env: { ...env } };
|
|
40
41
|
await writeFile(this.path, `${JSON.stringify(record, null, 2)}\n`, 'utf8');
|
|
@@ -59,10 +60,22 @@ export class SyncRecordFile {
|
|
|
59
60
|
}
|
|
60
61
|
return { channel, servingSha, env: this.parseEnv(env) };
|
|
61
62
|
}
|
|
63
|
+
/** The env digests, file by file: every file one a storefront reads, every digest a string. */
|
|
62
64
|
parseEnv(env) {
|
|
63
65
|
if (!isJsonObject(env))
|
|
64
66
|
throw new Error(this.unreadableMessage());
|
|
65
|
-
const
|
|
67
|
+
const digests = {};
|
|
68
|
+
for (const [file, fileDigests] of Object.entries(env)) {
|
|
69
|
+
if (!isStorefrontEnvFile(file))
|
|
70
|
+
throw new Error(this.unreadableMessage());
|
|
71
|
+
digests[file] = this.parseFileDigests(fileDigests);
|
|
72
|
+
}
|
|
73
|
+
return digests;
|
|
74
|
+
}
|
|
75
|
+
parseFileDigests(fileDigests) {
|
|
76
|
+
if (!isJsonObject(fileDigests))
|
|
77
|
+
throw new Error(this.unreadableMessage());
|
|
78
|
+
const entries = Object.entries(fileDigests);
|
|
66
79
|
if (!entries.every((entry) => typeof entry[1] === 'string')) {
|
|
67
80
|
throw new Error(this.unreadableMessage());
|
|
68
81
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"sync-record.file.js","sourceRoot":"","sources":["../../../src/sync/sync-record.file.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,SAAS,EAAE,MAAM,kBAAkB,CAAC;AACvD,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AAEjC,OAAO,EAAE,YAAY,EAAE,MAAM,YAAY,CAAC;
|
|
1
|
+
{"version":3,"file":"sync-record.file.js","sourceRoot":"","sources":["../../../src/sync/sync-record.file.ts"],"names":[],"mappings":"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"}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { CliCredentials } from '../authorization/type/cli-authorization.types.js';
|
|
2
2
|
import { type SyncDivergenceSelection } from './sync-divergence.js';
|
|
3
|
-
import type {
|
|
3
|
+
import type { SyncEnvChannel } from './sync-env-channel.js';
|
|
4
4
|
import type { SyncRecordFile } from './sync-record.file.js';
|
|
5
5
|
import type { SyncRepository } from './sync-repository.js';
|
|
6
6
|
import { type SyncRoundEnd, type SyncRoundOptions } from './type/sync.types.js';
|
|
@@ -18,7 +18,8 @@ import type { WorkspaceSyncClient } from './workspace-sync.client.js';
|
|
|
18
18
|
* rejects with the gates' diagnostics (exit 1). Both changed: ONE selection —
|
|
19
19
|
* local overrides workspace, workspace overrides local, or Abort — never a
|
|
20
20
|
* merge; an abort or an unanswered selection ends the run on exit 4. The
|
|
21
|
-
* merchant lines of
|
|
21
|
+
* merchant lines of every env file travel with the tree, file for file; the
|
|
22
|
+
* platform's keys never do.
|
|
22
23
|
*/
|
|
23
24
|
export declare class SyncRound {
|
|
24
25
|
private readonly options;
|
|
@@ -26,26 +27,26 @@ export declare class SyncRound {
|
|
|
26
27
|
private readonly client;
|
|
27
28
|
private readonly repository;
|
|
28
29
|
private readonly records;
|
|
29
|
-
private readonly
|
|
30
|
+
private readonly envChannel;
|
|
30
31
|
private readonly selection;
|
|
31
|
-
constructor(options: SyncRoundOptions, credentials: CliCredentials, client: WorkspaceSyncClient, repository: SyncRepository, records: SyncRecordFile,
|
|
32
|
+
constructor(options: SyncRoundOptions, credentials: CliCredentials, client: WorkspaceSyncClient, repository: SyncRepository, records: SyncRecordFile, envChannel: SyncEnvChannel, selection: SyncDivergenceSelection);
|
|
32
33
|
/** Run the round; a board that moves under its push is read again, a bounded number of times. */
|
|
33
34
|
run(): Promise<SyncRoundEnd>;
|
|
34
35
|
private attempt;
|
|
35
36
|
/** Decide, print the plan once, and move what it says. */
|
|
36
37
|
private settle;
|
|
37
38
|
private verdictOf;
|
|
38
|
-
/** The two trees agree: record where they stand, and send
|
|
39
|
+
/** The two trees agree: record where they stand, and send the env lines the laptop changed. */
|
|
39
40
|
private inSync;
|
|
40
|
-
/** Only the workspace changed — or it overrides the laptop: its tree and its
|
|
41
|
+
/** Only the workspace changed — or it overrides the laptop: its tree and its env files' lines land on the laptop. */
|
|
41
42
|
private pull;
|
|
42
43
|
/** Only the laptop changed, and the run never pushes: nothing moves. */
|
|
43
44
|
private keepLocal;
|
|
44
45
|
/**
|
|
45
46
|
* Push the laptop's tree onto the serving tip — or over it, when local
|
|
46
|
-
* overrides workspace — as ONE commit in ONE bundle, with the
|
|
47
|
-
*
|
|
48
|
-
* diagnostics; a board that moved before the push landed is read again.
|
|
47
|
+
* overrides workspace — as ONE commit in ONE bundle, with the env lines
|
|
48
|
+
* changed in each file. A tree the gates reject ends the run on exit 1 with
|
|
49
|
+
* their diagnostics; a board that moved before the push landed is read again.
|
|
49
50
|
*/
|
|
50
51
|
private land;
|
|
51
52
|
/** Both sides changed: the selection decides which tree wins, or the run stops with nothing moved. */
|
|
@@ -61,8 +62,13 @@ export declare class SyncRound {
|
|
|
61
62
|
private dryVerdict;
|
|
62
63
|
/** The plan, printed once per round before anything moves. */
|
|
63
64
|
private printPlan;
|
|
64
|
-
/** The
|
|
65
|
-
private
|
|
65
|
+
/** The env keys the plan carries, as a count per env file: those a pull takes, or those a push sends. */
|
|
66
|
+
private printEnvPlan;
|
|
67
|
+
/** The env keys a pull takes, per workspace env file — none when no bundle was read this round. */
|
|
68
|
+
private printIncomingEnv;
|
|
69
|
+
/** One plan line per env file — its name, then its merchant keys — or `none` when there is no file. */
|
|
70
|
+
private printEnvFiles;
|
|
71
|
+
private planLine;
|
|
66
72
|
private printChanges;
|
|
67
73
|
/** The record — when the private repository still holds its base; else a first sync. */
|
|
68
74
|
private heldRecord;
|
|
@@ -80,9 +86,13 @@ export declare class SyncRound {
|
|
|
80
86
|
private sendPush;
|
|
81
87
|
/** A push the workspace did not take: exit 1, with what refused it. */
|
|
82
88
|
private rejection;
|
|
83
|
-
/** The
|
|
89
|
+
/** The env lines a round in sync sends alone — none for a run that never pushes. */
|
|
84
90
|
private sendEnvAlone;
|
|
85
|
-
/** The
|
|
91
|
+
/** The env files a push carries: those with a line changed since it last travelled. */
|
|
92
|
+
private carried;
|
|
93
|
+
/** How many merchant keys a push merged into the workspace, across every env file. */
|
|
94
|
+
private mergedKeys;
|
|
95
|
+
/** The workspace's env files' lines, taken — none when no bundle was read this round. */
|
|
86
96
|
private takeEnv;
|
|
87
97
|
private viewOf;
|
|
88
98
|
/** The travelling tree of the recorded base — the empty tree before a first sync. */
|
|
@@ -11,6 +11,8 @@ const SYNC_FAILED_CODE = 'WORKSPACE_SYNC_FAILED';
|
|
|
11
11
|
const SHORT_SHA_LENGTH = 12;
|
|
12
12
|
/** How many changed paths a divergence lists per side before it counts the rest. */
|
|
13
13
|
const LISTED_PATHS = 20;
|
|
14
|
+
/** The width a plan line's label is padded to — an env file's name longer than it is followed by one space. */
|
|
15
|
+
const PLAN_LABEL_WIDTH = 10;
|
|
14
16
|
/**
|
|
15
17
|
* One round of `forgecart sync` (#2781): read the board's serving commit,
|
|
16
18
|
* snapshot the laptop's tree, hold the serving tree locally, print the plan
|
|
@@ -24,7 +26,8 @@ const LISTED_PATHS = 20;
|
|
|
24
26
|
* rejects with the gates' diagnostics (exit 1). Both changed: ONE selection —
|
|
25
27
|
* local overrides workspace, workspace overrides local, or Abort — never a
|
|
26
28
|
* merge; an abort or an unanswered selection ends the run on exit 4. The
|
|
27
|
-
* merchant lines of
|
|
29
|
+
* merchant lines of every env file travel with the tree, file for file; the
|
|
30
|
+
* platform's keys never do.
|
|
28
31
|
*/
|
|
29
32
|
export class SyncRound {
|
|
30
33
|
options;
|
|
@@ -32,15 +35,15 @@ export class SyncRound {
|
|
|
32
35
|
client;
|
|
33
36
|
repository;
|
|
34
37
|
records;
|
|
35
|
-
|
|
38
|
+
envChannel;
|
|
36
39
|
selection;
|
|
37
|
-
constructor(options, credentials, client, repository, records,
|
|
40
|
+
constructor(options, credentials, client, repository, records, envChannel, selection) {
|
|
38
41
|
this.options = options;
|
|
39
42
|
this.credentials = credentials;
|
|
40
43
|
this.client = client;
|
|
41
44
|
this.repository = repository;
|
|
42
45
|
this.records = records;
|
|
43
|
-
this.
|
|
46
|
+
this.envChannel = envChannel;
|
|
44
47
|
this.selection = selection;
|
|
45
48
|
}
|
|
46
49
|
/** Run the round; a board that moves under its push is read again, a bounded number of times. */
|
|
@@ -93,20 +96,20 @@ export class SyncRound {
|
|
|
93
96
|
return SyncVerdict.DIVERGED;
|
|
94
97
|
return this.options.pullOnly ? SyncVerdict.KEEP_LOCAL : SyncVerdict.PUSH;
|
|
95
98
|
}
|
|
96
|
-
/** The two trees agree: record where they stand, and send
|
|
99
|
+
/** The two trees agree: record where they stand, and send the env lines the laptop changed. */
|
|
97
100
|
async inSync(view) {
|
|
98
101
|
const env = await this.sendEnvAlone(view.record);
|
|
99
102
|
await this.record(view.serving.sha, env.digests);
|
|
100
|
-
const summary = `in sync at serving ${this.short(view.serving.sha)}; ${String(env.merged)}
|
|
103
|
+
const summary = `in sync at serving ${this.short(view.serving.sha)}; ${String(env.merged)} env key(s) merged into the workspace`;
|
|
101
104
|
print(summary);
|
|
102
105
|
return { localTree: view.local, servingSha: view.serving.sha, summary };
|
|
103
106
|
}
|
|
104
|
-
/** Only the workspace changed — or it overrides the laptop: its tree and its
|
|
107
|
+
/** Only the workspace changed — or it overrides the laptop: its tree and its env files' lines land on the laptop. */
|
|
105
108
|
async pull(view) {
|
|
106
109
|
const materialized = await this.repository.materialize(view.local, view.serving.tree);
|
|
107
110
|
const env = await this.takeEnv(view);
|
|
108
111
|
await this.record(view.serving.sha, env.digests);
|
|
109
|
-
const summary = `pulled serving ${this.short(view.serving.sha)}: ${String(materialized.written)} file(s) written, ${String(materialized.removed)} removed, ${String(env.taken)}
|
|
112
|
+
const summary = `pulled serving ${this.short(view.serving.sha)}: ${String(materialized.written)} file(s) written, ${String(materialized.removed)} removed, ${String(env.taken)} env key(s) taken`;
|
|
110
113
|
print(summary);
|
|
111
114
|
return { localTree: view.serving.tree, servingSha: view.serving.sha, summary };
|
|
112
115
|
}
|
|
@@ -122,16 +125,16 @@ export class SyncRound {
|
|
|
122
125
|
}
|
|
123
126
|
/**
|
|
124
127
|
* Push the laptop's tree onto the serving tip — or over it, when local
|
|
125
|
-
* overrides workspace — as ONE commit in ONE bundle, with the
|
|
126
|
-
*
|
|
127
|
-
* diagnostics; a board that moved before the push landed is read again.
|
|
128
|
+
* overrides workspace — as ONE commit in ONE bundle, with the env lines
|
|
129
|
+
* changed in each file. A tree the gates reject ends the run on exit 1 with
|
|
130
|
+
* their diagnostics; a board that moved before the push landed is read again.
|
|
128
131
|
*/
|
|
129
132
|
async land(view) {
|
|
130
133
|
const sent = await this.repository.changes(view.serving.tree, view.local);
|
|
131
|
-
const outgoing = await this.
|
|
134
|
+
const outgoing = await this.envChannel.outgoing(view.record?.env ?? null);
|
|
132
135
|
const commit = await this.repository.pushCommit(view.local, view.serving.sha);
|
|
133
136
|
const bundle = await this.repository.pushBundle(commit, view.serving.sha);
|
|
134
|
-
const result = await this.sendPush(bundle, outgoing
|
|
137
|
+
const result = await this.sendPush(bundle, this.carried(outgoing));
|
|
135
138
|
if (result === null) {
|
|
136
139
|
return { kind: 'moved', point: { localTree: view.local, servingSha: view.serving.sha } };
|
|
137
140
|
}
|
|
@@ -139,7 +142,7 @@ export class SyncRound {
|
|
|
139
142
|
throw this.rejection(result);
|
|
140
143
|
const landed = await this.holdLanded(result.servingSha, view.serving.sha);
|
|
141
144
|
await this.record(landed, outgoing.digests);
|
|
142
|
-
const summary = `pushed ${String(sent.length)} path(s): serving is ${this.short(landed)}; ${String(result
|
|
145
|
+
const summary = `pushed ${String(sent.length)} path(s): serving is ${this.short(landed)}; ${String(this.mergedKeys(result))} env key(s) merged into the workspace`;
|
|
143
146
|
print(summary);
|
|
144
147
|
return this.settled({ localTree: view.local, servingSha: landed, summary });
|
|
145
148
|
}
|
|
@@ -169,11 +172,11 @@ export class SyncRound {
|
|
|
169
172
|
async dryRun(state, record, local) {
|
|
170
173
|
const changes = await this.repository.changes(await this.baseOf(record), local);
|
|
171
174
|
const moved = record?.servingSha !== state.servingSha;
|
|
172
|
-
const outgoing = await this.
|
|
175
|
+
const outgoing = await this.envChannel.outgoing(record?.env ?? null);
|
|
173
176
|
print(` serving ${this.short(state.servingSha)}, ${this.lastSync(record)}`);
|
|
174
177
|
print(` local ${String(changes.length)} path(s) changed since the last sync`);
|
|
175
178
|
print(` workspace ${moved ? 'moved' : 'unchanged'} since the last sync`);
|
|
176
|
-
|
|
179
|
+
this.printEnvFiles(outgoing.files, 'a push would send', 'no env file on this laptop');
|
|
177
180
|
print(` plan ${this.dryVerdict(changes.length > 0, moved, state.servingSha)}`);
|
|
178
181
|
print(' paths a push would send:');
|
|
179
182
|
for (const change of changes)
|
|
@@ -199,24 +202,48 @@ export class SyncRound {
|
|
|
199
202
|
print(` serving ${this.short(view.serving.sha)}, ${this.lastSync(view.record)}`);
|
|
200
203
|
print(` local ${String(view.localChanges.length)} path(s) changed since the last sync`);
|
|
201
204
|
print(` workspace ${String(view.workspaceChanges.length)} path(s) changed since the last sync`);
|
|
202
|
-
|
|
205
|
+
await this.printEnvPlan(view, verdict);
|
|
203
206
|
print(` plan ${verdict}`);
|
|
204
207
|
if (verdict !== SyncVerdict.DIVERGED)
|
|
205
208
|
return;
|
|
206
209
|
this.printChanges('changed on this laptop', view.localChanges);
|
|
207
210
|
this.printChanges('changed on the workspace', view.workspaceChanges);
|
|
208
211
|
}
|
|
209
|
-
/** The
|
|
210
|
-
async
|
|
212
|
+
/** The env keys the plan carries, as a count per env file: those a pull takes, or those a push sends. */
|
|
213
|
+
async printEnvPlan(view, verdict) {
|
|
211
214
|
const recorded = view.record?.env ?? null;
|
|
212
215
|
if (verdict === SyncVerdict.PULL) {
|
|
213
|
-
|
|
214
|
-
return
|
|
216
|
+
await this.printIncomingEnv(view.serving.envFiles, recorded);
|
|
217
|
+
return;
|
|
215
218
|
}
|
|
216
|
-
if (this.options.pullOnly)
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
219
|
+
if (this.options.pullOnly) {
|
|
220
|
+
print(this.planLine('env', 'nothing to send: --pull never pushes'));
|
|
221
|
+
return;
|
|
222
|
+
}
|
|
223
|
+
const outgoing = await this.envChannel.outgoing(recorded);
|
|
224
|
+
this.printEnvFiles(outgoing.files, 'to send', 'no env file on this laptop');
|
|
225
|
+
}
|
|
226
|
+
/** The env keys a pull takes, per workspace env file — none when no bundle was read this round. */
|
|
227
|
+
async printIncomingEnv(workspaceFiles, recorded) {
|
|
228
|
+
if (workspaceFiles === null) {
|
|
229
|
+
print(this.planLine('env', 'nothing to take: no bundle was read this round'));
|
|
230
|
+
return;
|
|
231
|
+
}
|
|
232
|
+
const incoming = await this.envChannel.incoming(workspaceFiles, recorded);
|
|
233
|
+
this.printEnvFiles(incoming, 'to take', 'no env file in the workspace');
|
|
234
|
+
}
|
|
235
|
+
/** One plan line per env file — its name, then its merchant keys — or `none` when there is no file. */
|
|
236
|
+
printEnvFiles(files, action, none) {
|
|
237
|
+
if (files.length === 0) {
|
|
238
|
+
print(this.planLine('env', none));
|
|
239
|
+
return;
|
|
240
|
+
}
|
|
241
|
+
for (const { file, lines } of files) {
|
|
242
|
+
print(this.planLine(file, `${String(lines.length)} merchant key(s) ${action}`));
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
planLine(label, text) {
|
|
246
|
+
return ` ${label.padEnd(PLAN_LABEL_WIDTH)} ${text}`;
|
|
220
247
|
}
|
|
221
248
|
printChanges(heading, changes) {
|
|
222
249
|
print(` ${heading}:`);
|
|
@@ -242,12 +269,12 @@ export class SyncRound {
|
|
|
242
269
|
return {
|
|
243
270
|
sha: servingSha,
|
|
244
271
|
tree: await this.repository.travellingTree(servingSha),
|
|
245
|
-
|
|
272
|
+
envFiles: null,
|
|
246
273
|
};
|
|
247
274
|
}
|
|
248
275
|
const bundle = await this.pulledBundle(record);
|
|
249
276
|
const sha = await this.repository.fetchBundle(bundle.bytes);
|
|
250
|
-
return { sha, tree: await this.repository.travellingTree(sha),
|
|
277
|
+
return { sha, tree: await this.repository.travellingTree(sha), envFiles: bundle.envFiles };
|
|
251
278
|
}
|
|
252
279
|
/**
|
|
253
280
|
* The bundle a pull reads: the serving commits since the recorded base when
|
|
@@ -281,9 +308,9 @@ export class SyncRound {
|
|
|
281
308
|
return (await this.repository.hasCommit(landed)) ? landed : cutOn;
|
|
282
309
|
}
|
|
283
310
|
/** The push, or null when the board moved past the commit it was cut on before it landed. */
|
|
284
|
-
async sendPush(bundle,
|
|
311
|
+
async sendPush(bundle, envFiles) {
|
|
285
312
|
try {
|
|
286
|
-
return await this.client.push(bundle,
|
|
313
|
+
return await this.client.push(bundle, envFiles);
|
|
287
314
|
}
|
|
288
315
|
catch (error) {
|
|
289
316
|
if (error instanceof AdminApiRefusal && error.code === SYNC_DIVERGED_CODE)
|
|
@@ -301,22 +328,31 @@ export class SyncRound {
|
|
|
301
328
|
...result.seoFindings.flatMap((finding) => indented(`seo: ${finding}`)),
|
|
302
329
|
]);
|
|
303
330
|
}
|
|
304
|
-
/** The
|
|
331
|
+
/** The env lines a round in sync sends alone — none for a run that never pushes. */
|
|
305
332
|
async sendEnvAlone(record) {
|
|
306
333
|
if (this.options.pullOnly)
|
|
307
334
|
return { merged: 0, digests: record?.env ?? {} };
|
|
308
|
-
const outgoing = await this.
|
|
309
|
-
|
|
335
|
+
const outgoing = await this.envChannel.outgoing(record?.env ?? null);
|
|
336
|
+
const carried = this.carried(outgoing);
|
|
337
|
+
if (carried.length === 0)
|
|
310
338
|
return { merged: 0, digests: outgoing.digests };
|
|
311
|
-
const result = await this.client.push(null,
|
|
312
|
-
return { merged: result
|
|
339
|
+
const result = await this.client.push(null, carried);
|
|
340
|
+
return { merged: this.mergedKeys(result), digests: outgoing.digests };
|
|
341
|
+
}
|
|
342
|
+
/** The env files a push carries: those with a line changed since it last travelled. */
|
|
343
|
+
carried(outgoing) {
|
|
344
|
+
return outgoing.files.filter(({ lines }) => lines.length > 0);
|
|
345
|
+
}
|
|
346
|
+
/** How many merchant keys a push merged into the workspace, across every env file. */
|
|
347
|
+
mergedKeys(result) {
|
|
348
|
+
return result.envFiles.reduce((total, { merged }) => total + merged, 0);
|
|
313
349
|
}
|
|
314
|
-
/** The workspace's
|
|
350
|
+
/** The workspace's env files' lines, taken — none when no bundle was read this round. */
|
|
315
351
|
async takeEnv(view) {
|
|
316
352
|
const recorded = view.record?.env ?? null;
|
|
317
|
-
if (view.serving.
|
|
353
|
+
if (view.serving.envFiles === null)
|
|
318
354
|
return { taken: 0, digests: recorded ?? {} };
|
|
319
|
-
return this.
|
|
355
|
+
return this.envChannel.take(view.serving.envFiles, this.credentials, recorded);
|
|
320
356
|
}
|
|
321
357
|
async viewOf(record, local, serving) {
|
|
322
358
|
const base = await this.baseOf(record);
|