opencode-ship 0.3.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +18 -0
- package/README.md +3 -3
- package/THIRD_PARTY_NOTICES.md +7 -5
- package/dist/cli.js +168 -32
- package/dist/core.js +1 -1
- package/dist/plugin.js +27 -7
- package/package.json +1 -1
- package/schema/ship-config.schema.json +5 -0
- package/schema/ship-lock.schema.json +6 -2
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,24 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to `opencode-ship` are recorded here.
|
|
4
4
|
|
|
5
|
+
## 0.4.0 — Profile-aware installer foundation
|
|
6
|
+
|
|
7
|
+
`opencode-ship@0.4.0` adds the profile-aware installer foundation that issue #18 requires. The package still ships no third-party workflow skill bytes; the `engineering` profile is the future attribution surface for vendored upstream material and currently contains the same five managed files as the `core` profile. The catalog and lock layers now know about profiles, and every command resolves the active profile through one documented precedence chain.
|
|
8
|
+
|
|
9
|
+
### Verification
|
|
10
|
+
|
|
11
|
+
- `npm run verify` exits `0` with 226 tests across 34 suites on the v0.4 HEAD.
|
|
12
|
+
|
|
13
|
+
### Added
|
|
14
|
+
|
|
15
|
+
- **Profile model.** `src/profile.js` declares `PROFILES = ["core", "engineering"]` and exports `resolveProfile({ cli, config, lock })` for the documented precedence (CLI > ship.config > lock > default). Unknown profiles throw a descriptive `Error` so the CLI can surface them as `exit 2`.
|
|
16
|
+
- **`--profile` CLI flag.** Every subcommand (`init`, `diff`, `update`, `doctor`, `uninstall`) accepts `--profile <name>`; parse errors emit to `stderr` and return `exit 2`.
|
|
17
|
+
- **Lock schema v2.** `CURRENT_LOCK_SCHEMA` is bumped to 2. Newly written locks always carry `manager.profile`; v1 locks (no profile field) still validate as legacy core so v0.3 consumers can upgrade without manual migration. The `ship-lock.schema.json` `enum` allows `[1, 2]` for both `contractVersion` and `manager.schemaVersion`.
|
|
18
|
+
- **`ship.config.json .profile`.** The user config schema accepts an optional `profile` enum (`core | engineering`). The profile is loaded by the same precedence chain as the lock.
|
|
19
|
+
- **Profile-aware catalog.** Every `CATALOG` entry declares a `profiles` array. `filterCatalogByProfile(catalog, profile)` returns the subset that ships under the active profile; `validateCatalog` rejects entries that reference unknown profiles.
|
|
20
|
+
- **Profile-aware doctor.** The new `profile footprint` check scopes asset presence to the active profile; `package integrity` continues to check the full catalog so the maintainer can still see drift in the other profile.
|
|
21
|
+
- **Error to `stderr`.** CLI argument-parsing errors are now written to `stderr` (was `stdout`) so consumers can detect parse failures by exit code alone.
|
|
22
|
+
|
|
5
23
|
## 0.3.0 — Installer hardening and release pipeline
|
|
6
24
|
|
|
7
25
|
`opencode-ship@0.3.0` hardens the installer for the public registry. This is the v0.3 installer foundation with the `core` profile only; it carries no third-party workflow skill bytes. The package is now fully consumable from npm with provenance. The catalog installs the five managed files plus the two generated artifacts (`ship.config.json`, `ship.lock.json`), and adds tighter guards around every existing one. The plugin target is `.opencode/plugins/opencode-ship.js` so OpenCode auto-loads it from the plural directory; root-config pointer ownership is recorded for every installer-owned entry so the future v0.4 opt-in `engineering` profile can restore previous values on uninstall. v0.3 is the approved slice shipped by parent spec `Viktorxyz/opencode-ship#16` and plan revision `f85bae931d9eed7763e2f6f4dc68e5fad71bdd38c8a667fc9ffe78b5290200be`.
|
package/README.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
> npm-distributed OpenCode installer and delivery plugin: a single command materialises the lifecycle plugin, reviewer/verifier agents, and skills into any consumer repository, with a recoverable lock and never silently overwrites managed files.
|
|
4
4
|
>
|
|
5
|
-
> **Status:** v0.
|
|
5
|
+
> **Status:** v0.4.0 profile-aware installer foundation. The installer is a `pnpm dlx opencode-ship@latest <cmd>` workflow. Five idempotent CLI commands manage a managed-file lock, a transactional promoter, and a compiled ESM plugin that registers the canonical nine `delivery_*` tools. Every command now accepts `--profile <name>` (`core` default, `engineering` opt-in) and resolves the active profile through one documented precedence chain (CLI > ship.config > lock > default). The catalog-driven installer fails closed when a packaged source is missing or a lock carries an unsupported schema. The plugin target is `.opencode/plugins/opencode-ship.js`; OpenCode auto-loads plugins from the plural directory. v0.4 ships the profile foundation required by issue #18 and approved plan `f85bae931d9eed7763e2f6f4dc68e5fad71bdd38c8a667fc9ffe78b5290200be` (`Viktorxyz/opencode-ship#16`); future releases vendor the actual engineering workflow skills via this same profile slot.
|
|
6
6
|
|
|
7
7
|
---
|
|
8
8
|
|
|
@@ -111,7 +111,7 @@ Existing consumers of `opencode-delivery@0.1.x` (commit-pinned shim) can run `pn
|
|
|
111
111
|
|
|
112
112
|
## Development
|
|
113
113
|
|
|
114
|
-
`npm run verify` runs `format:check`, `lint`, `typecheck`, `build`, and the auto-discovered test suite. The tests cover the installer CLI, the lock and root-config planners, the catalog validator, the schema validator, agents, the packed-artifact smoke check, the transaction-recovery contract, and the order-preserving root-config merge. v0.
|
|
114
|
+
`npm run verify` runs `format:check`, `lint`, `typecheck`, `build`, and the auto-discovered test suite. The tests cover the installer CLI, the lock and root-config planners, the catalog validator, the schema validator, agents, the packed-artifact smoke check, the transaction-recovery contract, the profile-resolution precedence chain, the catalog profile filter, and the order-preserving root-config merge. v0.4 ships with 226 tests across 34 suites on the approved plan hash `f85bae931d9eed7763e2f6f4dc68e5fad71bdd38c8a667fc9ffe78b5290200be`.
|
|
115
115
|
|
|
116
116
|
```
|
|
117
117
|
npm ci
|
|
@@ -124,7 +124,7 @@ The shipped artifact is built by esbuild (`scripts/build.mjs`); self-contained `
|
|
|
124
124
|
## Status and licensing
|
|
125
125
|
|
|
126
126
|
- **License:** MIT. See `LICENSE`.
|
|
127
|
-
- **Versioning:** SemVer. v0.2.0 is the first npm-distributed release. v0.3.0 is the installer foundation with core-only defaults. v0.4.0 adds the
|
|
127
|
+
- **Versioning:** SemVer. v0.2.0 is the first npm-distributed release. v0.3.0 is the installer foundation with core-only defaults. v0.4.0 adds the profile-aware installer foundation (`--profile` flag, lock schema v2, profile precedence) that issue #18 requires.
|
|
128
128
|
- **Compatibility:** the bundled plugin targets `@opencode-ai/plugin >= 1.15.5 < 2` and OpenCode `>= 1.15.5`.
|
|
129
129
|
|
|
130
130
|
## FAQ
|
package/THIRD_PARTY_NOTICES.md
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
# Third-Party Notices
|
|
2
2
|
|
|
3
|
-
`opencode-ship@0.
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
3
|
+
`opencode-ship@0.4.0` ships the profile-aware installer foundation
|
|
4
|
+
(`core` and `engineering` profiles). The current package version does
|
|
5
|
+
not bundle third-party skill content. The `engineering` profile is
|
|
6
|
+
the attribution surface for vendored upstream material; future
|
|
7
|
+
releases that vendor skills must list the exact files, source
|
|
8
|
+
commit, reuse mode, and license here.
|
|
7
9
|
|
|
8
10
|
## Bundled skill content
|
|
9
11
|
|
|
10
|
-
None in v0.
|
|
12
|
+
None in v0.4.0.
|
package/dist/cli.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
// opencode-ship CLI v0.
|
|
2
|
+
// opencode-ship CLI v0.4.0
|
|
3
3
|
var __defProp = Object.defineProperty;
|
|
4
4
|
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
5
|
var __esm = (fn, res) => function __init() {
|
|
@@ -484,6 +484,46 @@ var init_root_config = __esm({
|
|
|
484
484
|
}
|
|
485
485
|
});
|
|
486
486
|
|
|
487
|
+
// src/profile.js
|
|
488
|
+
var PROFILES = Object.freeze(["core", "engineering"]);
|
|
489
|
+
var DEFAULT_PROFILE = "core";
|
|
490
|
+
function isValidProfile(name) {
|
|
491
|
+
return typeof name === "string" && PROFILES.includes(name);
|
|
492
|
+
}
|
|
493
|
+
function normalizeProfile(name) {
|
|
494
|
+
if (name === void 0 || name === null) return DEFAULT_PROFILE;
|
|
495
|
+
if (!isValidProfile(name)) return null;
|
|
496
|
+
return name;
|
|
497
|
+
}
|
|
498
|
+
function resolveProfile({ cli = null, config = null, lock = null } = {}) {
|
|
499
|
+
if (cli !== null && cli !== void 0) {
|
|
500
|
+
const v = normalizeProfile(cli);
|
|
501
|
+
if (v === null) {
|
|
502
|
+
throw new Error(`unknown CLI profile '${cli}' (expected one of: ${PROFILES.join(", ")})`);
|
|
503
|
+
}
|
|
504
|
+
return { profile: v, source: "cli" };
|
|
505
|
+
}
|
|
506
|
+
if (config && typeof config === "object" && config.profile !== void 0 && config.profile !== null) {
|
|
507
|
+
const v = normalizeProfile(config.profile);
|
|
508
|
+
if (v === null) {
|
|
509
|
+
throw new Error(
|
|
510
|
+
`unknown ship.config.json profile '${config.profile}' (expected one of: ${PROFILES.join(", ")})`
|
|
511
|
+
);
|
|
512
|
+
}
|
|
513
|
+
return { profile: v, source: "config" };
|
|
514
|
+
}
|
|
515
|
+
if (lock && typeof lock === "object" && lock.manager && lock.manager.profile !== void 0) {
|
|
516
|
+
const v = normalizeProfile(lock.manager.profile);
|
|
517
|
+
if (v === null) {
|
|
518
|
+
throw new Error(
|
|
519
|
+
`unknown lock manager.profile '${lock.manager.profile}' (expected one of: ${PROFILES.join(", ")})`
|
|
520
|
+
);
|
|
521
|
+
}
|
|
522
|
+
return { profile: v, source: "lock" };
|
|
523
|
+
}
|
|
524
|
+
return { profile: DEFAULT_PROFILE, source: "default" };
|
|
525
|
+
}
|
|
526
|
+
|
|
487
527
|
// src/installer/cli-args.js
|
|
488
528
|
var USAGE = `opencode-ship <command> [options]
|
|
489
529
|
|
|
@@ -498,6 +538,7 @@ Commands:
|
|
|
498
538
|
|
|
499
539
|
Options:
|
|
500
540
|
--root <path> Project root (defaults to cwd).
|
|
541
|
+
--profile <name> Override active profile: ${PROFILES.join(", ")}.
|
|
501
542
|
--force-config Rewrite the user config from detection (init only).
|
|
502
543
|
--force-root-config Create opencode.json when absent (init only).
|
|
503
544
|
--strict-doctor Fail init when doctor reports unhealthy checks.
|
|
@@ -508,6 +549,7 @@ Options:
|
|
|
508
549
|
function parseFlags(argv) {
|
|
509
550
|
const options = {
|
|
510
551
|
rootPath: null,
|
|
552
|
+
profile: null,
|
|
511
553
|
json: false,
|
|
512
554
|
replaceManaged: false,
|
|
513
555
|
purgeConfig: false,
|
|
@@ -524,7 +566,16 @@ function parseFlags(argv) {
|
|
|
524
566
|
else if (arg === "--force-root-config") options.forceRootConfig = true;
|
|
525
567
|
else if (arg === "--strict-doctor") options.strictDoctor = true;
|
|
526
568
|
else if (arg === "--root") options.rootPath = argv[++i];
|
|
527
|
-
else if (arg === "
|
|
569
|
+
else if (arg === "--profile") {
|
|
570
|
+
const value = argv[++i];
|
|
571
|
+
if (value === void 0) {
|
|
572
|
+
return { error: "--profile requires a value" };
|
|
573
|
+
}
|
|
574
|
+
if (!isValidProfile(value)) {
|
|
575
|
+
return { error: `unknown profile '${value}' (expected one of: ${PROFILES.join(", ")})` };
|
|
576
|
+
}
|
|
577
|
+
options.profile = value;
|
|
578
|
+
} else if (arg === "-h" || arg === "--help") return { help: true };
|
|
528
579
|
else if (arg === "-v" || arg === "--version") return { version: true };
|
|
529
580
|
else return { error: `unknown flag ${arg}` };
|
|
530
581
|
}
|
|
@@ -591,7 +642,7 @@ function resolvePackageRoot(startUrl) {
|
|
|
591
642
|
import { readFileSync as readFileSync2, existsSync as existsSync2 } from "node:fs";
|
|
592
643
|
import { dirname as dirname2, resolve as resolve2 } from "node:path";
|
|
593
644
|
import { fileURLToPath as fileURLToPath2 } from "node:url";
|
|
594
|
-
var PACKAGE_VERSION = "0.
|
|
645
|
+
var PACKAGE_VERSION = "0.4.0";
|
|
595
646
|
var TEMPLATE_SET = `v${PACKAGE_VERSION}`;
|
|
596
647
|
|
|
597
648
|
// src/installer/catalog.js
|
|
@@ -603,37 +654,51 @@ var CATALOG = [
|
|
|
603
654
|
kind: "plugin",
|
|
604
655
|
path: ".opencode/plugins/opencode-ship.js",
|
|
605
656
|
source: resolve3(packageRoot, "dist/plugin.js"),
|
|
606
|
-
mode: 420
|
|
657
|
+
mode: 420,
|
|
658
|
+
profiles: ["core", "engineering"]
|
|
607
659
|
},
|
|
608
660
|
{
|
|
609
661
|
id: "agent:delivery-reviewer",
|
|
610
662
|
kind: "agent",
|
|
611
663
|
path: ".opencode/agents/delivery-reviewer.md",
|
|
612
664
|
source: resolve3(packageRoot, "assets/agents/delivery-reviewer.md"),
|
|
613
|
-
mode: 420
|
|
665
|
+
mode: 420,
|
|
666
|
+
profiles: ["core", "engineering"]
|
|
614
667
|
},
|
|
615
668
|
{
|
|
616
669
|
id: "agent:delivery-verifier",
|
|
617
670
|
kind: "agent",
|
|
618
671
|
path: ".opencode/agents/delivery-verifier.md",
|
|
619
672
|
source: resolve3(packageRoot, "assets/agents/delivery-verifier.md"),
|
|
620
|
-
mode: 420
|
|
673
|
+
mode: 420,
|
|
674
|
+
profiles: ["core", "engineering"]
|
|
621
675
|
},
|
|
622
676
|
{
|
|
623
677
|
id: "skill:delivery-workflow",
|
|
624
678
|
kind: "skill",
|
|
625
679
|
path: ".opencode/skills/delivery-workflow/SKILL.md",
|
|
626
680
|
source: resolve3(packageRoot, "assets/skills/delivery-workflow/SKILL.md"),
|
|
627
|
-
mode: 420
|
|
681
|
+
mode: 420,
|
|
682
|
+
profiles: ["core", "engineering"]
|
|
628
683
|
},
|
|
629
684
|
{
|
|
630
685
|
id: "skill:planning-research-checkpoint",
|
|
631
686
|
kind: "skill",
|
|
632
687
|
path: ".opencode/skills/planning-research-checkpoint/SKILL.md",
|
|
633
688
|
source: resolve3(packageRoot, "assets/skills/planning-research-checkpoint/SKILL.md"),
|
|
634
|
-
mode: 420
|
|
689
|
+
mode: 420,
|
|
690
|
+
profiles: ["core", "engineering"]
|
|
635
691
|
}
|
|
636
692
|
];
|
|
693
|
+
function filterCatalogByProfile(catalog, profile) {
|
|
694
|
+
const effective = profile === void 0 || profile === null ? DEFAULT_PROFILE : profile;
|
|
695
|
+
if (!isValidProfile(effective)) {
|
|
696
|
+
throw new Error(
|
|
697
|
+
`filterCatalogByProfile: unknown profile '${profile}' (expected one of: ${PROFILES.join(", ")})`
|
|
698
|
+
);
|
|
699
|
+
}
|
|
700
|
+
return catalog.filter((entry) => Array.isArray(entry.profiles) && entry.profiles.includes(effective));
|
|
701
|
+
}
|
|
637
702
|
var ALLOWED_KINDS = /* @__PURE__ */ new Set(["plugin", "agent", "skill", "support"]);
|
|
638
703
|
function validateCatalog({ catalog = CATALOG } = {}) {
|
|
639
704
|
const seenIds = /* @__PURE__ */ new Set();
|
|
@@ -686,6 +751,15 @@ function validateCatalog({ catalog = CATALOG } = {}) {
|
|
|
686
751
|
if (mode !== 420) {
|
|
687
752
|
issues.push({ id, kind: "mode", message: `mode must be 0o644: ${id}` });
|
|
688
753
|
}
|
|
754
|
+
if (!Array.isArray(entry.profiles) || entry.profiles.length === 0) {
|
|
755
|
+
issues.push({ id, kind: "profiles", message: `profiles must be a non-empty array: ${id}` });
|
|
756
|
+
} else {
|
|
757
|
+
for (const p of entry.profiles) {
|
|
758
|
+
if (!isValidProfile(p)) {
|
|
759
|
+
issues.push({ id, kind: "profiles", message: `unknown profile in profiles[${entry.profiles.indexOf(p)}]: ${p} (expected one of: ${PROFILES.join(", ")})` });
|
|
760
|
+
}
|
|
761
|
+
}
|
|
762
|
+
}
|
|
689
763
|
}
|
|
690
764
|
if (issues.length > 0) {
|
|
691
765
|
const summary = issues.map((i) => i.message).join("; ");
|
|
@@ -717,6 +791,11 @@ var ship_config_schema_default = {
|
|
|
717
791
|
additionalProperties: false,
|
|
718
792
|
properties: {
|
|
719
793
|
schemaVersion: { const: 1 },
|
|
794
|
+
profile: {
|
|
795
|
+
type: "string",
|
|
796
|
+
enum: ["core", "engineering"],
|
|
797
|
+
description: "Active profile (precedence layer 2: ship.config > lock > core default)."
|
|
798
|
+
},
|
|
720
799
|
owner: {
|
|
721
800
|
type: "string",
|
|
722
801
|
description: "Optional override for the issue/manifest owner field. Defaults to the agent's local user.name."
|
|
@@ -1086,9 +1165,9 @@ async function planManagedFile({ entry, repoRoot, lock, allowUnowned }) {
|
|
|
1086
1165
|
reason: locked?.sha256 == null ? "managed file already exists; bytes differ from upstream" : "managed file is locally modified"
|
|
1087
1166
|
};
|
|
1088
1167
|
}
|
|
1089
|
-
async function planFileInstall({ repoRoot, lock, allowUnowned = false }) {
|
|
1168
|
+
async function planFileInstall({ repoRoot, lock, allowUnowned = false, catalog = CATALOG }) {
|
|
1090
1169
|
const plan = [];
|
|
1091
|
-
for (const entry of
|
|
1170
|
+
for (const entry of catalog) {
|
|
1092
1171
|
plan.push(await planManagedFile({ entry, repoRoot, lock, allowUnowned }));
|
|
1093
1172
|
}
|
|
1094
1173
|
return plan;
|
|
@@ -1330,7 +1409,7 @@ init_json_pointer();
|
|
|
1330
1409
|
import { readFile as readFile4, writeFile as writeFile2, rename as rename2, mkdir as mkdir2 } from "node:fs/promises";
|
|
1331
1410
|
import { existsSync as existsSync7 } from "node:fs";
|
|
1332
1411
|
import { dirname as dirname4, resolve as resolve6 } from "node:path";
|
|
1333
|
-
var CURRENT_LOCK_SCHEMA =
|
|
1412
|
+
var CURRENT_LOCK_SCHEMA = 2;
|
|
1334
1413
|
function lockPath(repoRoot) {
|
|
1335
1414
|
return resolve6(repoRoot, ".opencode", "ship.lock.json");
|
|
1336
1415
|
}
|
|
@@ -1350,8 +1429,8 @@ function validateLock(rawLock) {
|
|
|
1350
1429
|
}
|
|
1351
1430
|
const issues = [];
|
|
1352
1431
|
let kind = "ok";
|
|
1353
|
-
if (rawLock.contractVersion !== CURRENT_LOCK_SCHEMA) {
|
|
1354
|
-
issues.push(`unsupported contractVersion: ${JSON.stringify(rawLock.contractVersion)} (expected ${CURRENT_LOCK_SCHEMA})`);
|
|
1432
|
+
if (rawLock.contractVersion !== CURRENT_LOCK_SCHEMA && rawLock.contractVersion !== 1) {
|
|
1433
|
+
issues.push(`unsupported contractVersion: ${JSON.stringify(rawLock.contractVersion)} (expected ${CURRENT_LOCK_SCHEMA} or 1)`);
|
|
1355
1434
|
kind = "schema";
|
|
1356
1435
|
}
|
|
1357
1436
|
const manager = rawLock.manager;
|
|
@@ -1361,12 +1440,15 @@ function validateLock(rawLock) {
|
|
|
1361
1440
|
} else if (typeof manager !== "object" || manager === null) {
|
|
1362
1441
|
issues.push("manager section must be an object");
|
|
1363
1442
|
kind = kind === "ok" ? "shape" : kind;
|
|
1364
|
-
} else if (manager.schemaVersion !== CURRENT_LOCK_SCHEMA) {
|
|
1365
|
-
issues.push(`unsupported manager.schemaVersion: ${JSON.stringify(manager.schemaVersion)} (expected ${CURRENT_LOCK_SCHEMA})`);
|
|
1443
|
+
} else if (manager.schemaVersion !== CURRENT_LOCK_SCHEMA && manager.schemaVersion !== 1) {
|
|
1444
|
+
issues.push(`unsupported manager.schemaVersion: ${JSON.stringify(manager.schemaVersion)} (expected ${CURRENT_LOCK_SCHEMA} or 1)`);
|
|
1366
1445
|
kind = "schema";
|
|
1367
1446
|
} else if (manager.name !== "opencode-ship") {
|
|
1368
1447
|
issues.push(`unknown manager.name: ${JSON.stringify(manager.name)}`);
|
|
1369
1448
|
kind = "shape";
|
|
1449
|
+
} else if (rawLock.contractVersion === CURRENT_LOCK_SCHEMA && manager.schemaVersion === CURRENT_LOCK_SCHEMA && manager.profile !== void 0 && !isValidProfile(manager.profile)) {
|
|
1450
|
+
issues.push(`invalid manager.profile: ${JSON.stringify(manager.profile)} (expected one of: core, engineering)`);
|
|
1451
|
+
kind = "shape";
|
|
1370
1452
|
}
|
|
1371
1453
|
if (!rawLock.files || !Array.isArray(rawLock.files)) {
|
|
1372
1454
|
issues.push("files must be an array");
|
|
@@ -2003,7 +2085,7 @@ async function readCurrentBytes(targetPath) {
|
|
|
2003
2085
|
const buf = await readFile7(targetPath);
|
|
2004
2086
|
return { bytes: buf, hash: bytesHashString(buf.toString("utf8")) };
|
|
2005
2087
|
}
|
|
2006
|
-
async function previewInstall({ rootPath, replaceManaged, forceConfig, forceRootConfig }) {
|
|
2088
|
+
async function previewInstall({ rootPath, profile = null, replaceManaged, forceConfig, forceRootConfig }) {
|
|
2007
2089
|
const detection = detectProject(rootPath ?? process.cwd());
|
|
2008
2090
|
if (detection.errors.some((e) => e.kind === "not-a-git-repo")) {
|
|
2009
2091
|
return { ok: false, error: { kind: "invalid-project", errors: detection.errors } };
|
|
@@ -2018,6 +2100,14 @@ async function previewInstall({ rootPath, replaceManaged, forceConfig, forceRoot
|
|
|
2018
2100
|
}
|
|
2019
2101
|
const lock = validatedLock.lock;
|
|
2020
2102
|
const migrationReport = await migration({ repoRoot, lock, forceRepair: false, detection });
|
|
2103
|
+
const configResult = await loadConfig(repoRoot);
|
|
2104
|
+
const configValue = configResult?.ok ? configResult.value : null;
|
|
2105
|
+
const resolved = resolveProfile({
|
|
2106
|
+
cli: profile,
|
|
2107
|
+
config: configValue,
|
|
2108
|
+
lock
|
|
2109
|
+
});
|
|
2110
|
+
const activeCatalog = filterCatalogByProfile(CATALOG, resolved.profile);
|
|
2021
2111
|
const configPlan = await planConfigSynthesis({
|
|
2022
2112
|
repoRoot,
|
|
2023
2113
|
detection,
|
|
@@ -2025,7 +2115,7 @@ async function previewInstall({ rootPath, replaceManaged, forceConfig, forceRoot
|
|
|
2025
2115
|
forceOverwrite: Boolean(forceConfig),
|
|
2026
2116
|
migrationSeed: migrationReport?.proposedConfigSeed ?? null
|
|
2027
2117
|
});
|
|
2028
|
-
const filePlan = await planFileInstall({ repoRoot, lock, allowUnowned: Boolean(replaceManaged) });
|
|
2118
|
+
const filePlan = await planFileInstall({ repoRoot, lock, allowUnowned: Boolean(replaceManaged), catalog: activeCatalog });
|
|
2029
2119
|
const migrationPlan = await planMigrationCleanup({
|
|
2030
2120
|
repoRoot,
|
|
2031
2121
|
lock,
|
|
@@ -2036,7 +2126,17 @@ async function previewInstall({ rootPath, replaceManaged, forceConfig, forceRoot
|
|
|
2036
2126
|
const plan = [...filePlan ?? [], ...migrationPlan, configPlan, rootPlan];
|
|
2037
2127
|
const conflicts = plan.filter((p) => p && p.kind === "conflict");
|
|
2038
2128
|
const summary = summarise(plan);
|
|
2039
|
-
return {
|
|
2129
|
+
return {
|
|
2130
|
+
ok: true,
|
|
2131
|
+
repoRoot,
|
|
2132
|
+
detection,
|
|
2133
|
+
lock,
|
|
2134
|
+
profile: resolved,
|
|
2135
|
+
plan,
|
|
2136
|
+
conflicts,
|
|
2137
|
+
summary,
|
|
2138
|
+
migrationReport
|
|
2139
|
+
};
|
|
2040
2140
|
}
|
|
2041
2141
|
async function previewUninstall({ rootPath }) {
|
|
2042
2142
|
const detection = detectProject(rootPath ?? process.cwd());
|
|
@@ -2070,7 +2170,7 @@ function summarise(plan) {
|
|
|
2070
2170
|
}
|
|
2071
2171
|
return counts;
|
|
2072
2172
|
}
|
|
2073
|
-
async function assembleLock({ repoRoot, plan, lock, configPlan, rootPlan }) {
|
|
2173
|
+
async function assembleLock({ repoRoot, plan, lock, configPlan, rootPlan, profile = null }) {
|
|
2074
2174
|
const files = [];
|
|
2075
2175
|
const remain = lock?.files?.filter((f) => !plan.some((op) => op?.relPath === f.path)) ?? [];
|
|
2076
2176
|
for (const op of plan) {
|
|
@@ -2099,12 +2199,16 @@ async function assembleLock({ repoRoot, plan, lock, configPlan, rootPlan }) {
|
|
|
2099
2199
|
const hasRootPlan = Boolean(rootPlan?.target || rootPlan?.pointerRecords && rootPlan.pointerRecords.length > 0);
|
|
2100
2200
|
const hasRootDocuments = rootPlan?.pointerRecords && rootPlan.pointerRecords.length > 0 || lock?.manager?.rootDocuments && lock.manager.rootDocuments.length > 0;
|
|
2101
2201
|
return {
|
|
2102
|
-
contractVersion:
|
|
2202
|
+
contractVersion: CURRENT_LOCK_SCHEMA,
|
|
2103
2203
|
manager: {
|
|
2104
|
-
schemaVersion:
|
|
2204
|
+
schemaVersion: CURRENT_LOCK_SCHEMA,
|
|
2105
2205
|
name: "opencode-ship",
|
|
2106
|
-
version: "0.
|
|
2206
|
+
version: "0.4.0",
|
|
2107
2207
|
templateSet: TEMPLATE_SET_ID,
|
|
2208
|
+
// Newly written locks always carry the resolved profile so
|
|
2209
|
+
// future CLI invocations without --profile still resolve to
|
|
2210
|
+
// the same choice through the lock-precedence layer.
|
|
2211
|
+
profile: profile ?? lock?.manager?.profile,
|
|
2108
2212
|
appliedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2109
2213
|
config: {
|
|
2110
2214
|
path: ".opencode/ship.config.json",
|
|
@@ -2156,7 +2260,8 @@ async function commitInstall(preview, { json, command }) {
|
|
|
2156
2260
|
plan: fileOnly,
|
|
2157
2261
|
lock: preview.lock,
|
|
2158
2262
|
configPlan,
|
|
2159
|
-
rootPlan
|
|
2263
|
+
rootPlan,
|
|
2264
|
+
profile: preview.profile?.profile
|
|
2160
2265
|
});
|
|
2161
2266
|
const txPlan = await stageFiles(fileOnly, repoRoot);
|
|
2162
2267
|
if (configPlan && (configPlan.kind === "create" || configPlan.kind === "update")) {
|
|
@@ -2421,6 +2526,24 @@ async function checkManagedHashes(repoRoot, validatedLock) {
|
|
|
2421
2526
|
}
|
|
2422
2527
|
return { name: "managed hashes", ok: drift.length === 0, detail: drift.length ? drift.join(",") : "match" };
|
|
2423
2528
|
}
|
|
2529
|
+
async function checkActiveProfileFootprint(repoRoot, validatedLock, profile) {
|
|
2530
|
+
if (validatedLock.kind !== "ok" || !validatedLock.lock) {
|
|
2531
|
+
return { name: "profile footprint", ok: true, detail: "no lock; n/a" };
|
|
2532
|
+
}
|
|
2533
|
+
if (!profile) {
|
|
2534
|
+
return { name: "profile footprint", ok: true, detail: "no profile; n/a" };
|
|
2535
|
+
}
|
|
2536
|
+
const expectedPaths = new Set(
|
|
2537
|
+
filterCatalogByProfile(CATALOG, profile).map((e) => e.path)
|
|
2538
|
+
);
|
|
2539
|
+
const present = (validatedLock.lock.files ?? []).filter((f) => expectedPaths.has(f.path)).map((f) => f.path);
|
|
2540
|
+
const missing = [...expectedPaths].filter((p) => !present.includes(p));
|
|
2541
|
+
return {
|
|
2542
|
+
name: "profile footprint",
|
|
2543
|
+
ok: missing.length === 0,
|
|
2544
|
+
detail: missing.length ? `missing profile assets: ${missing.join(",")}` : `${present.length}/${expectedPaths.size} present`
|
|
2545
|
+
};
|
|
2546
|
+
}
|
|
2424
2547
|
async function checkRootConfig(repoRoot) {
|
|
2425
2548
|
const { findRootConfig: findRootConfig2 } = await Promise.resolve().then(() => (init_root_config(), root_config_exports));
|
|
2426
2549
|
const candidate = findRootConfig2(repoRoot);
|
|
@@ -2443,7 +2566,7 @@ function writeEnvelope({ command, plan, summary, diagnostics, json, exitCode })
|
|
|
2443
2566
|
process.stdout.write(renderHuman({ command, plan, conflicts, summary, diagnostics }) + "\n");
|
|
2444
2567
|
}
|
|
2445
2568
|
}
|
|
2446
|
-
async function runDoctor({ rootPath, json, writeOutput = true }) {
|
|
2569
|
+
async function runDoctor({ rootPath, profile, json, writeOutput = true }) {
|
|
2447
2570
|
const detection = detectProject(rootPath ?? process.cwd());
|
|
2448
2571
|
if (detection.errors.some((e) => e.kind === "not-a-git-repo")) {
|
|
2449
2572
|
const issues2 = ["not in a git repository"];
|
|
@@ -2464,6 +2587,13 @@ async function runDoctor({ rootPath, json, writeOutput = true }) {
|
|
|
2464
2587
|
const sourceHashes = buildSourceHashIndex();
|
|
2465
2588
|
const validatedLock = await readValidatedLock(repoRoot);
|
|
2466
2589
|
const packageIntegrity = checkPackageIntegrity();
|
|
2590
|
+
const configResult = await loadConfig(repoRoot);
|
|
2591
|
+
const configValue = configResult?.ok ? configResult.value : null;
|
|
2592
|
+
const resolved = resolveProfile({
|
|
2593
|
+
cli: profile,
|
|
2594
|
+
config: configValue,
|
|
2595
|
+
lock: validatedLock.lock
|
|
2596
|
+
});
|
|
2467
2597
|
const checks = [
|
|
2468
2598
|
checkNode(),
|
|
2469
2599
|
checkGit(),
|
|
@@ -2474,6 +2604,7 @@ async function runDoctor({ rootPath, json, writeOutput = true }) {
|
|
|
2474
2604
|
await checkLock(repoRoot),
|
|
2475
2605
|
await checkConfig(repoRoot),
|
|
2476
2606
|
await checkManagedHashes(repoRoot, validatedLock),
|
|
2607
|
+
await checkActiveProfileFootprint(repoRoot, validatedLock, resolved.profile),
|
|
2477
2608
|
await checkRootConfig(repoRoot)
|
|
2478
2609
|
];
|
|
2479
2610
|
const issues = checks.filter((c) => !c.ok).map((c) => `${c.name}: ${c.detail}`);
|
|
@@ -2491,7 +2622,7 @@ async function runDoctor({ rootPath, json, writeOutput = true }) {
|
|
|
2491
2622
|
if (validatedLock.kind === "schema") exitCode = 5;
|
|
2492
2623
|
if (writeOutput) writeEnvelope({ command: "doctor", plan, summary, diagnostics: issues, json, exitCode });
|
|
2493
2624
|
process.exitCode = exitCode;
|
|
2494
|
-
return { issues, exitCode, plan, checks };
|
|
2625
|
+
return { issues, exitCode, plan, checks, profile: resolved };
|
|
2495
2626
|
}
|
|
2496
2627
|
|
|
2497
2628
|
// src/installer/commands/init.js
|
|
@@ -2506,6 +2637,7 @@ async function runInit(options) {
|
|
|
2506
2637
|
}
|
|
2507
2638
|
const preview = await previewInstall({
|
|
2508
2639
|
rootPath: options.rootPath ?? null,
|
|
2640
|
+
profile: options.profile ?? null,
|
|
2509
2641
|
replaceManaged: false,
|
|
2510
2642
|
forceConfig: Boolean(options.forceConfig),
|
|
2511
2643
|
forceRootConfig: Boolean(options.forceRootConfig)
|
|
@@ -2529,6 +2661,7 @@ async function runInit(options) {
|
|
|
2529
2661
|
}
|
|
2530
2662
|
const doctor = await runDoctor({
|
|
2531
2663
|
rootPath: options.rootPath ?? null,
|
|
2664
|
+
profile: options.profile ?? null,
|
|
2532
2665
|
json: Boolean(options.json),
|
|
2533
2666
|
writeOutput: false
|
|
2534
2667
|
});
|
|
@@ -2634,6 +2767,7 @@ async function runDiff(options) {
|
|
|
2634
2767
|
}
|
|
2635
2768
|
const preview = await previewInstall({
|
|
2636
2769
|
rootPath: options.rootPath ?? null,
|
|
2770
|
+
profile: options.profile ?? null,
|
|
2637
2771
|
replaceManaged: false,
|
|
2638
2772
|
forceConfig: false,
|
|
2639
2773
|
forceRootConfig: false
|
|
@@ -2704,6 +2838,7 @@ async function runUpdate(options) {
|
|
|
2704
2838
|
}
|
|
2705
2839
|
const preview = await previewInstall({
|
|
2706
2840
|
rootPath: options.rootPath,
|
|
2841
|
+
profile: options.profile ?? null,
|
|
2707
2842
|
replaceManaged: options.replaceManaged,
|
|
2708
2843
|
forceConfig: options.forceConfig,
|
|
2709
2844
|
forceRootConfig: options.forceRootConfig
|
|
@@ -2765,7 +2900,7 @@ import { unlink as unlink3 } from "node:fs/promises";
|
|
|
2765
2900
|
import { existsSync as existsSync13 } from "node:fs";
|
|
2766
2901
|
import { resolve as resolve12 } from "node:path";
|
|
2767
2902
|
async function runUninstall(options) {
|
|
2768
|
-
const preview = await previewUninstall({ rootPath: options.rootPath });
|
|
2903
|
+
const preview = await previewUninstall({ rootPath: options.rootPath, profile: options.profile ?? null });
|
|
2769
2904
|
if (!preview.ok) {
|
|
2770
2905
|
if (preview.error?.kind === "unsupported-lock-schema") {
|
|
2771
2906
|
return emitFailure3(5, `unsupported lock schema: ${(preview.error.issues ?? []).join("; ")}`, options.json);
|
|
@@ -2856,7 +2991,7 @@ async function main() {
|
|
|
2856
2991
|
return;
|
|
2857
2992
|
}
|
|
2858
2993
|
if ("error" in parsed) {
|
|
2859
|
-
process.
|
|
2994
|
+
process.stderr.write(`opencode-ship: ${parsed.error}
|
|
2860
2995
|
|
|
2861
2996
|
${helpText()}`);
|
|
2862
2997
|
process.exitCode = 2;
|
|
@@ -2864,21 +2999,22 @@ ${helpText()}`);
|
|
|
2864
2999
|
}
|
|
2865
3000
|
const options = parsed.options ?? {};
|
|
2866
3001
|
const opts = options;
|
|
3002
|
+
const profile = opts.profile ?? null;
|
|
2867
3003
|
switch (parsed.command) {
|
|
2868
3004
|
case "init":
|
|
2869
|
-
await runInit({ json: !!opts.json, rootPath: opts.rootPath, forceConfig: !!opts.forceConfig, forceRootConfig: !!opts.forceRootConfig, strictDoctor: !!opts.strictDoctor });
|
|
3005
|
+
await runInit({ json: !!opts.json, rootPath: opts.rootPath, profile, forceConfig: !!opts.forceConfig, forceRootConfig: !!opts.forceRootConfig, strictDoctor: !!opts.strictDoctor });
|
|
2870
3006
|
return;
|
|
2871
3007
|
case "diff":
|
|
2872
|
-
await runDiff({ json: !!opts.json, rootPath: opts.rootPath });
|
|
3008
|
+
await runDiff({ json: !!opts.json, rootPath: opts.rootPath, profile });
|
|
2873
3009
|
return;
|
|
2874
3010
|
case "update":
|
|
2875
|
-
await runUpdate({ json: !!opts.json, rootPath: opts.rootPath, replaceManaged: !!opts.replaceManaged, forceConfig: !!opts.forceConfig, forceRootConfig: !!opts.forceRootConfig });
|
|
3011
|
+
await runUpdate({ json: !!opts.json, rootPath: opts.rootPath, profile, replaceManaged: !!opts.replaceManaged, forceConfig: !!opts.forceConfig, forceRootConfig: !!opts.forceRootConfig });
|
|
2876
3012
|
return;
|
|
2877
3013
|
case "doctor":
|
|
2878
|
-
await runDoctor({ json: !!opts.json, rootPath: opts.rootPath });
|
|
3014
|
+
await runDoctor({ json: !!opts.json, rootPath: opts.rootPath, profile });
|
|
2879
3015
|
return;
|
|
2880
3016
|
case "uninstall":
|
|
2881
|
-
await runUninstall({ json: !!opts.json, rootPath: opts.rootPath, purgeConfig: !!opts.purgeConfig });
|
|
3017
|
+
await runUninstall({ json: !!opts.json, rootPath: opts.rootPath, profile, purgeConfig: !!opts.purgeConfig });
|
|
2882
3018
|
return;
|
|
2883
3019
|
default:
|
|
2884
3020
|
process.stdout.write(helpText() + "\n");
|
package/dist/core.js
CHANGED
package/dist/plugin.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// opencode-ship v0.
|
|
1
|
+
// opencode-ship v0.4.0
|
|
2
2
|
var __defProp = Object.defineProperty;
|
|
3
3
|
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
4
4
|
var __esm = (fn, res) => function __init() {
|
|
@@ -40,6 +40,17 @@ var init_hash = __esm({
|
|
|
40
40
|
}
|
|
41
41
|
});
|
|
42
42
|
|
|
43
|
+
// src/profile.js
|
|
44
|
+
function isValidProfile(name) {
|
|
45
|
+
return typeof name === "string" && PROFILES.includes(name);
|
|
46
|
+
}
|
|
47
|
+
var PROFILES;
|
|
48
|
+
var init_profile = __esm({
|
|
49
|
+
"src/profile.js"() {
|
|
50
|
+
PROFILES = Object.freeze(["core", "engineering"]);
|
|
51
|
+
}
|
|
52
|
+
});
|
|
53
|
+
|
|
43
54
|
// src/installer/lock.js
|
|
44
55
|
var lock_exports = {};
|
|
45
56
|
__export(lock_exports, {
|
|
@@ -106,8 +117,8 @@ function validateLock(rawLock) {
|
|
|
106
117
|
}
|
|
107
118
|
const issues = [];
|
|
108
119
|
let kind = "ok";
|
|
109
|
-
if (rawLock.contractVersion !== CURRENT_LOCK_SCHEMA) {
|
|
110
|
-
issues.push(`unsupported contractVersion: ${JSON.stringify(rawLock.contractVersion)} (expected ${CURRENT_LOCK_SCHEMA})`);
|
|
120
|
+
if (rawLock.contractVersion !== CURRENT_LOCK_SCHEMA && rawLock.contractVersion !== 1) {
|
|
121
|
+
issues.push(`unsupported contractVersion: ${JSON.stringify(rawLock.contractVersion)} (expected ${CURRENT_LOCK_SCHEMA} or 1)`);
|
|
111
122
|
kind = "schema";
|
|
112
123
|
}
|
|
113
124
|
const manager = rawLock.manager;
|
|
@@ -117,12 +128,15 @@ function validateLock(rawLock) {
|
|
|
117
128
|
} else if (typeof manager !== "object" || manager === null) {
|
|
118
129
|
issues.push("manager section must be an object");
|
|
119
130
|
kind = kind === "ok" ? "shape" : kind;
|
|
120
|
-
} else if (manager.schemaVersion !== CURRENT_LOCK_SCHEMA) {
|
|
121
|
-
issues.push(`unsupported manager.schemaVersion: ${JSON.stringify(manager.schemaVersion)} (expected ${CURRENT_LOCK_SCHEMA})`);
|
|
131
|
+
} else if (manager.schemaVersion !== CURRENT_LOCK_SCHEMA && manager.schemaVersion !== 1) {
|
|
132
|
+
issues.push(`unsupported manager.schemaVersion: ${JSON.stringify(manager.schemaVersion)} (expected ${CURRENT_LOCK_SCHEMA} or 1)`);
|
|
122
133
|
kind = "schema";
|
|
123
134
|
} else if (manager.name !== "opencode-ship") {
|
|
124
135
|
issues.push(`unknown manager.name: ${JSON.stringify(manager.name)}`);
|
|
125
136
|
kind = "shape";
|
|
137
|
+
} else if (rawLock.contractVersion === CURRENT_LOCK_SCHEMA && manager.schemaVersion === CURRENT_LOCK_SCHEMA && manager.profile !== void 0 && !isValidProfile(manager.profile)) {
|
|
138
|
+
issues.push(`invalid manager.profile: ${JSON.stringify(manager.profile)} (expected one of: core, engineering)`);
|
|
139
|
+
kind = "shape";
|
|
126
140
|
}
|
|
127
141
|
if (!rawLock.files || !Array.isArray(rawLock.files)) {
|
|
128
142
|
issues.push("files must be an array");
|
|
@@ -181,7 +195,8 @@ var init_lock = __esm({
|
|
|
181
195
|
"src/installer/lock.js"() {
|
|
182
196
|
init_hash();
|
|
183
197
|
init_json_pointer();
|
|
184
|
-
|
|
198
|
+
init_profile();
|
|
199
|
+
CURRENT_LOCK_SCHEMA = 2;
|
|
185
200
|
}
|
|
186
201
|
});
|
|
187
202
|
|
|
@@ -14282,6 +14297,11 @@ var ship_config_schema_default = {
|
|
|
14282
14297
|
additionalProperties: false,
|
|
14283
14298
|
properties: {
|
|
14284
14299
|
schemaVersion: { const: 1 },
|
|
14300
|
+
profile: {
|
|
14301
|
+
type: "string",
|
|
14302
|
+
enum: ["core", "engineering"],
|
|
14303
|
+
description: "Active profile (precedence layer 2: ship.config > lock > core default)."
|
|
14304
|
+
},
|
|
14285
14305
|
owner: {
|
|
14286
14306
|
type: "string",
|
|
14287
14307
|
description: "Optional override for the issue/manifest owner field. Defaults to the agent's local user.name."
|
|
@@ -14877,7 +14897,7 @@ function flattenShipConfig(ship) {
|
|
|
14877
14897
|
import { readFileSync as readFileSync2, existsSync as existsSync6 } from "node:fs";
|
|
14878
14898
|
import { dirname as dirname5, resolve as resolve11 } from "node:path";
|
|
14879
14899
|
import { fileURLToPath } from "node:url";
|
|
14880
|
-
var PACKAGE_VERSION = "0.
|
|
14900
|
+
var PACKAGE_VERSION = "0.4.0";
|
|
14881
14901
|
var TEMPLATE_SET = `v${PACKAGE_VERSION}`;
|
|
14882
14902
|
|
|
14883
14903
|
// src/plugin.js
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "opencode-ship",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "npm-distributed OpenCode installer that materializes the delivery plugin, reviewer/verifier agents, skills, ship config and lock into any consumer repository.",
|
|
6
6
|
"license": "MIT",
|
|
@@ -7,6 +7,11 @@
|
|
|
7
7
|
"additionalProperties": false,
|
|
8
8
|
"properties": {
|
|
9
9
|
"schemaVersion": { "const": 1 },
|
|
10
|
+
"profile": {
|
|
11
|
+
"type": "string",
|
|
12
|
+
"enum": ["core", "engineering"],
|
|
13
|
+
"description": "Active profile (precedence layer 2: ship.config > lock > core default)."
|
|
14
|
+
},
|
|
10
15
|
"owner": {
|
|
11
16
|
"type": "string",
|
|
12
17
|
"description": "Optional override for the issue/manifest owner field. Defaults to the agent's local user.name."
|
|
@@ -6,16 +6,20 @@
|
|
|
6
6
|
"required": ["contractVersion", "manager", "files", "integrity"],
|
|
7
7
|
"additionalProperties": false,
|
|
8
8
|
"properties": {
|
|
9
|
-
"contractVersion": { "
|
|
9
|
+
"contractVersion": { "enum": [1, 2] },
|
|
10
10
|
"manager": {
|
|
11
11
|
"type": "object",
|
|
12
12
|
"required": ["schemaVersion", "name", "version", "appliedAt", "config"],
|
|
13
13
|
"additionalProperties": false,
|
|
14
14
|
"properties": {
|
|
15
|
-
"schemaVersion": { "
|
|
15
|
+
"schemaVersion": { "enum": [1, 2] },
|
|
16
16
|
"name": { "type": "string", "pattern": "^opencode-ship$" },
|
|
17
17
|
"version": { "type": "string", "minLength": 1 },
|
|
18
18
|
"templateSet": { "type": "string", "minLength": 1 },
|
|
19
|
+
"profile": {
|
|
20
|
+
"enum": ["core", "engineering"],
|
|
21
|
+
"description": "Profile the lock was applied under. Required on locks written at schema 2; absent on legacy schema-1 locks (resolved to core by the profile precedence layer)."
|
|
22
|
+
},
|
|
19
23
|
"appliedAt": { "type": "string", "format": "date-time" },
|
|
20
24
|
"config": {
|
|
21
25
|
"type": "object",
|