@forgecart/cli 2.202610130131.0 → 2.202610130431.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 (67) 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 +7 -0
  7. package/dist/src/admin-api/admin-api.result.js +31 -2
  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-bundle-push.d.ts +34 -0
  33. package/dist/src/sync/sync-bundle-push.js +97 -0
  34. package/dist/src/sync/sync-bundle-push.js.map +1 -0
  35. package/dist/src/sync/sync-dotenv.file.d.ts +30 -22
  36. package/dist/src/sync/sync-dotenv.file.js +49 -54
  37. package/dist/src/sync/sync-dotenv.file.js.map +1 -1
  38. package/dist/src/sync/sync-env-channel.d.ts +47 -0
  39. package/dist/src/sync/sync-env-channel.js +106 -0
  40. package/dist/src/sync/sync-env-channel.js.map +1 -0
  41. package/dist/src/sync/sync-env.runner.d.ts +26 -10
  42. package/dist/src/sync/sync-env.runner.js +73 -40
  43. package/dist/src/sync/sync-env.runner.js.map +1 -1
  44. package/dist/src/sync/sync-record.file.d.ts +18 -10
  45. package/dist/src/sync/sync-record.file.js +30 -10
  46. package/dist/src/sync/sync-record.file.js.map +1 -1
  47. package/dist/src/sync/sync-repository.d.ts +7 -2
  48. package/dist/src/sync/sync-repository.js +26 -5
  49. package/dist/src/sync/sync-repository.js.map +1 -1
  50. package/dist/src/sync/sync-round.d.ts +28 -15
  51. package/dist/src/sync/sync-round.js +78 -39
  52. package/dist/src/sync/sync-round.js.map +1 -1
  53. package/dist/src/sync/sync-watch.d.ts +2 -1
  54. package/dist/src/sync/sync-watch.js +2 -1
  55. package/dist/src/sync/sync-watch.js.map +1 -1
  56. package/dist/src/sync/sync.runner.d.ts +16 -8
  57. package/dist/src/sync/sync.runner.js +30 -26
  58. package/dist/src/sync/sync.runner.js.map +1 -1
  59. package/dist/src/sync/type/sync.types.d.ts +78 -16
  60. package/dist/src/sync/type/sync.types.js.map +1 -1
  61. package/dist/src/sync/workspace-sync.client.d.ts +43 -16
  62. package/dist/src/sync/workspace-sync.client.js +125 -31
  63. package/dist/src/sync/workspace-sync.client.js.map +1 -1
  64. package/dist/src/util/dotenv.util.d.ts +19 -5
  65. package/dist/src/util/dotenv.util.js +42 -5
  66. package/dist/src/util/dotenv.util.js.map +1 -1
  67. package/package.json +1 -1
@@ -0,0 +1,47 @@
1
+ import { type CliCredentials } from '../authorization/type/cli-authorization.types.js';
2
+ import type { SyncEnvDigests, SyncEnvFileLines, SyncOutgoingEnv, SyncTakenEnv } from './type/sync.types.js';
3
+ /**
4
+ * The laptop's side of `forgecart sync`'s env channel (#2781): EVERY env file a
5
+ * storefront reads — `.env`, `.env.local`, `.env.development`,
6
+ * `.env.development.local`, `.env.production`, `.env.production.local` — carried
7
+ * file for file, each one's merchant lines into the other side's file of the
8
+ * same name, so every key resolves in the workspace as it does on the laptop.
9
+ * The files are never flattened into one, and the platform's `FORGECART_*`
10
+ * keys never cross in any of them. What travelled is recorded per file and per
11
+ * key. A pull also writes the run's four credentials into `.env` — the `.env` a
12
+ * fresh pull writes.
13
+ */
14
+ export declare class SyncEnvChannel {
15
+ private readonly files;
16
+ /** @param storefront the storefront directory. */
17
+ constructor(storefront: string);
18
+ /**
19
+ * The merchant lines a push carries — one entry per env file the laptop
20
+ * holds, its lines changed since `recorded` — and the digests to record once
21
+ * they land.
22
+ */
23
+ outgoing(recorded: SyncEnvDigests | null): Promise<SyncOutgoingEnv>;
24
+ /**
25
+ * The workspace's merchant lines a pull would take — one entry per env file
26
+ * the workspace holds, its lines for every key the laptop has not changed
27
+ * since `recorded`.
28
+ */
29
+ incoming(workspaceFiles: readonly SyncEnvFileLines[], recorded: SyncEnvDigests | null): Promise<SyncEnvFileLines[]>;
30
+ /**
31
+ * Take each workspace env file's merchant lines into the laptop file of the
32
+ * same name — created when the laptop lacks it — and the four credentials
33
+ * into `.env`. A file the workspace does not hold is left as it is, with the
34
+ * digests it last travelled with.
35
+ */
36
+ take(workspaceFiles: readonly SyncEnvFileLines[], credentials: CliCredentials, recorded: SyncEnvDigests | null): Promise<SyncTakenEnv>;
37
+ /** Take into one file — null for a file with neither workspace lines nor credentials to take. */
38
+ private takeInto;
39
+ /**
40
+ * The platform keys a pull writes into `file`: the four credentials in
41
+ * `.env`, in the order init writes them; none elsewhere.
42
+ */
43
+ private platformEntries;
44
+ /** The digests `file` last travelled with: null before a first sync, none for a file that never travelled. */
45
+ private recordOf;
46
+ private linesByFile;
47
+ }
@@ -0,0 +1,106 @@
1
+ import { CliCredentialKey, } from '../authorization/type/cli-authorization.types.js';
2
+ import { STOREFRONT_ENV_FILES, StorefrontEnvFile } from '../util/dotenv.util.js';
3
+ import { SyncDotenvFile } from './sync-dotenv.file.js';
4
+ /**
5
+ * The laptop's side of `forgecart sync`'s env channel (#2781): EVERY env file a
6
+ * storefront reads — `.env`, `.env.local`, `.env.development`,
7
+ * `.env.development.local`, `.env.production`, `.env.production.local` — carried
8
+ * file for file, each one's merchant lines into the other side's file of the
9
+ * same name, so every key resolves in the workspace as it does on the laptop.
10
+ * The files are never flattened into one, and the platform's `FORGECART_*`
11
+ * keys never cross in any of them. What travelled is recorded per file and per
12
+ * key. A pull also writes the run's four credentials into `.env` — the `.env` a
13
+ * fresh pull writes.
14
+ */
15
+ export class SyncEnvChannel {
16
+ files;
17
+ /** @param storefront the storefront directory. */
18
+ constructor(storefront) {
19
+ this.files = STOREFRONT_ENV_FILES.map((file) => new SyncDotenvFile(storefront, file));
20
+ }
21
+ /**
22
+ * The merchant lines a push carries — one entry per env file the laptop
23
+ * holds, its lines changed since `recorded` — and the digests to record once
24
+ * they land.
25
+ */
26
+ async outgoing(recorded) {
27
+ const files = [];
28
+ const digests = {};
29
+ for (const envFile of this.files) {
30
+ const outgoing = await envFile.outgoing(this.recordOf(recorded, envFile.file));
31
+ if (outgoing === null)
32
+ continue;
33
+ files.push({ file: envFile.file, lines: outgoing.lines });
34
+ digests[envFile.file] = outgoing.digests;
35
+ }
36
+ return { files, digests };
37
+ }
38
+ /**
39
+ * The workspace's merchant lines a pull would take — one entry per env file
40
+ * the workspace holds, its lines for every key the laptop has not changed
41
+ * since `recorded`.
42
+ */
43
+ async incoming(workspaceFiles, recorded) {
44
+ const workspace = this.linesByFile(workspaceFiles);
45
+ const files = [];
46
+ for (const envFile of this.files) {
47
+ const workspaceLines = workspace.get(envFile.file);
48
+ if (workspaceLines === undefined)
49
+ continue;
50
+ const lines = await envFile.incoming(workspaceLines, this.recordOf(recorded, envFile.file));
51
+ files.push({ file: envFile.file, lines });
52
+ }
53
+ return files;
54
+ }
55
+ /**
56
+ * Take each workspace env file's merchant lines into the laptop file of the
57
+ * same name — created when the laptop lacks it — and the four credentials
58
+ * into `.env`. A file the workspace does not hold is left as it is, with the
59
+ * digests it last travelled with.
60
+ */
61
+ async take(workspaceFiles, credentials, recorded) {
62
+ const workspace = this.linesByFile(workspaceFiles);
63
+ const digests = {};
64
+ let taken = 0;
65
+ for (const envFile of this.files) {
66
+ const workspaceLines = workspace.get(envFile.file);
67
+ const result = await this.takeInto(envFile, workspaceLines, credentials, recorded);
68
+ const fileDigests = result?.digests ?? recorded?.[envFile.file];
69
+ if (fileDigests !== undefined)
70
+ digests[envFile.file] = fileDigests;
71
+ taken += result?.taken ?? 0;
72
+ }
73
+ return { taken, digests };
74
+ }
75
+ /** Take into one file — null for a file with neither workspace lines nor credentials to take. */
76
+ async takeInto(envFile, workspaceLines, credentials, recorded) {
77
+ const platform = this.platformEntries(envFile.file, credentials);
78
+ if (workspaceLines === undefined && platform.size === 0)
79
+ return null;
80
+ return envFile.take(workspaceLines ?? [], platform, this.recordOf(recorded, envFile.file));
81
+ }
82
+ /**
83
+ * The platform keys a pull writes into `file`: the four credentials in
84
+ * `.env`, in the order init writes them; none elsewhere.
85
+ */
86
+ platformEntries(file, credentials) {
87
+ if (file !== StorefrontEnvFile.BASE)
88
+ return new Map();
89
+ return new Map([
90
+ [CliCredentialKey.CHANNEL_TOKEN, credentials.channelToken],
91
+ [CliCredentialKey.SHOP_API_URL, credentials.shopApiUrl],
92
+ [CliCredentialKey.ADMIN_SECRET, credentials.adminSecret],
93
+ [CliCredentialKey.ADMIN_API_URL, credentials.adminApiUrl],
94
+ ]);
95
+ }
96
+ /** The digests `file` last travelled with: null before a first sync, none for a file that never travelled. */
97
+ recordOf(recorded, file) {
98
+ if (recorded === null)
99
+ return null;
100
+ return recorded[file] ?? {};
101
+ }
102
+ linesByFile(files) {
103
+ return new Map(files.map(({ file, lines }) => [file, lines]));
104
+ }
105
+ }
106
+ //# sourceMappingURL=sync-env-channel.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sync-env-channel.js","sourceRoot":"","sources":["../../../src/sync/sync-env-channel.ts"],"names":[],"mappings":"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"}
@@ -1,12 +1,16 @@
1
1
  import type { SyncEnvOptions } from './type/sync.types.js';
2
2
  /**
3
3
  * `forgecart sync-env [name]` (#2781) — the workspace's side of `forgecart
4
- * sync`, which the admin API runs in the pod: it reads the merchant lines of the
5
- * storefront's `.env`, or, with `--merge`, merges the lines
6
- * FORGECART_SYNC_ENV_LINES carries under init's merge rule, and prints ONE JSON
7
- * report — the merchant lines as they stand, and how many keys the merge
8
- * carried in. The platform's own `FORGECART_*` keys are never read out and
9
- * never written: a laptop's never land, the pod's never leave.
4
+ * sync`, which the admin API runs in the pod. It reads EVERY env file the
5
+ * storefront holds — `.env`, `.env.local`, `.env.development`,
6
+ * `.env.development.local`, `.env.production`, `.env.production.local` — or,
7
+ * with `--merge`, merges the lines FORGECART_SYNC_ENV_FILES carries for each
8
+ * file into the storefront file of that name under init's merge rule, creating
9
+ * one the storefront lacks. The files stay apart, so the dev-server resolves
10
+ * every key as the laptop does. It prints ONE JSON report: one entry per env
11
+ * file the storefront holds — its merchant lines as they stand, and how many
12
+ * keys the merge carried in. The platform's own `FORGECART_*` keys are never
13
+ * read out and never written: a laptop's never land, the pod's never leave.
10
14
  */
11
15
  export declare class SyncEnvRunner {
12
16
  private readonly name;
@@ -17,13 +21,25 @@ export declare class SyncEnvRunner {
17
21
  */
18
22
  constructor(name: string | undefined, options: SyncEnvOptions);
19
23
  run(): Promise<void>;
20
- /** The merchant keys the variable carries, keyed to their values — platform keys and other lines dropped. */
24
+ /**
25
+ * One env file once the run is done: the merchant keys carried for it merged
26
+ * in — the file created when the storefront lacks it — and its merchant lines
27
+ * as they then stand; null for a file the storefront does not hold.
28
+ */
29
+ private settle;
30
+ /** A file nothing was merged into, as it stands — null when the storefront does not hold it. */
31
+ private held;
32
+ /**
33
+ * The merchant keys a merge carries for each env file, keyed to their values
34
+ * — platform keys and other lines dropped; none for a read.
35
+ */
21
36
  private incoming;
22
- private parseLines;
37
+ private managedOf;
38
+ private parseEntries;
39
+ /** One entry: an env file the storefront reads — never a path outside them — and its lines. */
40
+ private parseEntry;
23
41
  private malformedMessage;
24
42
  private merchantLines;
25
43
  /** The report the admin API parses: ONE line of JSON on stdout. */
26
44
  private report;
27
- /** The `.env` text — empty when the storefront has none; any other failure propagates. */
28
- private read;
29
45
  }
@@ -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":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,kBAAkB,CAAC;AAC7C,OAAO,EAAE,IAAI,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAE1C,OAAO,EAAE,YAAY,EAAE,MAAM,YAAY,CAAC;AAC1C,OAAO,EAAE,KAAK,EAAE,MAAM,cAAc,CAAC;AACrC,OAAO,EACL,oBAAoB,EACpB,WAAW,EACX,mBAAmB,EACnB,WAAW,EACX,mBAAmB,EACnB,cAAc,GAEf,MAAM,wBAAwB,CAAC;AAShC;;;GAGG;AACH,MAAM,uBAAuB,GAAG,0BAA0B,CAAC;AAE3D;;;;;;;;;;;;GAYG;AACH,MAAM,OAAO,aAAa;IAML;IACA;IANnB;;;OAGG;IACH,YACmB,IAAwB,EACxB,OAAuB;QADvB,SAAI,GAAJ,IAAI,CAAoB;QACxB,YAAO,GAAP,OAAO,CAAgB;IACvC,CAAC;IAEJ,KAAK,CAAC,GAAG;QACP,MAAM,UAAU,GAAG,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC,OAAO,CAAC,OAAO,CAAC,GAAG,EAAE,EAAE,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,GAAG,EAAE,CAAC;QACjF,MAAM,QAAQ,GAAG,IAAI,CAAC,QAAQ,EAAE,CAAC;QACjC,MAAM,KAAK,GAAwB,EAAE,CAAC;QACtC,KAAK,MAAM,IAAI,IAAI,oBAAoB,EAAE,CAAC;YACxC,MAAM,OAAO,GAAG,MAAM,IAAI,CAAC,MAAM,CAAC,UAAU,EAAE,IAAI,EAAE,QAAQ,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC,CAAC;YACxE,IAAI,OAAO,KAAK,IAAI;gBAAE,KAAK,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;QAC5C,CAAC;QACD,IAAI,CAAC,MAAM,CAAC,EAAE,KAAK,EAAE,CAAC,CAAC;IACzB,CAAC;IAED;;;;OAIG;IACK,KAAK,CAAC,MAAM,CAClB,UAAkB,EAClB,IAAuB,EACvB,OAAgD;QAEhD,MAAM,IAAI,GAAG,IAAI,CAAC,UAAU,EAAE,IAAI,CAAC,CAAC;QACpC,MAAM,QAAQ,GAAG,MAAM,cAAc,CAAC,IAAI,CAAC,CAAC;QAC5C,IAAI,OAAO,KAAK,SAAS,IAAI,OAAO,CAAC,IAAI,KAAK,CAAC;YAAE,OAAO,IAAI,CAAC,IAAI,CAAC,IAAI,EAAE,QAAQ,CAAC,CAAC;QAClF,MAAM,IAAI,GAAG,WAAW,CAAC,QAAQ,IAAI,EAAE,EAAE,OAAO,CAAC,CAAC;QAClD,IAAI,IAAI,KAAK,QAAQ;YAAE,MAAM,SAAS,CAAC,IAAI,EAAE,IAAI,EAAE,MAAM,CAAC,CAAC;QAC3D,OAAO,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,CAAC,aAAa,CAAC,IAAI,CAAC,EAAE,MAAM,EAAE,OAAO,CAAC,IAAI,EAAE,CAAC;IACzE,CAAC;IAED,gGAAgG;IACxF,IAAI,CAAC,IAAuB,EAAE,OAAsB;QAC1D,IAAI,OAAO,KAAK,IAAI;YAAE,OAAO,IAAI,CAAC;QAClC,OAAO,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,CAAC,aAAa,CAAC,OAAO,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,CAAC;IACjE,CAAC;IAED;;;OAGG;IACK,QAAQ;QACd,MAAM,QAAQ,GAAG,IAAI,GAAG,EAA0C,CAAC;QACnE,IAAI,IAAI,CAAC,OAAO,CAAC,KAAK,KAAK,IAAI;YAAE,OAAO,QAAQ,CAAC;QACjD,KAAK,MAAM,KAAK,IAAI,IAAI,CAAC,YAAY,CAAC,OAAO,CAAC,GAAG,CAAC,uBAAuB,CAAC,CAAC,EAAE,CAAC;YAC5E,MAAM,EAAE,IAAI,EAAE,KAAK,EAAE,GAAG,IAAI,CAAC,UAAU,CAAC,KAAK,CAAC,CAAC;YAC/C,IAAI,QAAQ,CAAC,GAAG,CAAC,IAAI,CAAC;gBAAE,MAAM,IAAI,KAAK,CAAC,IAAI,CAAC,gBAAgB,EAAE,CAAC,CAAC;YACjE,QAAQ,CAAC,GAAG,CAAC,IAAI,EAAE,IAAI,CAAC,SAAS,CAAC,KAAK,CAAC,CAAC,CAAC;QAC5C,CAAC;QACD,OAAO,QAAQ,CAAC;IAClB,CAAC;IAEO,SAAS,CAAC,KAAwB;QACxC,OAAO,IAAI,GAAG,CACZ,CAAC,GAAG,mBAAmB,CAAC,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,GAAG,EAAE,IAAI,CAAC,EAAoB,EAAE,CAAC;YACrE,GAAG;YACH,WAAW,CAAC,IAAI,EAAE,GAAG,CAAC;SACvB,CAAC,CACH,CAAC;IACJ,CAAC;IAEO,YAAY,CAAC,GAAuB;QAC1C,IAAI,GAAG,KAAK,SAAS;YAAE,MAAM,IAAI,KAAK,CAAC,IAAI,CAAC,gBAAgB,EAAE,CAAC,CAAC;QAChE,IAAI,MAAe,CAAC;QACpB,IAAI,CAAC;YACH,MAAM,GAAG,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;QAC3B,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,MAAM,IAAI,KAAK,CAAC,IAAI,CAAC,gBAAgB,EAAE,EAAE,EAAE,KAAK,EAAE,KAAK,EAAE,CAAC,CAAC;QAC7D,CAAC;QACD,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,MAAM,CAAC;YAAE,MAAM,IAAI,KAAK,CAAC,IAAI,CAAC,gBAAgB,EAAE,CAAC,CAAC;QACrE,OAAO,MAAM,CAAC;IAChB,CAAC;IAED,+FAA+F;IACvF,UAAU,CAAC,KAAc;QAC/B,IAAI,CAAC,YAAY,CAAC,KAAK,CAAC;YAAE,MAAM,IAAI,KAAK,CAAC,IAAI,CAAC,gBAAgB,EAAE,CAAC,CAAC;QACnE,MAAM,EAAE,IAAI,EAAE,KAAK,EAAE,GAAG,KAAK,CAAC;QAC9B,IAAI,OAAO,IAAI,KAAK,QAAQ,IAAI,CAAC,mBAAmB,CAAC,IAAI,CAAC,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,EAAE,CAAC;YACpF,MAAM,IAAI,KAAK,CAAC,IAAI,CAAC,gBAAgB,EAAE,CAAC,CAAC;QAC3C,CAAC;QACD,MAAM,KAAK,GAAc,KAAK,CAAC;QAC/B,IAAI,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC,IAAI,EAAkB,EAAE,CAAC,OAAO,IAAI,KAAK,QAAQ,CAAC,EAAE,CAAC;YACrE,MAAM,IAAI,KAAK,CAAC,IAAI,CAAC,gBAAgB,EAAE,CAAC,CAAC;QAC3C,CAAC;QACD,OAAO,EAAE,IAAI,EAAE,KAAK,EAAE,KAAK,EAAE,CAAC;IAChC,CAAC;IAEO,gBAAgB;QACtB,OAAO,iBAAiB,uBAAuB,mFAAmF,oBAAoB,CAAC,IAAI,CAAC,IAAI,CAAC,8CAA8C,CAAC;IAClN,CAAC;IAEO,aAAa,CAAC,OAAe;QACnC,OAAO,CAAC,GAAG,mBAAmB,CAAC,OAAO,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC,MAAM,EAAE,CAAC,CAAC;IAChE,CAAC;IAED,mEAAmE;IAC3D,MAAM,CAAC,MAAqB;QAClC,KAAK,CAAC,IAAI,CAAC,SAAS,CAAC,MAAM,CAAC,CAAC,CAAC;IAChC,CAAC;CACF"}
@@ -1,27 +1,35 @@
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
- * 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.
5
+ * channel token, never the token — the serving commit the laptop's tree last
6
+ * matched, and a digest of every merchant line of every env file as it last
7
+ * travelled, file by file. A record of another channel is no record: a
8
+ * storefront pointed at another channel syncs as a first sync.
9
+ *
10
+ * The admin API's URL is no part of the channel: it is the route a run reaches
11
+ * the shop by, and a hop in front of the shop — a proxy, an ingress, the edge —
12
+ * changes it while the channel and its board stay the same (factory task
13
+ * 2375). A laptop that pulled through one route pushes through another onto
14
+ * the base it pulled.
9
15
  */
10
16
  export declare class SyncRecordFile {
11
- private readonly channel;
12
17
  private readonly path;
18
+ private readonly channel;
13
19
  /**
14
20
  * @param storefront the storefront directory.
15
- * @param channel the digest naming the channel this run syncs against.
21
+ * @param channelToken the token of the channel this run syncs against, kept only as its digest.
16
22
  */
17
- constructor(storefront: string, channel: string);
23
+ constructor(storefront: string, channelToken: string);
18
24
  /** This channel's last sync — null before the first, or after a sync against another channel. */
19
25
  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>;
26
+ /** Record a sync that left the laptop on `servingSha`, with the env files' lines as `env` digests them. */
27
+ write(servingSha: string, env: Readonly<SyncEnvDigests>): Promise<void>;
22
28
  /** The record's fields, checked one by one: a file sync cannot read is an error naming it. */
23
29
  private parse;
30
+ /** The env digests, file by file: every file one a storefront reads, every digest a string. */
24
31
  private parseEnv;
32
+ private parseFileDigests;
25
33
  private unreadableMessage;
26
34
  /** The record's text — null before the first sync; any other failure propagates. */
27
35
  private readIfPresent;
@@ -1,6 +1,8 @@
1
+ import { createHash } from 'node:crypto';
1
2
  import { readFile, writeFile } from 'node:fs/promises';
2
3
  import { join } from 'node:path';
3
4
  import { isJsonObject } from '../json.js';
5
+ import { isStorefrontEnvFile } from '../util/dotenv.util.js';
4
6
  import { SYNC_STATE_PATH } from './sync-repository.js';
5
7
  /** The record's file, in the private state directory. */
6
8
  const RECORD_FILE = 'state.json';
@@ -10,21 +12,27 @@ const COMMIT_ID_LENGTHS = new Set([40, 64]);
10
12
  /**
11
13
  * The last sync of one storefront (#2781), kept in `.forgecart/sync/state.json`
12
14
  * beside the private repository: the channel it ran against — a digest of the
13
- * 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
+ * channel token, never the token — the serving commit the laptop's tree last
16
+ * matched, and a digest of every merchant line of every env file as it last
17
+ * travelled, file by file. A record of another channel is no record: a
18
+ * storefront pointed at another channel syncs as a first sync.
19
+ *
20
+ * The admin API's URL is no part of the channel: it is the route a run reaches
21
+ * the shop by, and a hop in front of the shop — a proxy, an ingress, the edge —
22
+ * changes it while the channel and its board stay the same (factory task
23
+ * 2375). A laptop that pulled through one route pushes through another onto
24
+ * the base it pulled.
17
25
  */
18
26
  export class SyncRecordFile {
19
- channel;
20
27
  path;
28
+ channel;
21
29
  /**
22
30
  * @param storefront the storefront directory.
23
- * @param channel the digest naming the channel this run syncs against.
31
+ * @param channelToken the token of the channel this run syncs against, kept only as its digest.
24
32
  */
25
- constructor(storefront, channel) {
26
- this.channel = channel;
33
+ constructor(storefront, channelToken) {
27
34
  this.path = join(storefront, ...SYNC_STATE_PATH, RECORD_FILE);
35
+ this.channel = createHash('sha256').update(channelToken).digest('hex');
28
36
  }
29
37
  /** This channel's last sync — null before the first, or after a sync against another channel. */
30
38
  async read() {
@@ -34,7 +42,7 @@ export class SyncRecordFile {
34
42
  const record = this.parse(raw);
35
43
  return record.channel === this.channel ? record : null;
36
44
  }
37
- /** Record a sync that left the laptop on `servingSha`, with the `.env` lines as `env` digests them. */
45
+ /** Record a sync that left the laptop on `servingSha`, with the env files' lines as `env` digests them. */
38
46
  async write(servingSha, env) {
39
47
  const record = { channel: this.channel, servingSha, env: { ...env } };
40
48
  await writeFile(this.path, `${JSON.stringify(record, null, 2)}\n`, 'utf8');
@@ -59,10 +67,22 @@ export class SyncRecordFile {
59
67
  }
60
68
  return { channel, servingSha, env: this.parseEnv(env) };
61
69
  }
70
+ /** The env digests, file by file: every file one a storefront reads, every digest a string. */
62
71
  parseEnv(env) {
63
72
  if (!isJsonObject(env))
64
73
  throw new Error(this.unreadableMessage());
65
- const entries = Object.entries(env);
74
+ const digests = {};
75
+ for (const [file, fileDigests] of Object.entries(env)) {
76
+ if (!isStorefrontEnvFile(file))
77
+ throw new Error(this.unreadableMessage());
78
+ digests[file] = this.parseFileDigests(fileDigests);
79
+ }
80
+ return digests;
81
+ }
82
+ parseFileDigests(fileDigests) {
83
+ if (!isJsonObject(fileDigests))
84
+ throw new Error(this.unreadableMessage());
85
+ const entries = Object.entries(fileDigests);
66
86
  if (!entries.every((entry) => typeof entry[1] === 'string')) {
67
87
  throw new Error(this.unreadableMessage());
68
88
  }
@@ -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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"}
@@ -60,12 +60,17 @@ export declare class SyncRepository {
60
60
  /**
61
61
  * The ONE commit a push sends: the laptop's tree on the serving tip `serving`,
62
62
  * keeping the platform's `.forgecart/` the serving tree carries — the laptop's
63
- * own never travels.
63
+ * own never travels. It is dated as the serving tip it is cut on, so the same
64
+ * tree cut on the same tip is the same commit on every run: a run that
65
+ * re-sends a push an earlier run broke off sends the very bundle the
66
+ * workspace holds parts of. The pod mints the landed commit itself.
64
67
  */
65
68
  pushCommit(localTree: string, serving: string): Promise<string>;
66
69
  /**
67
70
  * `commit` as the git bundle a push sends: its one ref `refs/heads/serving`,
68
- * its one prerequisite the serving tip `serving` it was cut on.
71
+ * its one prerequisite the serving tip `serving` it was cut on. The pack is
72
+ * written by one thread, so the same commit is the same bundle, byte for
73
+ * byte, on every run.
69
74
  */
70
75
  pushBundle(commit: string, serving: string): Promise<Buffer>;
71
76
  /** The top-level entries of `tree`, in `ls-tree -z` form — the form `mktree -z` reads back. */
@@ -188,23 +188,41 @@ export class SyncRepository {
188
188
  /**
189
189
  * The ONE commit a push sends: the laptop's tree on the serving tip `serving`,
190
190
  * keeping the platform's `.forgecart/` the serving tree carries — the laptop's
191
- * own never travels.
191
+ * own never travels. It is dated as the serving tip it is cut on, so the same
192
+ * tree cut on the same tip is the same commit on every run: a run that
193
+ * re-sends a push an earlier run broke off sends the very bundle the
194
+ * workspace holds parts of. The pod mints the landed commit itself.
192
195
  */
193
196
  async pushCommit(localTree, serving) {
194
197
  const local = await this.topLevelEntries(localTree);
195
198
  const platform = (await this.topLevelEntries(`${serving}^{tree}`)).filter((entry) => this.entryName(entry) === PLATFORM_DIR);
196
199
  const tree = await this.makeTree([...local, ...platform]);
197
- return this.line(await this.git(['commit-tree', tree, '-p', serving, '-m', PUSH_MESSAGE]));
200
+ const servingDate = await this.git(['log', '-1', '--format=%cd', '--date=raw', serving]);
201
+ const date = `@${this.line(servingDate)}`;
202
+ const run = await this.git(['commit-tree', tree, '-p', serving, '-m', PUSH_MESSAGE], {
203
+ env: { GIT_AUTHOR_DATE: date, GIT_COMMITTER_DATE: date },
204
+ });
205
+ return this.line(run);
198
206
  }
199
207
  /**
200
208
  * `commit` as the git bundle a push sends: its one ref `refs/heads/serving`,
201
- * its one prerequisite the serving tip `serving` it was cut on.
209
+ * its one prerequisite the serving tip `serving` it was cut on. The pack is
210
+ * written by one thread, so the same commit is the same bundle, byte for
211
+ * byte, on every run.
202
212
  */
203
213
  async pushBundle(commit, serving) {
204
214
  await this.git(['update-ref', PUSH_REF, commit]);
205
215
  const bundlePath = this.spoolPath('push');
206
216
  try {
207
- await this.git(['bundle', 'create', bundlePath, PUSH_REF, `^${serving}`]);
217
+ await this.git([
218
+ '-c',
219
+ 'pack.threads=1',
220
+ 'bundle',
221
+ 'create',
222
+ bundlePath,
223
+ PUSH_REF,
224
+ `^${serving}`,
225
+ ]);
208
226
  return await readFile(bundlePath);
209
227
  }
210
228
  finally {
@@ -297,7 +315,10 @@ export class SyncRepository {
297
315
  * caller reads it as an answer.
298
316
  */
299
317
  async git(args, options = {}) {
300
- const env = this.repositoryEnv(options.indexFile ?? join(this.gitDir, INDEX_FILE));
318
+ const env = {
319
+ ...this.repositoryEnv(options.indexFile ?? join(this.gitDir, INDEX_FILE)),
320
+ ...options.env,
321
+ };
301
322
  const run = await this.spawnGit([...this.gitFlags(), ...args], env, options);
302
323
  if (options.allowFailure !== true)
303
324
  this.assertSucceeded(args, run);