@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,39 +1,19 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Schema-driven validation for resolved omega.json5 configs.
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
* target's refinements against the same resolved top-level namespace
|
|
12
|
-
* - `targets` sanity (#886): every key is a target NAME, so it must be a
|
|
13
|
-
* dir-safe slug, and every entry must be an object declaring a `type` the
|
|
14
|
-
* framework list knows (an ARRAY is the retired multi-instance form, whose
|
|
15
|
-
* replacement is a sibling key; >1 backend target is a WARNING)
|
|
16
|
-
* - the `repo` block (#883): one `provider` from REPO_PROVIDERS, and an `org`
|
|
17
|
-
* that is a real non-empty string whenever the block is there, because the
|
|
18
|
-
* org is the owner of every repo the brand's names derive under
|
|
19
|
-
* - `hosting` (#883) is a WEB target's key: a non-web target carrying it says
|
|
20
|
-
* something nothing serves, and its provider comes from HOSTING_PROVIDERS
|
|
21
|
-
* - `cloud.config.authDomain`, when set, must be the brand's OWN host (the
|
|
22
|
-
* resolved target url, else brand.url): a firebaseapp.com value or a
|
|
23
|
-
* mismatch is an error, and demo-* (emulator-only) projects are exempt
|
|
24
|
-
* - retired keys are always errors (see retired-keys.js) — a name that was
|
|
25
|
-
* renamed outright reads as nothing at all, so it fails loudly instead of
|
|
26
|
-
* losing its settings silently
|
|
27
|
-
* - secret-shaped keys are always errors (see secrets.js) — loadConfig
|
|
28
|
-
* additionally hard-fails on them before any merge happens
|
|
29
|
-
* - keys the schema does not declare are WARNINGS (#636): the walker only
|
|
30
|
-
* ever visits declared paths, so a typo — or a live path nobody declared —
|
|
31
|
-
* used to pass in silence
|
|
2
|
+
* Schema-driven validation for resolved omega.json5 configs. runSchema walks
|
|
3
|
+
* the rules (required/type/match/enum/itemEnum, the value checks only on a
|
|
4
|
+
* PRESENT value; schema.js has the rule format). validateConfig() adds the
|
|
5
|
+
* target's refinements, the `targets` sanity (each key a dir-safe NAME, each
|
|
6
|
+
* entry a known `type`), the `repo` block and web-only `hosting`, the brand's
|
|
7
|
+
* own authDomain, the price and feature shapes, and secret-shaped keys. The
|
|
8
|
+
* schema is STRICT: a path no rule declares is an error, one line per path,
|
|
9
|
+
* naming `omega migrate`. The validator knows only the present shape; the
|
|
10
|
+
* migrate verb is the one code that knows an old name.
|
|
32
11
|
*/
|
|
33
12
|
|
|
34
|
-
const { TARGETS, SHARED_SCHEMA, TARGET_SCHEMAS } = require('./schema.js');
|
|
13
|
+
const { TARGETS, SHARED_SCHEMA, TARGET_SCHEMAS, isCustomTargetEntry } = require('./schema.js');
|
|
35
14
|
const { findSecretKeys } = require('./secrets.js');
|
|
36
|
-
const {
|
|
15
|
+
const { CLIENT_FACT_KEYS } = require('./client-config.js');
|
|
16
|
+
const { RESOLVED_COMPANY_KEYS } = require('./company.js');
|
|
37
17
|
const { isPlainObject } = require('./merge.js');
|
|
38
18
|
const { TARGET_NAME_PATTERN, TARGET_TYPES } = require('./targets.js');
|
|
39
19
|
const { REPO_PROVIDERS, HOSTING_PROVIDERS } = require('./repo.js');
|
|
@@ -43,6 +23,14 @@ const { isCountedFeature } = require('../account/features.js');
|
|
|
43
23
|
// Firebase's own default authDomain shape: a third-party host by definition
|
|
44
24
|
const FIREBASE_AUTH_DOMAIN = /\.firebaseapp\.com$/;
|
|
45
25
|
|
|
26
|
+
// The company facts the loader fills: a staged compose output carries them on
|
|
27
|
+
// purpose, and the loader refuses a TYPED one in a brand or company file.
|
|
28
|
+
const RESOLVED_PATHS = RESOLVED_COMPANY_KEYS.map((key) => `company.${key}`);
|
|
29
|
+
|
|
30
|
+
// The build facts a surface bakes AFTER the load: declared only for
|
|
31
|
+
// `decorated: true`, the re-validation of a baked config (a desktop boot).
|
|
32
|
+
const BUILD_FACT_PATHS = CLIENT_FACT_KEYS;
|
|
33
|
+
|
|
46
34
|
/**
|
|
47
35
|
* Read a dotted path out of a config object — the path resolver the schema
|
|
48
36
|
* walk and the env presence checker (env-rules.js) share.
|
|
@@ -419,24 +407,23 @@ function validateFeatures(config) {
|
|
|
419
407
|
}
|
|
420
408
|
|
|
421
409
|
/**
|
|
422
|
-
* The LEAF paths of a
|
|
423
|
-
*
|
|
424
|
-
*
|
|
425
|
-
*
|
|
426
|
-
*
|
|
427
|
-
*
|
|
428
|
-
*
|
|
429
|
-
*
|
|
430
|
-
* The `targets` subtree is skipped whole: those keys belong to a framework or,
|
|
431
|
-
* for a custom target (#603), to the brand — the targets-sanity check above is
|
|
432
|
-
* what polices that namespace.
|
|
433
|
-
*
|
|
434
|
-
* @param {object} config - The resolved config object.
|
|
410
|
+
* The LEAF paths of a config the schema does not declare. A rule declares its
|
|
411
|
+
* own path; an `object`/`array` rule opens its whole subtree only when no rule
|
|
412
|
+
* is declared beneath it (or it says `open: true`), so a section with typed
|
|
413
|
+
* keys stays closed. An empty object/array (or a `false` off switch) is a leaf,
|
|
414
|
+
* declared when the schema declares anything below it. `targets` is skipped
|
|
415
|
+
* whole: each target's own load judges its entry.
|
|
416
|
+
* @param {object} config - The config object.
|
|
435
417
|
* @param {object[]} schema - Rule array in the schema.js entry format.
|
|
418
|
+
* @param {string[]} [written] - Paths framework code wrote onto a decorated config, each open.
|
|
436
419
|
* @returns {string[]} Undeclared leaf paths, in config order.
|
|
437
420
|
*/
|
|
438
|
-
function findUndeclaredPaths(config, schema) {
|
|
439
|
-
const
|
|
421
|
+
function findUndeclaredPaths(config, schema, written = []) {
|
|
422
|
+
const paths = [...schema.map((rule) => rule.path), ...written];
|
|
423
|
+
const open = schema
|
|
424
|
+
.filter((rule) => /\b(object|array)\b/.test(rule.type || '') && (rule.open || !paths.some((other) => other.startsWith(`${rule.path}.`))))
|
|
425
|
+
.map((rule) => rule.path)
|
|
426
|
+
.concat(written);
|
|
440
427
|
const found = [];
|
|
441
428
|
|
|
442
429
|
const walk = (value, path) => {
|
|
@@ -444,9 +431,10 @@ function findUndeclaredPaths(config, schema) {
|
|
|
444
431
|
Object.keys(value).forEach((key) => walk(value[key], `${path}.${key}`));
|
|
445
432
|
return;
|
|
446
433
|
}
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
434
|
+
const declared = paths.includes(path)
|
|
435
|
+
|| open.some((rule) => path.startsWith(`${rule}.`))
|
|
436
|
+
|| paths.some((rule) => rule.startsWith(`${path}.`));
|
|
437
|
+
if (!declared) found.push(path);
|
|
450
438
|
};
|
|
451
439
|
|
|
452
440
|
Object.keys(isPlainObject(config) ? config : {})
|
|
@@ -456,25 +444,69 @@ function findUndeclaredPaths(config, schema) {
|
|
|
456
444
|
return found;
|
|
457
445
|
}
|
|
458
446
|
|
|
447
|
+
/**
|
|
448
|
+
* The rules a target's resolved config answers to: shared + its refinements.
|
|
449
|
+
* @param {string} [target] - Canonical target name; omitted = shared only.
|
|
450
|
+
* @returns {object[]}
|
|
451
|
+
*/
|
|
452
|
+
function schemaFor(target) {
|
|
453
|
+
if (target && !TARGETS.includes(target)) {
|
|
454
|
+
throw new Error(`Unknown target "${target}": must be one of [${TARGETS.join(', ')}]`);
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
return target ? [...SHARED_SCHEMA, ...TARGET_SCHEMAS[target]] : SHARED_SCHEMA;
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
/**
|
|
461
|
+
* The paths a config carries that no rule declares, for one target's view:
|
|
462
|
+
* what the strict check fails, for a caller (a converter) that drops them.
|
|
463
|
+
* @param {object} config - A resolved config (a target's keys at the top level).
|
|
464
|
+
* @param {object} [options]
|
|
465
|
+
* @param {string} [options.target]
|
|
466
|
+
* @returns {string[]} Undeclared leaf paths, in config order.
|
|
467
|
+
*/
|
|
468
|
+
function undeclaredPaths(config, options) {
|
|
469
|
+
return findUndeclaredPaths(config, schemaFor((options || {}).target), RESOLVED_PATHS);
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
/**
|
|
473
|
+
* Every path of an AUTHORED config (one omega.json5 as written) the strict
|
|
474
|
+
* schema refuses: the whole file, `targets` left to its targets, then each
|
|
475
|
+
* framework target entry the way its own load sees it (hoisted to the top
|
|
476
|
+
* level). A target's OWN file names its type, since its top level is that
|
|
477
|
+
* target's layer. A custom target's keys are its own; an unknown type is the
|
|
478
|
+
* targets check's to report.
|
|
479
|
+
* @param {object} config - One file's parsed config.
|
|
480
|
+
* @param {object} [options]
|
|
481
|
+
* @param {string} [options.target] - The type whose own file this is.
|
|
482
|
+
* @returns {string[]} Dotted paths as the file spells them.
|
|
483
|
+
*/
|
|
484
|
+
function undeclaredAuthoredPaths(config, options) {
|
|
485
|
+
const found = undeclaredPaths(config, { target: (options || {}).target });
|
|
486
|
+
const targets = isPlainObject(config) && isPlainObject(config.targets) ? config.targets : {};
|
|
487
|
+
|
|
488
|
+
for (const [name, entry] of Object.entries(targets)) {
|
|
489
|
+
if (!isPlainObject(entry) || isCustomTargetEntry(entry) || !TARGETS.includes(entry.type)) continue;
|
|
490
|
+
found.push(...undeclaredPaths(entry, { target: entry.type }).map((dotted) => `targets.${name}.${dotted}`));
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
return found;
|
|
494
|
+
}
|
|
495
|
+
|
|
459
496
|
/**
|
|
460
497
|
* Validate a resolved config: shared schema + optional target refinements +
|
|
461
498
|
* targets-key sanity + secret-shaped-key detection.
|
|
462
499
|
* @param {object} config - The resolved config object.
|
|
463
500
|
* @param {object} [options]
|
|
464
501
|
* @param {string} [options.target] - Canonical target name; adds TARGET_SCHEMAS[target].
|
|
502
|
+
* @param {boolean} [options.decorated] - The config already carries a build's
|
|
503
|
+
* facts (a baked config, validated again at boot).
|
|
465
504
|
* @returns {{ errors: string[], warnings: string[] }}
|
|
466
505
|
*/
|
|
467
506
|
function validateConfig(config, options) {
|
|
468
507
|
options = options || {};
|
|
469
508
|
|
|
470
|
-
|
|
471
|
-
throw new Error(`Unknown target "${options.target}" — must be one of [${TARGETS.join(', ')}]`);
|
|
472
|
-
}
|
|
473
|
-
|
|
474
|
-
const schema = options.target
|
|
475
|
-
? [...SHARED_SCHEMA, ...TARGET_SCHEMAS[options.target]]
|
|
476
|
-
: SHARED_SCHEMA;
|
|
477
|
-
|
|
509
|
+
const schema = schemaFor(options.target);
|
|
478
510
|
const errors = runSchema(config, schema);
|
|
479
511
|
const warnings = [];
|
|
480
512
|
|
|
@@ -573,25 +605,13 @@ function validateConfig(config, options) {
|
|
|
573
605
|
// ─── the features catalog and the values products name (#647) ──────────
|
|
574
606
|
validateFeatures(config).forEach((error) => errors.push(error));
|
|
575
607
|
|
|
576
|
-
// ───
|
|
577
|
-
// A
|
|
578
|
-
//
|
|
579
|
-
|
|
580
|
-
const undeclared = findUndeclaredPaths(config, schema);
|
|
581
|
-
if (undeclared.length > 0) {
|
|
582
|
-
warnings.push(
|
|
583
|
-
'config carries keys the schema does not declare — nothing reads them, so a typo looks exactly like a '
|
|
584
|
-
+ `feature. Remove them, or give each one a rule in @omega.js/config's schema.js `
|
|
585
|
-
+ `(docs/shared/config.md → Validation): ${undeclared.join(', ')}`,
|
|
586
|
-
);
|
|
587
|
-
}
|
|
588
|
-
|
|
589
|
-
// ─── retired keys (#142) ───────────────────────────────────────────────
|
|
590
|
-
findRetiredKeys(config).forEach(({ path, replacement, why }) => {
|
|
608
|
+
// ─── the schema is strict ──────────────────────────────────────────────
|
|
609
|
+
// A key nothing declares is a key nothing reads: a typo or a legacy shape.
|
|
610
|
+
// One line per path, so a brand sees every key it has to move or delete.
|
|
611
|
+
findUndeclaredPaths(config, schema, options.decorated ? [...RESOLVED_PATHS, ...BUILD_FACT_PATHS] : RESOLVED_PATHS).forEach((path) => {
|
|
591
612
|
errors.push(
|
|
592
|
-
`config.${path} is
|
|
593
|
-
+ `
|
|
594
|
-
+ `(docs/shared/config.md → "Migration — legacy configs")`,
|
|
613
|
+
`config.${path} is not a key the schema declares. Remove it, or if it is a legacy key run `
|
|
614
|
+
+ '`npx omega migrate` at the brand root (report) and `--execute` to convert (docs/shared/config.md → Validation)',
|
|
595
615
|
);
|
|
596
616
|
});
|
|
597
617
|
|
|
@@ -610,4 +630,4 @@ function formatErrors(errors) {
|
|
|
610
630
|
return errors.map((e, i) => ` ${i + 1}. ${e}`).join('\n');
|
|
611
631
|
}
|
|
612
632
|
|
|
613
|
-
module.exports = { validateConfig, runSchema, formatErrors, getPath, resolvedBrandHost };
|
|
633
|
+
module.exports = { validateConfig, undeclaredPaths, undeclaredAuthoredPaths, runSchema, formatErrors, getPath, resolvedBrandHost };
|
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The ONE builder of a project-root AGENTS.md, a brand root and a framework
|
|
3
|
+
* used alone alike, written through the one marker engine: the Default section
|
|
4
|
+
* imports the installed manager's AGENTS.md (which imports the omega map from
|
|
5
|
+
* the docs the manager ships), the Custom section holds the project's own
|
|
6
|
+
* notes, verbatim. Every older shape converges once, its notes kept under
|
|
7
|
+
* Custom. A brand target carries none: its brand root is the doc home.
|
|
8
|
+
*/
|
|
9
|
+
const { join } = require('node:path');
|
|
10
|
+
const fs = require('node:fs');
|
|
11
|
+
const jetpack = require('fs-jetpack');
|
|
12
|
+
const { mergeLineBasedFiles, hasSectionMarkers, getCustomSection, isShipped, sectionMarkers, DEFAULT_MARKER } = require('./merge-line-files');
|
|
13
|
+
|
|
14
|
+
const FILE_NAME = 'AGENTS.md';
|
|
15
|
+
const GUIDE_SUBPATH = 'node_modules/@omega.js/manager/AGENTS.md';
|
|
16
|
+
const IMPORT_LINE = `@${GUIDE_SUBPATH}`;
|
|
17
|
+
// The retired scope-level link: an import of it is healed away and the link
|
|
18
|
+
// itself removed.
|
|
19
|
+
const RETIRED_SUBPATH = 'node_modules/@omega.js/AGENTS.md';
|
|
20
|
+
|
|
21
|
+
const SCOPE_PREFIXES = ['', '../', '../../', '../../../'];
|
|
22
|
+
|
|
23
|
+
// The skeleton heading earlier builders wrote under the import.
|
|
24
|
+
const NOTES_HEADING = /^# .+: (project|brand) notes$/;
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Is this line an import of the manager's AGENTS.md or the retired scope link
|
|
28
|
+
* (any relative depth)?
|
|
29
|
+
*
|
|
30
|
+
* @param {string} line - A single file line
|
|
31
|
+
* @returns {boolean}
|
|
32
|
+
*/
|
|
33
|
+
function isImportLine(line) {
|
|
34
|
+
const trimmed = line.trim();
|
|
35
|
+
return trimmed.startsWith('@') && (trimmed.endsWith(GUIDE_SUBPATH) || trimmed.endsWith(RETIRED_SUBPATH));
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* The @omega.js scope directory serving this project: its own node_modules
|
|
40
|
+
* normally, an ancestor's when hoisted (npm workspaces). Only a scope holding
|
|
41
|
+
* the manager counts: an empty dir from a partial install would leave the
|
|
42
|
+
* import dangling while the real scope sits a level up.
|
|
43
|
+
*
|
|
44
|
+
* @param {string} root - Absolute project root
|
|
45
|
+
* @returns {{prefix: string, scopeDir: string}|null}
|
|
46
|
+
*/
|
|
47
|
+
function findScope(root) {
|
|
48
|
+
for (const prefix of SCOPE_PREFIXES) {
|
|
49
|
+
const scopeDir = join(root, prefix, 'node_modules', '@omega.js');
|
|
50
|
+
if (jetpack.exists(join(scopeDir, 'manager')) === 'dir') {
|
|
51
|
+
return { prefix, scopeDir };
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
return null;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Resolve the import line for THIS project: the depth that reaches the scope
|
|
59
|
+
* holding the manager. Falls back to the canonical project-local path when
|
|
60
|
+
* the manager is not installed, so the line still names where the docs live.
|
|
61
|
+
*
|
|
62
|
+
* @param {string} root - Absolute project root
|
|
63
|
+
* @returns {string} - The `@<relative path>` import line
|
|
64
|
+
*/
|
|
65
|
+
function resolveImportLine(root) {
|
|
66
|
+
const scope = findScope(root);
|
|
67
|
+
return scope ? `@${scope.prefix}${GUIDE_SUBPATH}` : IMPORT_LINE;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Remove the retired `node_modules/@omega.js/AGENTS.md` link (or a stale file
|
|
72
|
+
* in its place) from the scope serving this brand.
|
|
73
|
+
*
|
|
74
|
+
* @param {string} brandRoot - Absolute brand monorepo root
|
|
75
|
+
* @param {{ dryRun?: boolean }} [options] - dryRun: the same verdict, nothing removed
|
|
76
|
+
* @returns {'removed'|'absent'} - What happened
|
|
77
|
+
*/
|
|
78
|
+
function removeRetiredLink(brandRoot, { dryRun = false } = {}) {
|
|
79
|
+
const scope = findScope(brandRoot);
|
|
80
|
+
const link = scope && join(scope.scopeDir, 'AGENTS.md');
|
|
81
|
+
if (!link || !fs.lstatSync(link, { throwIfNoEntry: false })) {
|
|
82
|
+
return 'absent';
|
|
83
|
+
}
|
|
84
|
+
if (!dryRun) {
|
|
85
|
+
fs.rmSync(link, { force: true });
|
|
86
|
+
}
|
|
87
|
+
return 'removed';
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Render a fresh project-root AGENTS.md: the import under the Default marker,
|
|
92
|
+
* then the Custom marker and nothing below it.
|
|
93
|
+
*
|
|
94
|
+
* @param {string} [importLine] - The resolved import line
|
|
95
|
+
* @returns {string} - Full file content
|
|
96
|
+
*/
|
|
97
|
+
function renderAgentsMd(importLine = IMPORT_LINE) {
|
|
98
|
+
const { defaultMarker, customMarker } = sectionMarkers(FILE_NAME);
|
|
99
|
+
return `${defaultMarker}\n${importLine}\n\n${customMarker}\n`;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Is this the retired per-framework template? It OPENS with the `#` Default
|
|
104
|
+
* marker; a file that merely quotes the markers in its notes is not one.
|
|
105
|
+
*
|
|
106
|
+
* @param {string} content - The AGENTS.md content
|
|
107
|
+
* @returns {boolean}
|
|
108
|
+
*/
|
|
109
|
+
function isRetiredTemplate(content) {
|
|
110
|
+
return content.split('\n')[0].trim() === DEFAULT_MARKER && hasSectionMarkers(content);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* The notes a consumer wrote in an AGENTS.md of any shape, trimmed: the Custom
|
|
115
|
+
* section of a marked or retired-template file, else the whole file, minus
|
|
116
|
+
* every line a framework wrote (the import, the skeleton heading, shipped
|
|
117
|
+
* boilerplate).
|
|
118
|
+
*
|
|
119
|
+
* @param {string} content - The AGENTS.md content
|
|
120
|
+
* @returns {string} - '' when the file holds nothing the framework did not write
|
|
121
|
+
*/
|
|
122
|
+
function consumerNotes(content) {
|
|
123
|
+
const source = hasSectionMarkers(content, FILE_NAME)
|
|
124
|
+
? getCustomSection(content, FILE_NAME)
|
|
125
|
+
: isRetiredTemplate(content) ? getCustomSection(content) : content;
|
|
126
|
+
return source.split('\n')
|
|
127
|
+
.filter((line) => !isImportLine(line) && !NOTES_HEADING.test(line.trim()) && !isShipped(line, FILE_NAME))
|
|
128
|
+
.join('\n')
|
|
129
|
+
.trim();
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Ensure the project-root AGENTS.md is the marked shape with the resolved
|
|
134
|
+
* import. A marked file goes through the engine (the Default section healed,
|
|
135
|
+
* the Custom section verbatim); any other shape converges, its notes landing
|
|
136
|
+
* under Custom in order.
|
|
137
|
+
*
|
|
138
|
+
* @param {string} root - Absolute project root
|
|
139
|
+
* @param {{ dryRun?: boolean }} [options] - dryRun: the same verdict, nothing written
|
|
140
|
+
* @returns {'present'|'created'|'healed'|'converged'} - What happened
|
|
141
|
+
*/
|
|
142
|
+
function ensureAgentsMd(root, { dryRun = false } = {}) {
|
|
143
|
+
const file = join(root, FILE_NAME);
|
|
144
|
+
const template = renderAgentsMd(resolveImportLine(root));
|
|
145
|
+
const existing = jetpack.read(file);
|
|
146
|
+
|
|
147
|
+
if (existing === undefined) {
|
|
148
|
+
if (!dryRun) jetpack.write(file, template);
|
|
149
|
+
return 'created';
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
const marked = hasSectionMarkers(existing, FILE_NAME);
|
|
153
|
+
const next = mergeLineBasedFiles(marked ? existing : consumerNotes(existing), template, FILE_NAME);
|
|
154
|
+
if (next === existing) {
|
|
155
|
+
return 'present';
|
|
156
|
+
}
|
|
157
|
+
if (!dryRun) jetpack.write(file, next);
|
|
158
|
+
return marked ? 'healed' : 'converged';
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Keep a brand target free of any AGENTS.md: the brand root is its doc home. A
|
|
163
|
+
* copy holding nothing the consumer wrote is removed; one with notes stays.
|
|
164
|
+
*
|
|
165
|
+
* @param {string} targetDir - Absolute brand target root
|
|
166
|
+
* @returns {'absent'|'removed'|'kept'} - What happened
|
|
167
|
+
*/
|
|
168
|
+
function retireAgentsMd(targetDir) {
|
|
169
|
+
const file = join(targetDir, 'AGENTS.md');
|
|
170
|
+
const existing = jetpack.read(file);
|
|
171
|
+
|
|
172
|
+
if (existing === undefined) {
|
|
173
|
+
return 'absent';
|
|
174
|
+
}
|
|
175
|
+
if (consumerNotes(existing)) {
|
|
176
|
+
return 'kept';
|
|
177
|
+
}
|
|
178
|
+
jetpack.remove(file);
|
|
179
|
+
return 'removed';
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* A framework scaffold's AGENTS.md step, recorded into the defaults engine's
|
|
184
|
+
* result and logged the way the engine logs: a standalone project root gets
|
|
185
|
+
* the builder's file; a brand target gets none.
|
|
186
|
+
*
|
|
187
|
+
* @param {object} options
|
|
188
|
+
* @param {string} options.outputDir - Absolute project or target root
|
|
189
|
+
* @param {boolean} options.standalone - True when no brand root sits above it
|
|
190
|
+
* @param {{written: string[], merged: string[], skipped: string[], removed: string[]}} options.result - The engine result to record into
|
|
191
|
+
* @param {object} [options.logger] - `{ log, warn }` (defaults to console)
|
|
192
|
+
*/
|
|
193
|
+
function scaffoldAgentsMd({ outputDir, standalone, result, logger = console }) {
|
|
194
|
+
if (!standalone) {
|
|
195
|
+
const verdict = retireAgentsMd(outputDir);
|
|
196
|
+
if (verdict === 'removed') {
|
|
197
|
+
result.removed.push('AGENTS.md');
|
|
198
|
+
logger.warn('Retired AGENTS.md: a brand target carries none, the brand root AGENTS.md is the one doc home');
|
|
199
|
+
} else if (verdict === 'kept') {
|
|
200
|
+
result.skipped.push('AGENTS.md');
|
|
201
|
+
logger.warn('Kept AGENTS.md: it carries consumer content. A brand target carries no AGENTS.md: move those notes under the Custom marker of the brand root AGENTS.md, then delete the file');
|
|
202
|
+
}
|
|
203
|
+
return;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
const verdict = ensureAgentsMd(outputDir);
|
|
207
|
+
if (verdict === 'created') {
|
|
208
|
+
result.written.push('AGENTS.md');
|
|
209
|
+
logger.log('Scaffolded → AGENTS.md');
|
|
210
|
+
} else if (verdict === 'healed') {
|
|
211
|
+
result.merged.push('AGENTS.md');
|
|
212
|
+
logger.log('Healed → AGENTS.md: the Default section imports the manager, your Custom section kept');
|
|
213
|
+
} else if (verdict === 'converged') {
|
|
214
|
+
result.merged.push('AGENTS.md');
|
|
215
|
+
logger.warn('Converged AGENTS.md to the marker sections: the manager import under Default, your notes under Custom');
|
|
216
|
+
} else {
|
|
217
|
+
result.skipped.push('AGENTS.md');
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
module.exports = {
|
|
222
|
+
GUIDE_SUBPATH,
|
|
223
|
+
IMPORT_LINE,
|
|
224
|
+
RETIRED_SUBPATH,
|
|
225
|
+
isImportLine,
|
|
226
|
+
findScope,
|
|
227
|
+
resolveImportLine,
|
|
228
|
+
removeRetiredLink,
|
|
229
|
+
renderAgentsMd,
|
|
230
|
+
ensureAgentsMd,
|
|
231
|
+
retireAgentsMd,
|
|
232
|
+
scaffoldAgentsMd,
|
|
233
|
+
};
|
|
@@ -25,7 +25,8 @@
|
|
|
25
25
|
// Two attaches on ONE tee stack the same way: a second attach of a DIFFERENT path pushes a
|
|
26
26
|
// layer on top of the first instead of replacing it, so a verb that runs another verb in
|
|
27
27
|
// process (`omega test` running `omega deploy`) keeps teeing both files. Detach order is
|
|
28
|
-
// LIFO, and the module-level `detach()` pops the newest layer.
|
|
28
|
+
// LIFO, and the module-level `detach()` pops the newest layer. A harness that runs a verb
|
|
29
|
+
// takes `mark()` first and calls the restore it returns after, never a blind `detach()`.
|
|
29
30
|
//
|
|
30
31
|
// Skipped in CI: a runner has its own log capture and wants no logs/ left in the workspace.
|
|
31
32
|
// `attachInCI: true` opts out of that skip, for a sink whose file is the POINT rather than
|
|
@@ -142,8 +143,20 @@ function createTee() {
|
|
|
142
143
|
return detach;
|
|
143
144
|
}
|
|
144
145
|
|
|
146
|
+
// A harness around a verb restores to the mark instead of popping blind: a run can
|
|
147
|
+
// push one layer, several, or none, and only what it pushed comes off, newest first.
|
|
148
|
+
function mark() {
|
|
149
|
+
const kept = layers.slice();
|
|
150
|
+
return function restore() {
|
|
151
|
+
for (const layer of layers.slice().reverse()) {
|
|
152
|
+
if (!kept.includes(layer)) { layer.detach(); }
|
|
153
|
+
}
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
|
|
145
157
|
return {
|
|
146
158
|
attach,
|
|
159
|
+
mark,
|
|
147
160
|
detach: () => {
|
|
148
161
|
const newest = layers[layers.length - 1];
|
|
149
162
|
if (newest) { newest.detach(); }
|
|
@@ -265,6 +278,7 @@ function attachLogFile(filePath, options) {
|
|
|
265
278
|
|
|
266
279
|
module.exports = attachLogFile;
|
|
267
280
|
module.exports.detach = singleton.detach;
|
|
281
|
+
module.exports.mark = singleton.mark;
|
|
268
282
|
module.exports.stripAnsi = stripAnsi;
|
|
269
283
|
module.exports.createTee = createTee;
|
|
270
284
|
module.exports.createChildLog = createChildLog;
|