@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.
Files changed (61) hide show
  1. package/dist/src/admin-api/admin-api-refusal.error.d.ts +2 -2
  2. package/dist/src/admin-api/admin-api-refusal.error.js +2 -2
  3. package/dist/src/admin-api/admin-api.client.d.ts +8 -4
  4. package/dist/src/admin-api/admin-api.client.js +11 -9
  5. package/dist/src/admin-api/admin-api.client.js.map +1 -1
  6. package/dist/src/admin-api/admin-api.result.d.ts +4 -0
  7. package/dist/src/admin-api/admin-api.result.js +17 -0
  8. package/dist/src/admin-api/admin-api.result.js.map +1 -1
  9. package/dist/src/authorization/cli-authorization-flow.d.ts +21 -14
  10. package/dist/src/authorization/cli-authorization-flow.js +40 -31
  11. package/dist/src/authorization/cli-authorization-flow.js.map +1 -1
  12. package/dist/src/authorization/cli-credential-check.d.ts +6 -13
  13. package/dist/src/authorization/cli-credential-check.js +6 -23
  14. package/dist/src/authorization/cli-credential-check.js.map +1 -1
  15. package/dist/src/authorization/cli-credential-step.d.ts +74 -0
  16. package/dist/src/authorization/cli-credential-step.js +154 -0
  17. package/dist/src/authorization/cli-credential-step.js.map +1 -0
  18. package/dist/src/authorization/cli-unauthorized-refusal.d.ts +18 -8
  19. package/dist/src/authorization/cli-unauthorized-refusal.js +26 -11
  20. package/dist/src/authorization/cli-unauthorized-refusal.js.map +1 -1
  21. package/dist/src/authorization/type/cli-authorization.types.d.ts +36 -4
  22. package/dist/src/authorization/type/cli-authorization.types.js +16 -3
  23. package/dist/src/authorization/type/cli-authorization.types.js.map +1 -1
  24. package/dist/src/commands/init.d.ts +0 -10
  25. package/dist/src/commands/init.js +17 -42
  26. package/dist/src/commands/init.js.map +1 -1
  27. package/dist/src/commands/sync.js +2 -2
  28. package/dist/src/commands/sync.js.map +1 -1
  29. package/dist/src/deploy/deploy.runner.d.ts +15 -8
  30. package/dist/src/deploy/deploy.runner.js +19 -10
  31. package/dist/src/deploy/deploy.runner.js.map +1 -1
  32. package/dist/src/sync/sync-dotenv.file.d.ts +30 -22
  33. package/dist/src/sync/sync-dotenv.file.js +49 -54
  34. package/dist/src/sync/sync-dotenv.file.js.map +1 -1
  35. package/dist/src/sync/sync-env-channel.d.ts +47 -0
  36. package/dist/src/sync/sync-env-channel.js +106 -0
  37. package/dist/src/sync/sync-env-channel.js.map +1 -0
  38. package/dist/src/sync/sync-env.runner.d.ts +26 -10
  39. package/dist/src/sync/sync-env.runner.js +73 -40
  40. package/dist/src/sync/sync-env.runner.js.map +1 -1
  41. package/dist/src/sync/sync-record.file.d.ts +8 -6
  42. package/dist/src/sync/sync-record.file.js +18 -5
  43. package/dist/src/sync/sync-record.file.js.map +1 -1
  44. package/dist/src/sync/sync-round.d.ts +23 -13
  45. package/dist/src/sync/sync-round.js +73 -37
  46. package/dist/src/sync/sync-round.js.map +1 -1
  47. package/dist/src/sync/sync-watch.d.ts +2 -1
  48. package/dist/src/sync/sync-watch.js +2 -1
  49. package/dist/src/sync/sync-watch.js.map +1 -1
  50. package/dist/src/sync/sync.runner.d.ts +16 -6
  51. package/dist/src/sync/sync.runner.js +29 -19
  52. package/dist/src/sync/sync.runner.js.map +1 -1
  53. package/dist/src/sync/type/sync.types.d.ts +49 -16
  54. package/dist/src/sync/type/sync.types.js.map +1 -1
  55. package/dist/src/sync/workspace-sync.client.d.ts +14 -8
  56. package/dist/src/sync/workspace-sync.client.js +40 -17
  57. package/dist/src/sync/workspace-sync.client.js.map +1 -1
  58. package/dist/src/util/dotenv.util.d.ts +19 -5
  59. package/dist/src/util/dotenv.util.js +42 -5
  60. package/dist/src/util/dotenv.util.js.map +1 -1
  61. package/package.json +1 -1
@@ -1,20 +1,25 @@
1
- import { readFile, writeFile } from 'node:fs/promises';
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 SYNC_ENV_LINES_VARIABLE = 'FORGECART_SYNC_ENV_LINES';
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: it reads the merchant lines of the
13
- * storefront's `.env`, or, with `--merge`, merges the lines
14
- * FORGECART_SYNC_ENV_LINES carries under init's merge rule, and prints ONE JSON
15
- * report — the merchant lines as they stand, and how many keys the merge
16
- * carried in. The platform's own `FORGECART_*` keys are never read out and
17
- * never written: a laptop's never land, the pod's never leave.
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 envPath = join(this.name ? resolve(process.cwd(), this.name) : process.cwd(), '.env');
32
- const existing = await this.read(envPath);
33
- if (this.options.merge !== true) {
34
- this.report({ lines: this.merchantLines(existing), merged: 0 });
35
- return;
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
- const managed = this.incoming(process.env[SYNC_ENV_LINES_VARIABLE]);
38
- if (managed.size === 0) {
39
- this.report({ lines: this.merchantLines(existing), merged: 0 });
40
- return;
41
- }
42
- const next = mergeDotenv(existing, managed);
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(envPath, next, 'utf8');
45
- this.report({ lines: this.merchantLines(next), merged: managed.size });
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
- /** The merchant keys the variable carries, keyed to their values — platform keys and other lines dropped. */
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
- parseLines(raw) {
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
- const items = parsed;
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 ${SYNC_ENV_LINES_VARIABLE}, which must hold a JSON list of KEY=value lines.`;
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":"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"}
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 `.env` line as it
7
- * last travelled. A record of another channel is no record: a storefront pointed
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 `.env` lines as `env` digests them. */
21
- write(servingSha: string, env: Readonly<Record<string, string>>): Promise<void>;
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 `.env` line as it
15
- * last travelled. A record of another channel is no record: a storefront pointed
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 `.env` lines as `env` digests them. */
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 entries = Object.entries(env);
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":"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"}
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 { SyncDotenvFile } from './sync-dotenv.file.js';
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 `.env` travel with the tree; the platform's keys never do.
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 dotenv;
30
+ private readonly envChannel;
30
31
  private readonly selection;
31
- constructor(options: SyncRoundOptions, credentials: CliCredentials, client: WorkspaceSyncClient, repository: SyncRepository, records: SyncRecordFile, dotenv: SyncDotenvFile, selection: SyncDivergenceSelection);
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 `.env` lines the laptop changed. */
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 `.env` lines land on the laptop. */
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 changed `.env`
47
- * lines. A tree the gates reject ends the run on exit 1 with their
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 `.env` keys the plan carries, as a count. */
65
- private envPlan;
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 `.env` lines a round in sync sends alone — none for a run that never pushes. */
89
+ /** The env lines a round in sync sends alone — none for a run that never pushes. */
84
90
  private sendEnvAlone;
85
- /** The workspace's `.env` lines, taken — none when no bundle was read this round. */
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 `.env` travel with the tree; the platform's keys never do.
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
- dotenv;
38
+ envChannel;
36
39
  selection;
37
- constructor(options, credentials, client, repository, records, dotenv, selection) {
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.dotenv = dotenv;
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 `.env` lines the laptop changed. */
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)} .env key(s) merged into the workspace`;
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 `.env` lines land on the laptop. */
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)} .env key(s) 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 changed `.env`
126
- * lines. A tree the gates reject ends the run on exit 1 with their
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.dotenv.outgoing(view.record?.env ?? null);
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.lines);
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.envKeys)} .env key(s) merged into the workspace`;
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.dotenv.outgoing(record?.env ?? null);
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
- print(` .env ${String(outgoing.lines.length)} merchant key(s) a push would send`);
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
- print(` .env ${await this.envPlan(view, verdict)}`);
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 `.env` keys the plan carries, as a count. */
210
- async envPlan(view, verdict) {
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
- const taken = await this.dotenv.takeable(view.serving.envLines ?? [], recorded);
214
- return `${String(taken)} merchant key(s) to take`;
216
+ await this.printIncomingEnv(view.serving.envFiles, recorded);
217
+ return;
215
218
  }
216
- if (this.options.pullOnly)
217
- return 'nothing to send: --pull never pushes';
218
- const outgoing = await this.dotenv.outgoing(recorded);
219
- return `${String(outgoing.lines.length)} merchant key(s) to send`;
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
- envLines: null,
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), envLines: bundle.envLines };
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, envLines) {
311
+ async sendPush(bundle, envFiles) {
285
312
  try {
286
- return await this.client.push(bundle, envLines);
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 `.env` lines a round in sync sends alone — none for a run that never pushes. */
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.dotenv.outgoing(record?.env ?? null);
309
- if (outgoing.lines.length === 0)
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, outgoing.lines);
312
- return { merged: result.envKeys, digests: outgoing.digests };
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 `.env` lines, taken — none when no bundle was read this round. */
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.envLines === null)
353
+ if (view.serving.envFiles === null)
318
354
  return { taken: 0, digests: recorded ?? {} };
319
- return this.dotenv.take(view.serving.envLines, this.credentials, recorded);
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);