@omega.js/desktop 0.53.0 → 0.54.1
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/README.md +38 -38
- package/dist/cli-run.js +4 -1
- package/dist/cli.js +2 -2
- package/dist/commands/cdp/client.js +1 -1
- package/dist/commands/cdp.js +1 -1
- package/dist/commands/clean.js +2 -3
- package/dist/commands/dev.js +25 -0
- package/dist/commands/lib/ensure-target.js +12 -17
- package/dist/commands/lib/migrate.js +17 -0
- package/dist/commands/logs.js +1 -1
- package/dist/commands/release.js +1 -1
- package/dist/commands/test.js +4 -4
- package/dist/commands/update.js +5 -4
- package/dist/defaults/.github/workflows/build.yml +18 -18
- package/dist/defaults/_.gitignore +0 -2
- package/dist/defaults/_mas/README.md +3 -3
- package/dist/defaults/config/certs/README.md +1 -1
- package/dist/defaults/config/omega.json5 +36 -36
- package/dist/defaults/docs/README.md +3 -3
- package/dist/defaults/gulpfile.js +1 -1
- package/dist/defaults/hooks/build/post.js +1 -1
- package/dist/defaults/hooks/build/pre.js +1 -1
- package/dist/defaults/hooks/notarize/post.js +2 -2
- package/dist/defaults/hooks/release/post.js +1 -1
- package/dist/defaults/hooks/release/pre.js +1 -1
- package/dist/defaults/src/assets/scss/pages/about.scss +1 -1
- package/dist/defaults/src/assets/scss/pages/main.scss +1 -1
- package/dist/defaults/src/assets/scss/pages/settings.scss +1 -1
- package/dist/defaults/src/integrations/context-menu/index.js +11 -11
- package/dist/defaults/src/integrations/menu/index.js +5 -5
- package/dist/defaults/src/integrations/tray/index.js +9 -9
- package/dist/defaults/src/main.js +2 -2
- package/dist/defaults/src/preload.js +1 -1
- package/dist/defaults/test/README.md +3 -3
- package/dist/defaults/test/_init.js +1 -1
- package/dist/gulp/tasks/audit.js +5 -8
- package/dist/lib/restart-manager/index.js +1 -1
- package/dist/lib/restart-manager/install.js +1 -1
- package/dist/lib/restart-manager/protocol.js +1 -1
- package/dist/main.js +4 -3
- package/dist/preload.js +1 -1
- package/dist/test/suites/build/audit.test.js +20 -7
- package/dist/test/suites/build/build-workflow-jobs.test.js +2 -2
- package/dist/test/suites/build/cli.test.js +28 -0
- package/dist/test/suites/build/defaults-em-dash.test.js +22 -0
- package/dist/test/suites/build/defaults-scaffold.test.js +19 -5
- package/dist/test/suites/build/deploy-direct.test.js +7 -5
- package/dist/test/suites/build/deploy-dispatch.test.js +2 -1
- package/dist/test/suites/build/deploy-hook.test.js +4 -2
- package/dist/test/suites/build/dev-verb.test.js +67 -0
- package/dist/test/suites/build/ensure-target.test.js +11 -3
- package/dist/test/suites/build/merge-line-files.test.js +6 -6
- package/dist/test/suites/build/migrate.test.js +29 -0
- package/dist/test/suites/build/project-scripts-deps.test.js +6 -10
- package/dist/test/suites/build/runner-env-write.test.js +73 -0
- package/dist/test/suites/build/runner.test.js +9 -8
- package/dist/test/suites/build/setup-scripts.test.js +27 -0
- package/dist/test/suites/build/validate-config.test.js +13 -2
- package/dist/test/suites/build/verb-logs.test.js +20 -0
- package/dist/test/suites/renderer/window-desktop-surface.test.js +1 -1
- package/dist/utils/build-pipeline.js +4 -4
- package/dist/utils/runner-env.js +13 -28
- package/dist/vendor/config/company.js +46 -14
- package/dist/vendor/config/defaults.js +30 -7
- package/dist/vendor/config/edit.js +25 -3
- package/dist/vendor/config/env-delivery.js +1 -1
- package/dist/vendor/config/env-schema.js +3 -6
- package/dist/vendor/config/env.js +34 -22
- package/dist/vendor/config/index.js +13 -17
- package/dist/vendor/config/load.js +15 -7
- package/dist/vendor/config/repo.js +10 -27
- package/dist/vendor/config/schema-client.js +64 -0
- package/dist/vendor/config/schema-cloud.js +38 -0
- package/dist/vendor/config/schema-manager.js +118 -0
- package/dist/vendor/config/schema-overrides.js +68 -0
- package/dist/vendor/config/schema.js +99 -152
- package/dist/vendor/config/validate.js +97 -77
- package/dist/vendor/devkit/agents-md.js +233 -0
- package/dist/vendor/devkit/attach-log-file.js +15 -1
- package/dist/vendor/devkit/ci-workflows.js +30 -30
- package/dist/vendor/devkit/cli-router.js +13 -7
- package/dist/vendor/devkit/defaults-engine.js +9 -43
- package/dist/vendor/devkit/deploy-snapshot.js +44 -9
- package/dist/vendor/devkit/env-lines.js +183 -0
- package/dist/vendor/devkit/local.js +62 -10
- package/dist/vendor/devkit/lockfile.js +32 -13
- package/dist/vendor/devkit/logger.js +7 -2
- package/dist/vendor/devkit/merge-line-files.js +219 -176
- package/dist/vendor/devkit/omega-bin.js +208 -111
- package/dist/vendor/devkit/preludes/docs-sync.js +52 -0
- package/dist/vendor/devkit/preludes/index.js +1 -0
- package/dist/vendor/devkit/target-picker.js +45 -0
- package/dist/vendor/devkit/test/dashed-files.js +37 -0
- package/dist/vendor/devkit/test/run-verb-under-tee.js +71 -0
- package/dist/vendor/devkit/update.js +15 -15
- package/dist/vendor/devkit/verb-scripts.js +40 -0
- package/dist/vendor/devkit/verbs.js +170 -0
- package/package.json +18 -24
- package/dist/commands/install.js +0 -37
- package/dist/defaults/AGENTS.md +0 -119
- package/dist/defaults/CLAUDE.md +0 -1
- package/dist/vendor/config/env-retired.js +0 -137
- package/dist/vendor/config/retired-keys.js +0 -635
- package/docs/analytics.md +0 -140
- package/docs/app-state.md +0 -92
- package/docs/audit.md +0 -69
- package/docs/auth.md +0 -284
- package/docs/auto-updater.md +0 -243
- package/docs/boot-sequence.md +0 -44
- package/docs/build-system.md +0 -169
- package/docs/cdp-debugging.md +0 -169
- package/docs/common-mistakes.md +0 -21
- package/docs/config-schema.md +0 -120
- package/docs/context-menu.md +0 -112
- package/docs/context.md +0 -81
- package/docs/css.md +0 -84
- package/docs/deep-link.md +0 -186
- package/docs/environment-detection.md +0 -112
- package/docs/fontawesome.md +0 -109
- package/docs/hooks.md +0 -89
- package/docs/icons.md +0 -79
- package/docs/index.md +0 -328
- package/docs/installer-options.md +0 -165
- package/docs/ipc.md +0 -61
- package/docs/lib-modules.md +0 -53
- package/docs/logging.md +0 -227
- package/docs/menu.md +0 -160
- package/docs/releasing.md +0 -239
- package/docs/remote-config.md +0 -118
- package/docs/remote-scripts.md +0 -144
- package/docs/restart-manager.md +0 -144
- package/docs/runner.md +0 -290
- package/docs/sentry.md +0 -97
- package/docs/shared/agent-docs.md +0 -89
- package/docs/shared/analytics.md +0 -612
- package/docs/shared/brands.md +0 -57
- package/docs/shared/breaking-changes.md +0 -917
- package/docs/shared/config.md +0 -1948
- package/docs/shared/deploys.md +0 -341
- package/docs/shared/icons.md +0 -219
- package/docs/shared/local-dev.md +0 -167
- package/docs/shared/logging.md +0 -205
- package/docs/shared/monitoring.md +0 -167
- package/docs/shared/publishing.md +0 -187
- package/docs/shared/rulings.md +0 -34
- package/docs/shared/testing.md +0 -147
- package/docs/shared/theming.md +0 -629
- package/docs/shared/translation.md +0 -342
- package/docs/shared/updates.md +0 -61
- package/docs/signing.md +0 -293
- package/docs/startup.md +0 -142
- package/docs/storage.md +0 -59
- package/docs/templating.md +0 -101
- package/docs/test-boot-layer.md +0 -157
- package/docs/test-framework.md +0 -362
- package/docs/themes.md +0 -149
- package/docs/tooltips.md +0 -99
- package/docs/tray.md +0 -164
- package/docs/usage.md +0 -58
- package/docs/verts.md +0 -62
- package/docs/windows.md +0 -149
|
@@ -1,72 +1,87 @@
|
|
|
1
|
-
// Merge line-based files (.env, .gitignore, AGENTS.md)
|
|
2
|
-
// the OMEGA marker
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
// comment token per file type; the rules-file managed block is the `//` flavor of the
|
|
10
|
-
// same family, owned by @omega.js/backend — see _attic/plans/archive/marker-harmonization.md):
|
|
11
|
-
//
|
|
12
|
-
// # ========== Default Values ==========
|
|
13
|
-
// # framework-managed; overwritten on every setup
|
|
14
|
-
// KEY1=
|
|
15
|
-
// KEY2="value with spaces"
|
|
16
|
-
//
|
|
17
|
-
// # ========== Custom Values ==========
|
|
18
|
-
// # user's section; preserved verbatim across setups
|
|
19
|
-
// USER_SECRET="my-secret"
|
|
20
|
-
//
|
|
21
|
-
// Behavior:
|
|
22
|
-
// - The Default section is replaced with the new framework's defaults.
|
|
23
|
-
// - Keys that already had values (in either Default or Custom) keep those values
|
|
24
|
-
// in the same section they were in.
|
|
25
|
-
// - The Default section is COMPLETELY managed ([#926](https://github.com/Omega-JS-Stack/omega/issues/926)):
|
|
26
|
-
// a line the new framework block no longer carries LEAVES the consumer, so a
|
|
27
|
-
// rule the framework retires or rewrites actually takes effect everywhere. A
|
|
28
|
-
// retired framework line and a line a user typed inside the framework block
|
|
29
|
-
// cannot be told apart, and the block header already says it is overwritten on
|
|
30
|
-
// every setup, so `.gitignore` and `AGENTS.md` drop it. Lines a user owns
|
|
31
|
-
// belong under the Custom marker, which stays verbatim.
|
|
32
|
-
// - .env is the one exception: a retired key holding a NON-EMPTY value migrates
|
|
33
|
-
// to the Custom section (that value is data, not a rule), while an empty
|
|
34
|
-
// retired key (`KEY=` / `KEY=""`) drops like any other retired line.
|
|
35
|
-
// - .env values are normalized to **double-quoted** form on every merge:
|
|
36
|
-
// KEY=raw-value → KEY="raw-value"
|
|
37
|
-
// KEY="already-quoted" → KEY="already-quoted" (left alone)
|
|
38
|
-
// KEY= → KEY= (empty stays empty/unquoted)
|
|
39
|
-
// This protects values containing spaces, #, $, or other shell-meaningful chars.
|
|
40
|
-
// - .env COMMENTED PLACEHOLDERS (`# KEY=` in the template's Default section)
|
|
41
|
-
// mark keys the framework knows but ships no value for (the env cascade
|
|
42
|
-
// supplies values from stronger layers — dogfood friction #20). On merge:
|
|
43
|
-
// an existing NON-EMPTY value for that key (either section) keeps its line
|
|
44
|
-
// in Default; an existing EMPTY value (`KEY=` / `KEY=""`) converges to the
|
|
45
|
-
// commented placeholder instead of migrating to Custom.
|
|
46
|
-
// - .gitignore: same logic, line-based instead of key-based (no quoting).
|
|
47
|
-
// - AGENTS.md: same logic as .gitignore (line-based, no quoting). The markers
|
|
48
|
-
// render as visible H1 headings in markdown — that's intentional UX.
|
|
49
|
-
// - First setup (no existing file): the framework template lands as-is.
|
|
1
|
+
// Merge line-based files (.env, .gitignore, .gitattributes, AGENTS.md) through
|
|
2
|
+
// the OMEGA marker sections: the framework rewrites Default on every run, the
|
|
3
|
+
// consumer's Custom stays verbatim, and a file with no markers yet converges
|
|
4
|
+
// once, its consumer lines landing under Custom. The protocol (the grammar and its
|
|
5
|
+
// flavors, the .env value rules, placeholders, promotion, the first converge)
|
|
6
|
+
// lives in docs/devkit/index.md.
|
|
7
|
+
|
|
8
|
+
const { joinEnvUnits, envKey, normalizeEnvLine, findKeyLine, parsePlaceholderKey, isMachineComment, envValueIsEmpty } = require('./env-lines');
|
|
50
9
|
|
|
51
10
|
const DEFAULT_MARKER = '# ========== Default Values ==========';
|
|
52
11
|
const CUSTOM_MARKER = '# ========== Custom Values ==========';
|
|
53
12
|
|
|
13
|
+
// The markdown flavor of the one grammar: an HTML comment renders as nothing.
|
|
14
|
+
const MARKDOWN_DEFAULT_MARKER = '<!-- ========== Default Values ========== -->';
|
|
15
|
+
const MARKDOWN_CUSTOM_MARKER = '<!-- ========== Custom Values ========== -->';
|
|
16
|
+
|
|
17
|
+
// Framework text earlier generations wrote, one pattern per line, each block
|
|
18
|
+
// scoped to the file type that shipped it: Custom boilerplate (the retired
|
|
19
|
+
// per-framework AGENTS.md's included), and old .env headers and group notes,
|
|
20
|
+
// which only an unmarked .env's first converge strips.
|
|
21
|
+
const SHIPPED = [
|
|
22
|
+
{ file: '.gitignore', lines: [/^# Add your custom ignore patterns below this line$/, /^# \.\.\.$/] },
|
|
23
|
+
{ file: '.gitignore', lines: [/^# Add your own ignores below\. This section is preserved across (framework re-syncs|`npx omega setup` runs|`npx mgr setup` runs)\.$/] },
|
|
24
|
+
{ file: '.gitignore', lines: [/^# \.\.\.$/] },
|
|
25
|
+
{ file: 'AGENTS.md', lines: [/^## Project-specific notes$/, /^Add anything specific to THIS project here\. Edits below this line are preserved across (runs|framework re-syncs|`npx omega setup` runs)\.$/] },
|
|
26
|
+
{ file: 'AGENTS.md', lines: [/^<!-- Add your project-specific notes below this line -->$/] },
|
|
27
|
+
{
|
|
28
|
+
file: '.env',
|
|
29
|
+
lines: [
|
|
30
|
+
/^# .+(?: \u2014|:) brand secrets \(gitignored; loaded before every omega(?:-manager)? run\)\.$/,
|
|
31
|
+
/^# Uncomment and fill what this brand uses\. Services without their credentials$/,
|
|
32
|
+
/^# skip cleanly, so add these as the brand adopts each service\.$/,
|
|
33
|
+
],
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
file: '.env',
|
|
37
|
+
lines: [
|
|
38
|
+
/^# .+(?: \u2014|:) COMPANY secrets \(gitignored; loaded UNDER every (?:managed brand's own \.env|brand of this company)\)\.$/,
|
|
39
|
+
/^# Precedence: shell env > brand \.env > this file\. Put here only what every brand$/,
|
|
40
|
+
/^# shares(?: \u2014 anything|\. Anything) brand-specific belongs in that brand's \.env, never here\.$/,
|
|
41
|
+
],
|
|
42
|
+
},
|
|
43
|
+
{
|
|
44
|
+
file: '.env',
|
|
45
|
+
lines: [
|
|
46
|
+
/^# one per stream; disperse composes each target its own GOOGLE_ANALYTICS_SECRET\), and the$/,
|
|
47
|
+
/^# Auto-generated and persisted here on the first real run \u2014 leave unset:$/,
|
|
48
|
+
/^# ACCOUNT_PASSWORD_SEED, CSC_KEY_PASSWORD$/,
|
|
49
|
+
],
|
|
50
|
+
},
|
|
51
|
+
];
|
|
52
|
+
|
|
54
53
|
function mergeLineBasedFiles(existingContent, newContent, fileName) {
|
|
55
54
|
const isEnvFile = fileName === '.env';
|
|
55
|
+
const markers = sectionMarkers(fileName);
|
|
56
56
|
|
|
57
|
-
const
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
const
|
|
62
|
-
= splitSections(existingLines, isEnvFile);
|
|
57
|
+
const hasMarkers = hasSectionMarkers(existingContent, fileName);
|
|
58
|
+
// .env merges by logical line: a quoted value spanning lines is one unit
|
|
59
|
+
const toLines = (content) => (isEnvFile ? joinEnvUnits(content.split('\n')) : content.split('\n'));
|
|
60
|
+
const existingLines = toLines(existingContent);
|
|
61
|
+
const newLines = toLines(newContent);
|
|
63
62
|
|
|
64
63
|
// Parse new content. We only use its default section (custom is the user's domain).
|
|
65
|
-
const { defaultLines: newDefault
|
|
64
|
+
const { defaultLines: newDefault } = splitSections(newLines, markers);
|
|
65
|
+
const newDefaultKeys = new Set(isEnvFile ? newDefault.map((line) => envKey(line) || parsePlaceholderKey(line.trim())).filter(Boolean) : []);
|
|
66
|
+
|
|
67
|
+
let existingDefault = [];
|
|
68
|
+
let existingCustom = [];
|
|
69
|
+
if (hasMarkers) {
|
|
70
|
+
const sections = splitSections(existingLines, markers);
|
|
71
|
+
existingDefault = sections.defaultLines;
|
|
72
|
+
const shipped = !isEnvFile && isShippedBlock(sections.customLines, fileName);
|
|
73
|
+
existingCustom = withPreamble(sections.preamble, shipped ? [''] : sections.customLines);
|
|
74
|
+
} else if (isEnvFile) {
|
|
75
|
+
existingDefault = existingLines.filter((line) => !isMarkerLine(line, markers));
|
|
76
|
+
} else {
|
|
77
|
+
existingCustom = convergeUnmarked(existingLines, newDefault, markers, fileName);
|
|
78
|
+
}
|
|
79
|
+
const keysOf = (lines) => new Set(isEnvFile ? lines.map(envKey).filter(Boolean) : []);
|
|
80
|
+
const existingDefaultKeys = keysOf(existingDefault);
|
|
81
|
+
const existingCustomKeys = keysOf(existingCustom);
|
|
66
82
|
|
|
67
83
|
// Build the merged default section: walk new defaults in order, substituting the
|
|
68
84
|
// user's existing value for any key they had set in either section.
|
|
69
|
-
const newDefaultKeys = new Set();
|
|
70
85
|
const mergedDefault = [];
|
|
71
86
|
const emit = (line) => mergedDefault.push(isEnvFile ? normalizeEnvLine(line) : line);
|
|
72
87
|
|
|
@@ -74,9 +89,8 @@ function mergeLineBasedFiles(existingContent, newContent, fileName) {
|
|
|
74
89
|
const trimmed = line.trim();
|
|
75
90
|
|
|
76
91
|
if (isEnvFile && trimmed && !trimmed.startsWith('#')) {
|
|
77
|
-
const key =
|
|
92
|
+
const key = envKey(line);
|
|
78
93
|
if (key) {
|
|
79
|
-
newDefaultKeys.add(key);
|
|
80
94
|
if (existingDefaultKeys.has(key)) {
|
|
81
95
|
emit(findKeyLine(existingDefault, key));
|
|
82
96
|
continue;
|
|
@@ -94,13 +108,11 @@ function mergeLineBasedFiles(existingContent, newContent, fileName) {
|
|
|
94
108
|
// .gitignore: just keep the new line.
|
|
95
109
|
mergedDefault.push(line);
|
|
96
110
|
} else {
|
|
97
|
-
// Comment
|
|
111
|
+
// Comment or blank. For .env, a `# KEY=""` placeholder claims the key:
|
|
98
112
|
// an existing set value keeps its line, an empty one converges away.
|
|
99
113
|
const placeholderKey = isEnvFile ? parsePlaceholderKey(trimmed) : null;
|
|
100
114
|
|
|
101
115
|
if (placeholderKey) {
|
|
102
|
-
newDefaultKeys.add(placeholderKey);
|
|
103
|
-
|
|
104
116
|
const existingLine = existingDefaultKeys.has(placeholderKey)
|
|
105
117
|
? findKeyLine(existingDefault, placeholderKey)
|
|
106
118
|
: existingCustomKeys.has(placeholderKey)
|
|
@@ -117,159 +129,175 @@ function mergeLineBasedFiles(existingContent, newContent, fileName) {
|
|
|
117
129
|
}
|
|
118
130
|
}
|
|
119
131
|
|
|
120
|
-
//
|
|
121
|
-
//
|
|
122
|
-
//
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
const key = trimmed.split('=')[0].trim();
|
|
134
|
-
if (key && !newDefaultKeys.has(key) && !existingCustomKeys.has(key) && !envValueIsEmpty(trimmed)) {
|
|
135
|
-
migratedToCustom.push(normalizeEnvLine(line));
|
|
136
|
-
}
|
|
137
|
-
}
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
// The user's Custom section is preserved — except (a) .env values get normalized
|
|
141
|
-
// to double-quoted form for consistent quoting, and (b) any key that was promoted
|
|
142
|
-
// UP into the Default section above is dropped here so it isn't duplicated. Keys
|
|
143
|
-
// present in BOTH sections are NOT dropped (the Default copy's value won there,
|
|
144
|
-
// so removing the Custom copy would silently change dotenv's effective value).
|
|
132
|
+
// A line the new framework block does not carry drops, except an .env key
|
|
133
|
+
// holding a value: the consumer's data, so it moves under Custom. An unmarked
|
|
134
|
+
// .env's first converge also carries every consumer comment there.
|
|
135
|
+
const known = new Set([...newDefaultKeys, ...existingCustomKeys]);
|
|
136
|
+
const migratedToCustom = !isEnvFile
|
|
137
|
+
? []
|
|
138
|
+
: hasMarkers
|
|
139
|
+
? existingDefault.filter((line) => isValuedUnknown(line, known)).map(normalizeEnvLine)
|
|
140
|
+
: convergeUnmarkedEnv(existingDefault, newDefault, known);
|
|
141
|
+
|
|
142
|
+
// Custom stays verbatim but for .env quoting and dropping a key promoted UP
|
|
143
|
+
// into Default. A key set in both sections keeps both lines: dotenv reads the
|
|
144
|
+
// last one, the Custom line, so dropping it would change the value.
|
|
145
145
|
const finalCustom = isEnvFile
|
|
146
146
|
? existingCustom
|
|
147
147
|
.filter((line) => {
|
|
148
148
|
const trimmed = line.trim();
|
|
149
149
|
if (!trimmed || trimmed.startsWith('#')) return true;
|
|
150
|
-
const key =
|
|
150
|
+
const key = envKey(line);
|
|
151
151
|
return !(key && newDefaultKeys.has(key) && !existingDefaultKeys.has(key));
|
|
152
152
|
})
|
|
153
153
|
.map((line) => normalizeEnvLine(line))
|
|
154
154
|
: existingCustom;
|
|
155
155
|
|
|
156
156
|
const result = [];
|
|
157
|
-
result.push(
|
|
157
|
+
result.push(markers.defaultMarker);
|
|
158
158
|
result.push(...mergedDefault);
|
|
159
159
|
// Insert a single blank line before CUSTOM_MARKER, but only if the merged default
|
|
160
160
|
// doesn't already end with one (otherwise we'd accumulate an extra blank line on
|
|
161
|
-
// every merge
|
|
161
|
+
// every merge, breaking idempotency on the first re-run after a fresh `jetpack.copy`).
|
|
162
162
|
if (mergedDefault.length === 0 || mergedDefault[mergedDefault.length - 1].trim() !== '') {
|
|
163
163
|
result.push('');
|
|
164
164
|
}
|
|
165
|
-
result.push(
|
|
165
|
+
result.push(markers.customMarker);
|
|
166
166
|
if (migratedToCustom.length > 0) {
|
|
167
167
|
result.push(...migratedToCustom);
|
|
168
168
|
}
|
|
169
169
|
result.push(...finalCustom);
|
|
170
|
+
// A converged file ends on a newline like every marked one
|
|
171
|
+
if (!hasMarkers && result[result.length - 1] !== '') {
|
|
172
|
+
result.push('');
|
|
173
|
+
}
|
|
170
174
|
|
|
171
175
|
return result.join('\n');
|
|
172
176
|
}
|
|
173
177
|
|
|
174
|
-
//
|
|
175
|
-
//
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
178
|
+
// Split a file on its marker lines: the lines above the first marker, the
|
|
179
|
+
// Default section and the Custom section.
|
|
180
|
+
function splitSections(lines, markers) {
|
|
181
|
+
const sections = { preamble: [], defaultLines: [], customLines: [] };
|
|
182
|
+
let into = sections.preamble;
|
|
183
|
+
for (const line of lines) {
|
|
184
|
+
const trimmed = line.trim();
|
|
185
|
+
if (trimmed === markers.defaultMarker) { into = sections.defaultLines; continue; }
|
|
186
|
+
if (trimmed === markers.customMarker) { into = sections.customLines; continue; }
|
|
187
|
+
into.push(line);
|
|
188
|
+
}
|
|
189
|
+
return sections;
|
|
190
|
+
}
|
|
182
191
|
|
|
183
|
-
|
|
192
|
+
function isMarkerLine(line, markers) {
|
|
184
193
|
const trimmed = line.trim();
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
// Capture leading whitespace so we don't lose indentation.
|
|
188
|
-
const leadingMatch = line.match(/^(\s*)/);
|
|
189
|
-
const leading = leadingMatch ? leadingMatch[1] : '';
|
|
194
|
+
return trimmed === markers.defaultMarker || trimmed === markers.customMarker;
|
|
195
|
+
}
|
|
190
196
|
|
|
191
|
-
|
|
192
|
-
|
|
197
|
+
// Blank lines at either end dropped, a run of blanks inside collapsed to one.
|
|
198
|
+
function tidyParagraphs(lines) {
|
|
199
|
+
const tidy = [];
|
|
200
|
+
for (const line of lines) {
|
|
201
|
+
if (!line.trim() && (tidy.length === 0 || !tidy[tidy.length - 1].trim())) continue;
|
|
202
|
+
tidy.push(line);
|
|
203
|
+
}
|
|
204
|
+
if (tidy.length > 0 && !tidy[tidy.length - 1].trim()) tidy.pop();
|
|
205
|
+
return tidy;
|
|
206
|
+
}
|
|
193
207
|
|
|
194
|
-
|
|
195
|
-
|
|
208
|
+
// A marked file's lines above its Default marker open its Custom section.
|
|
209
|
+
function withPreamble(preamble, custom) {
|
|
210
|
+
const lead = tidyParagraphs(preamble);
|
|
211
|
+
if (lead.length === 0) return custom;
|
|
212
|
+
const start = custom.findIndex((line) => line.trim());
|
|
213
|
+
return start < 0 ? [...lead, ''] : [...lead, '', ...custom.slice(start)];
|
|
214
|
+
}
|
|
196
215
|
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
216
|
+
// A marker-less file's first converge: paragraph by paragraph, a line the new
|
|
217
|
+
// framework block carries is the framework's, a paragraph whose every entry is
|
|
218
|
+
// one goes whole (its comments were a framework header), and the rest land
|
|
219
|
+
// under Custom in order, one blank line between paragraphs.
|
|
220
|
+
function convergeUnmarked(lines, newDefault, markers, fileName) {
|
|
221
|
+
const framework = new Set(newDefault.map((line) => line.trim()).filter(Boolean));
|
|
222
|
+
// The grammar's comment token opens every marker: `#`, or `<!--` in markdown
|
|
223
|
+
const commentToken = markers.defaultMarker.split(' ')[0];
|
|
224
|
+
const paragraphs = [[]];
|
|
225
|
+
for (const line of lines) {
|
|
226
|
+
const trimmed = line.trim();
|
|
227
|
+
if (isMarkerLine(line, markers)) continue;
|
|
228
|
+
if (trimmed) paragraphs[paragraphs.length - 1].push(line);
|
|
229
|
+
else if (paragraphs[paragraphs.length - 1].length > 0) paragraphs.push([]);
|
|
201
230
|
}
|
|
202
231
|
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
232
|
+
const custom = [];
|
|
233
|
+
for (const paragraph of paragraphs) {
|
|
234
|
+
const entries = paragraph.filter((line) => !line.trim().startsWith(commentToken));
|
|
235
|
+
if (entries.length > 0 && entries.every((line) => framework.has(line.trim()))) continue;
|
|
207
236
|
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
237
|
+
const kept = paragraph.filter((line) => !framework.has(line.trim()) && !isShipped(line, fileName));
|
|
238
|
+
if (kept.length === 0) continue;
|
|
239
|
+
if (custom.length > 0) custom.push('');
|
|
240
|
+
custom.push(...kept);
|
|
212
241
|
}
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
return `${leading}${key}="${escapeForDoubleQuote(value)}"`;
|
|
242
|
+
custom.push('');
|
|
243
|
+
return custom;
|
|
216
244
|
}
|
|
217
245
|
|
|
218
|
-
|
|
219
|
-
|
|
246
|
+
// An .env key the framework block does not know, holding a value.
|
|
247
|
+
function isValuedUnknown(line, known) {
|
|
248
|
+
const key = envKey(line);
|
|
249
|
+
return Boolean(key) && !known.has(key) && !envValueIsEmpty(line.trim());
|
|
220
250
|
}
|
|
221
251
|
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
const
|
|
227
|
-
|
|
228
|
-
let
|
|
252
|
+
// An unmarked .env's first converge, in file order with its paragraphs: every
|
|
253
|
+
// comment but the framework's own and a run directly above a known key or its
|
|
254
|
+
// placeholder, and every valued key the framework block does not know.
|
|
255
|
+
function convergeUnmarkedEnv(lines, newDefault, known) {
|
|
256
|
+
const framework = new Set(newDefault.map((line) => line.trim()).filter(Boolean));
|
|
257
|
+
const kept = [];
|
|
258
|
+
let run = [];
|
|
229
259
|
for (const line of lines) {
|
|
230
260
|
const trimmed = line.trim();
|
|
231
|
-
if (trimmed
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
// Lines before any marker are treated as default (legacy / fresh files).
|
|
235
|
-
if (mode === 'custom') {
|
|
236
|
-
customLines.push(line);
|
|
237
|
-
if (isEnvFile && trimmed && !trimmed.startsWith('#')) {
|
|
238
|
-
const key = trimmed.split('=')[0].trim();
|
|
239
|
-
if (key) existingCustomKeys.add(key);
|
|
240
|
-
}
|
|
241
|
-
} else {
|
|
242
|
-
defaultLines.push(line);
|
|
243
|
-
if (isEnvFile && trimmed && !trimmed.startsWith('#')) {
|
|
244
|
-
const key = trimmed.split('=')[0].trim();
|
|
245
|
-
if (key) existingDefaultKeys.add(key);
|
|
246
|
-
}
|
|
261
|
+
if (trimmed.startsWith('#') && !isMachineComment(trimmed) && !framework.has(trimmed)) {
|
|
262
|
+
if (!isShipped(line, '.env')) run.push(line);
|
|
263
|
+
continue;
|
|
247
264
|
}
|
|
265
|
+
const key = envKey(line) || parsePlaceholderKey(trimmed);
|
|
266
|
+
if (!key || !known.has(key)) kept.push(...run);
|
|
267
|
+
if (isValuedUnknown(line, known)) kept.push(normalizeEnvLine(line));
|
|
268
|
+
if (!trimmed) kept.push('');
|
|
269
|
+
run = [];
|
|
248
270
|
}
|
|
249
|
-
|
|
250
|
-
return
|
|
271
|
+
kept.push(...run);
|
|
272
|
+
return tidyParagraphs(kept);
|
|
251
273
|
}
|
|
252
274
|
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
275
|
+
/**
|
|
276
|
+
* Is this line framework text an earlier generation shipped in this file type?
|
|
277
|
+
* @param {string} line
|
|
278
|
+
* @param {string} fileName - The file's name (e.g. `.gitignore`, `AGENTS.md`, `.env`)
|
|
279
|
+
* @returns {boolean}
|
|
280
|
+
*/
|
|
281
|
+
function isShipped(line, fileName) {
|
|
282
|
+
return SHIPPED.some((entry) => entry.file === fileName && entry.lines.some((shipped) => shipped.test(line.trim())));
|
|
259
283
|
}
|
|
260
284
|
|
|
261
|
-
//
|
|
262
|
-
function
|
|
263
|
-
const
|
|
264
|
-
return
|
|
285
|
+
// Is this Custom section exactly one block this file type shipped?
|
|
286
|
+
function isShippedBlock(lines, fileName) {
|
|
287
|
+
const body = lines.map((line) => line.trim()).filter(Boolean);
|
|
288
|
+
return SHIPPED.some((entry) => entry.file === fileName && entry.lines.length === body.length && entry.lines.every((shipped, i) => shipped.test(body[i])));
|
|
265
289
|
}
|
|
266
290
|
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
291
|
+
/**
|
|
292
|
+
* The marker pair a file speaks, by file type: markdown takes the HTML-comment
|
|
293
|
+
* flavor of the one grammar, every other line file the `#` flavor.
|
|
294
|
+
* @param {string} [fileName] - The file's name (e.g. `AGENTS.md`, `.gitignore`)
|
|
295
|
+
* @returns {{ defaultMarker: string, customMarker: string }}
|
|
296
|
+
*/
|
|
297
|
+
function sectionMarkers(fileName) {
|
|
298
|
+
return typeof fileName === 'string' && fileName.endsWith('.md')
|
|
299
|
+
? { defaultMarker: MARKDOWN_DEFAULT_MARKER, customMarker: MARKDOWN_CUSTOM_MARKER }
|
|
300
|
+
: { defaultMarker: DEFAULT_MARKER, customMarker: CUSTOM_MARKER };
|
|
273
301
|
}
|
|
274
302
|
|
|
275
303
|
/**
|
|
@@ -277,26 +305,41 @@ function envValueIsEmpty(line) {
|
|
|
277
305
|
* through the merge protocol at least once). Setup validators use this to
|
|
278
306
|
* distinguish legacy/no-marker files from protocol-managed ones.
|
|
279
307
|
* @param {string} content
|
|
308
|
+
* @param {string} [fileName] - Selects the marker flavor (default: the `#` flavor)
|
|
280
309
|
* @returns {boolean}
|
|
281
310
|
*/
|
|
282
|
-
function hasSectionMarkers(content) {
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
311
|
+
function hasSectionMarkers(content, fileName) {
|
|
312
|
+
if (typeof content !== 'string') return false;
|
|
313
|
+
const { defaultMarker, customMarker } = sectionMarkers(fileName);
|
|
314
|
+
const lines = content.split('\n').map((line) => line.trim());
|
|
315
|
+
const at = lines.indexOf(defaultMarker);
|
|
316
|
+
return at >= 0 && lines.indexOf(customMarker, at + 1) > at;
|
|
286
317
|
}
|
|
287
318
|
|
|
288
319
|
/**
|
|
289
320
|
* Extract the consumer-owned Custom section of a marker file (everything after
|
|
290
|
-
* the Custom marker), or '' when there is no marker. The defaults engine's
|
|
321
|
+
* the Custom marker line), or '' when there is no marker. The defaults engine's
|
|
291
322
|
* `retire` rule uses this to judge whether a per-target doc carries consumer
|
|
292
323
|
* content or is framework-owned-only.
|
|
293
324
|
* @param {string} content
|
|
325
|
+
* @param {string} [fileName] - Selects the marker flavor (default: the `#` flavor)
|
|
294
326
|
* @returns {string}
|
|
295
327
|
*/
|
|
296
|
-
function getCustomSection(content) {
|
|
328
|
+
function getCustomSection(content, fileName) {
|
|
297
329
|
if (typeof content !== 'string') return '';
|
|
298
|
-
const
|
|
299
|
-
|
|
330
|
+
const { customMarker } = sectionMarkers(fileName);
|
|
331
|
+
const lines = content.split('\n');
|
|
332
|
+
const at = lines.findIndex((line) => line.trim() === customMarker);
|
|
333
|
+
return at < 0 ? '' : ['', ...lines.slice(at + 1)].join('\n');
|
|
300
334
|
}
|
|
301
335
|
|
|
302
|
-
module.exports = {
|
|
336
|
+
module.exports = {
|
|
337
|
+
mergeLineBasedFiles,
|
|
338
|
+
normalizeEnvLine,
|
|
339
|
+
hasSectionMarkers,
|
|
340
|
+
getCustomSection,
|
|
341
|
+
sectionMarkers,
|
|
342
|
+
isShipped,
|
|
343
|
+
DEFAULT_MARKER,
|
|
344
|
+
CUSTOM_MARKER,
|
|
345
|
+
};
|