orionfold-relay 0.21.0 → 0.22.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/dist/cli.js +167 -47
- package/package.json +1 -1
- package/src/app/packs/page.tsx +19 -5
- package/src/lib/licensing/cli.ts +59 -0
- package/src/lib/licensing/recap.ts +119 -0
- package/src/lib/packs/format.ts +7 -0
- package/src/lib/packs/templates/relay-agency-pro/pack.yaml +14 -1
- package/src/lib/packs/update.ts +20 -0
- package/src/lib/plugins/examples/echo-server/plugin.yaml +1 -1
- package/src/lib/plugins/examples/finance-pack/plugin.yaml +1 -1
- package/src/lib/plugins/examples/reading-radar/plugin.yaml +1 -1
- package/src/lib/plugins/registry.ts +1 -1
- package/src/lib/plugins/sdk/types.ts +1 -1
package/dist/cli.js
CHANGED
|
@@ -1186,7 +1186,7 @@ var CURRENT_PLUGIN_API_VERSION, CAPABILITY_VALUES, ORIGIN_VALUES, PrimitivesBund
|
|
|
1186
1186
|
var init_types = __esm({
|
|
1187
1187
|
"src/lib/plugins/sdk/types.ts"() {
|
|
1188
1188
|
"use strict";
|
|
1189
|
-
CURRENT_PLUGIN_API_VERSION = "0.
|
|
1189
|
+
CURRENT_PLUGIN_API_VERSION = "0.22";
|
|
1190
1190
|
CAPABILITY_VALUES = ["fs", "net", "child_process", "env"];
|
|
1191
1191
|
ORIGIN_VALUES = ["ainative-internal", "third-party"];
|
|
1192
1192
|
PrimitivesBundleManifestSchema = z.object({
|
|
@@ -1232,16 +1232,16 @@ function computeHash(content) {
|
|
|
1232
1232
|
function safePreview(content) {
|
|
1233
1233
|
return content.slice(0, MAX_PREVIEW_CHARS).trim();
|
|
1234
1234
|
}
|
|
1235
|
-
function safeStat(
|
|
1235
|
+
function safeStat(path24) {
|
|
1236
1236
|
try {
|
|
1237
|
-
return statSync2(
|
|
1237
|
+
return statSync2(path24);
|
|
1238
1238
|
} catch {
|
|
1239
1239
|
return null;
|
|
1240
1240
|
}
|
|
1241
1241
|
}
|
|
1242
|
-
function safeReadFile(
|
|
1242
|
+
function safeReadFile(path24) {
|
|
1243
1243
|
try {
|
|
1244
|
-
return readFileSync4(
|
|
1244
|
+
return readFileSync4(path24, "utf-8");
|
|
1245
1245
|
} catch {
|
|
1246
1246
|
return null;
|
|
1247
1247
|
}
|
|
@@ -3744,6 +3744,13 @@ var init_format = __esm({
|
|
|
3744
3744
|
price: z3.string().min(1).optional(),
|
|
3745
3745
|
/** Get-license CTA target on the locked card. */
|
|
3746
3746
|
purchaseUrl: z3.url().optional(),
|
|
3747
|
+
/**
|
|
3748
|
+
* Per-version customer-voice recap, version → one line. The single source
|
|
3749
|
+
* for every renewal value-recap surface (`license status`, the 402 update
|
|
3750
|
+
* refusal, the /packs update card, the Website renewal email). Optional —
|
|
3751
|
+
* but paid packs should carry it, or their renewal case argues generically.
|
|
3752
|
+
*/
|
|
3753
|
+
changelog: z3.record(z3.string(), z3.string().min(1)).optional(),
|
|
3747
3754
|
/** Customer slugs seeded via ensureCustomer at install. */
|
|
3748
3755
|
customers: z3.array(z3.string()).default([])
|
|
3749
3756
|
}).strict();
|
|
@@ -11951,9 +11958,9 @@ function buildPermissionSummary(toolName, input) {
|
|
|
11951
11958
|
}
|
|
11952
11959
|
}
|
|
11953
11960
|
if (toolName === "Read" || toolName === "Write" || toolName === "Edit" || toolName === "read" || toolName === "write" || toolName === "edit") {
|
|
11954
|
-
const
|
|
11955
|
-
if (typeof
|
|
11956
|
-
return truncate(
|
|
11961
|
+
const path24 = input.file_path ?? input.path;
|
|
11962
|
+
if (typeof path24 === "string" && path24.trim().length > 0) {
|
|
11963
|
+
return truncate(path24.trim());
|
|
11957
11964
|
}
|
|
11958
11965
|
}
|
|
11959
11966
|
if (toolName?.startsWith("mcp__")) {
|
|
@@ -11987,9 +11994,9 @@ function getPermissionDetailEntries(toolName, input) {
|
|
|
11987
11994
|
}
|
|
11988
11995
|
}
|
|
11989
11996
|
if (toolName === "Read" || toolName === "Write" || toolName === "Edit" || toolName === "read" || toolName === "write" || toolName === "edit") {
|
|
11990
|
-
const
|
|
11991
|
-
if (typeof
|
|
11992
|
-
return [{ label: "Path", value:
|
|
11997
|
+
const path24 = input.file_path ?? input.path;
|
|
11998
|
+
if (typeof path24 === "string") {
|
|
11999
|
+
return [{ label: "Path", value: path24 }];
|
|
11993
12000
|
}
|
|
11994
12001
|
}
|
|
11995
12002
|
return Object.entries(input).slice(0, 6).map(([key, value]) => ({
|
|
@@ -12891,7 +12898,7 @@ var init_registry6 = __esm({
|
|
|
12891
12898
|
init_registry5();
|
|
12892
12899
|
init_installer();
|
|
12893
12900
|
init_schedule_spec();
|
|
12894
|
-
SUPPORTED_API_VERSIONS = /* @__PURE__ */ new Set([CURRENT_PLUGIN_API_VERSION, "0.
|
|
12901
|
+
SUPPORTED_API_VERSIONS = /* @__PURE__ */ new Set([CURRENT_PLUGIN_API_VERSION, "0.21"]);
|
|
12895
12902
|
pluginCache = null;
|
|
12896
12903
|
lastLoadedPluginIds = /* @__PURE__ */ new Set();
|
|
12897
12904
|
PluginTableSchema = z16.object({
|
|
@@ -12934,8 +12941,8 @@ function pluginTools(_ctx) {
|
|
|
12934
12941
|
Promise.resolve().then(() => (init_ainative_paths(), ainative_paths_exports)),
|
|
12935
12942
|
Promise.resolve().then(() => (init_types(), types_exports))
|
|
12936
12943
|
]);
|
|
12937
|
-
const
|
|
12938
|
-
const
|
|
12944
|
+
const fs23 = await import("fs");
|
|
12945
|
+
const path24 = await import("path");
|
|
12939
12946
|
const yaml13 = await import("js-yaml");
|
|
12940
12947
|
const kind5 = listPlugins2();
|
|
12941
12948
|
const registrations = await listPluginMcpRegistrations2();
|
|
@@ -12952,13 +12959,13 @@ function pluginTools(_ctx) {
|
|
|
12952
12959
|
let manifestHash;
|
|
12953
12960
|
let capabilityAcceptStatus = "pending";
|
|
12954
12961
|
try {
|
|
12955
|
-
const pluginYamlPath =
|
|
12962
|
+
const pluginYamlPath = path24.join(
|
|
12956
12963
|
pluginsDir,
|
|
12957
12964
|
pluginId,
|
|
12958
12965
|
"plugin.yaml"
|
|
12959
12966
|
);
|
|
12960
|
-
if (
|
|
12961
|
-
const content =
|
|
12967
|
+
if (fs23.existsSync(pluginYamlPath)) {
|
|
12968
|
+
const content = fs23.readFileSync(pluginYamlPath, "utf-8");
|
|
12962
12969
|
const rawManifest = yaml13.load(content);
|
|
12963
12970
|
if (rawManifest !== null && typeof rawManifest === "object" && !Array.isArray(rawManifest)) {
|
|
12964
12971
|
const record = rawManifest;
|
|
@@ -12971,7 +12978,7 @@ function pluginTools(_ctx) {
|
|
|
12971
12978
|
try {
|
|
12972
12979
|
manifestHash = deriveManifestHash2(content);
|
|
12973
12980
|
const parsed = PluginManifestSchema2.safeParse(rawManifest);
|
|
12974
|
-
const pluginRootDir =
|
|
12981
|
+
const pluginRootDir = path24.join(pluginsDir, pluginId);
|
|
12975
12982
|
const check = isCapabilityAccepted2(
|
|
12976
12983
|
pluginId,
|
|
12977
12984
|
manifestHash,
|
|
@@ -19387,13 +19394,13 @@ var init_codex_app_server_client = __esm({
|
|
|
19387
19394
|
await syncPluginMcpToCodex2();
|
|
19388
19395
|
} catch (err2) {
|
|
19389
19396
|
try {
|
|
19390
|
-
const
|
|
19391
|
-
const
|
|
19397
|
+
const fs23 = await import("fs");
|
|
19398
|
+
const path24 = await import("path");
|
|
19392
19399
|
const { getAinativeLogsDir: getAinativeLogsDir2 } = await Promise.resolve().then(() => (init_ainative_paths(), ainative_paths_exports));
|
|
19393
19400
|
const dir = getAinativeLogsDir2();
|
|
19394
|
-
|
|
19395
|
-
|
|
19396
|
-
|
|
19401
|
+
fs23.mkdirSync(dir, { recursive: true });
|
|
19402
|
+
fs23.appendFileSync(
|
|
19403
|
+
path24.join(dir, "plugins.log"),
|
|
19397
19404
|
`${(/* @__PURE__ */ new Date()).toISOString()} codex-sync-failed: ${err2 instanceof Error ? err2.message : String(err2)}
|
|
19398
19405
|
`
|
|
19399
19406
|
);
|
|
@@ -22985,14 +22992,14 @@ function resolvePostAction(action, row, itemVariable) {
|
|
|
22985
22992
|
function substituteRowPath(template, row, itemVariable) {
|
|
22986
22993
|
const escaped = itemVariable.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
22987
22994
|
const pattern = new RegExp(`\\{\\{\\s*${escaped}\\.([\\w.]+)\\s*\\}\\}`, "g");
|
|
22988
|
-
return template.replace(pattern, (_match,
|
|
22989
|
-
const value = readPath(row,
|
|
22995
|
+
return template.replace(pattern, (_match, path24) => {
|
|
22996
|
+
const value = readPath(row, path24);
|
|
22990
22997
|
if (value === void 0 || value === null) return "";
|
|
22991
22998
|
return String(value);
|
|
22992
22999
|
});
|
|
22993
23000
|
}
|
|
22994
|
-
function readPath(obj,
|
|
22995
|
-
const parts =
|
|
23001
|
+
function readPath(obj, path24) {
|
|
23002
|
+
const parts = path24.split(".");
|
|
22996
23003
|
let current = obj;
|
|
22997
23004
|
for (const part of parts) {
|
|
22998
23005
|
if (current === null || current === void 0) return void 0;
|
|
@@ -23179,14 +23186,14 @@ ${resolvedTemplate}`);
|
|
|
23179
23186
|
return parts.join("");
|
|
23180
23187
|
}
|
|
23181
23188
|
function resolveRowTemplate(template, context) {
|
|
23182
|
-
return template.replace(/\{\{\s*([^}]+)\s*\}\}/g, (_match,
|
|
23183
|
-
const value = readContextPath(context,
|
|
23189
|
+
return template.replace(/\{\{\s*([^}]+)\s*\}\}/g, (_match, path24) => {
|
|
23190
|
+
const value = readContextPath(context, path24.trim());
|
|
23184
23191
|
if (value === void 0 || value === null) return "";
|
|
23185
23192
|
return typeof value === "string" ? value : JSON.stringify(value);
|
|
23186
23193
|
});
|
|
23187
23194
|
}
|
|
23188
|
-
function readContextPath(value,
|
|
23189
|
-
const parts =
|
|
23195
|
+
function readContextPath(value, path24) {
|
|
23196
|
+
const parts = path24.split(".");
|
|
23190
23197
|
let current = value;
|
|
23191
23198
|
for (const part of parts) {
|
|
23192
23199
|
if (current === null || current === void 0) return void 0;
|
|
@@ -25674,8 +25681,8 @@ import { execFileSync as execFileSync3 } from "child_process";
|
|
|
25674
25681
|
import yaml12 from "js-yaml";
|
|
25675
25682
|
import semver from "semver";
|
|
25676
25683
|
function relayCoreVersion() {
|
|
25677
|
-
if (semver.valid("0.
|
|
25678
|
-
return "0.
|
|
25684
|
+
if (semver.valid("0.22.0")) {
|
|
25685
|
+
return "0.22.0";
|
|
25679
25686
|
}
|
|
25680
25687
|
try {
|
|
25681
25688
|
const root = getAppRoot(import.meta.dirname, 3);
|
|
@@ -26014,6 +26021,68 @@ var init_install = __esm({
|
|
|
26014
26021
|
}
|
|
26015
26022
|
});
|
|
26016
26023
|
|
|
26024
|
+
// src/lib/licensing/recap.ts
|
|
26025
|
+
var recap_exports = {};
|
|
26026
|
+
__export(recap_exports, {
|
|
26027
|
+
changelogWindow: () => changelogWindow,
|
|
26028
|
+
entitledPackRecaps: () => entitledPackRecaps
|
|
26029
|
+
});
|
|
26030
|
+
import fs21 from "fs";
|
|
26031
|
+
import path22 from "path";
|
|
26032
|
+
import semver2 from "semver";
|
|
26033
|
+
function changelogWindow(changelog, fromExclusive, toInclusive) {
|
|
26034
|
+
if (!changelog || !toInclusive || !semver2.valid(toInclusive)) return [];
|
|
26035
|
+
const from = fromExclusive && semver2.valid(fromExclusive) ? fromExclusive : null;
|
|
26036
|
+
return Object.entries(changelog).filter(([version]) => semver2.valid(version)).filter(
|
|
26037
|
+
([version]) => semver2.compare(version, toInclusive) <= 0 && (from === null || semver2.compare(version, from) > 0)
|
|
26038
|
+
).sort(([a], [b]) => semver2.compare(a, b)).map(([version, note]) => ({ version, note }));
|
|
26039
|
+
}
|
|
26040
|
+
function entitledPackRecaps(entitlements, opts2 = {}) {
|
|
26041
|
+
try {
|
|
26042
|
+
const appsDir = opts2.appsDir ?? getAinativeAppsDir();
|
|
26043
|
+
const covered = new Set(entitlements);
|
|
26044
|
+
const out = [];
|
|
26045
|
+
for (const tpl of listPackTemplates({ templatesDir: opts2.templatesDir })) {
|
|
26046
|
+
if (tpl.error || !tpl.meta?.entitlement) continue;
|
|
26047
|
+
if (!covered.has(tpl.meta.entitlement)) continue;
|
|
26048
|
+
if (!fs21.existsSync(path22.join(appsDir, tpl.id))) continue;
|
|
26049
|
+
const avail = packUpdateAvailability(tpl.id, {
|
|
26050
|
+
appsDir,
|
|
26051
|
+
templatesDir: opts2.templatesDir
|
|
26052
|
+
});
|
|
26053
|
+
const state = readInstallState(appsDir, tpl.id);
|
|
26054
|
+
const changelog = tpl.meta.changelog;
|
|
26055
|
+
out.push({
|
|
26056
|
+
packId: tpl.id,
|
|
26057
|
+
packName: tpl.meta.name,
|
|
26058
|
+
installedVersion: avail.installedVersion,
|
|
26059
|
+
...state?.installedAt ? { installedAt: state.installedAt } : {},
|
|
26060
|
+
availableVersion: avail.availableVersion,
|
|
26061
|
+
updateAvailable: avail.updateAvailable,
|
|
26062
|
+
...avail.installedVersion && changelog?.[avail.installedVersion] ? { received: changelog[avail.installedVersion] } : {},
|
|
26063
|
+
pending: avail.updateAvailable ? changelogWindow(
|
|
26064
|
+
changelog,
|
|
26065
|
+
avail.installedVersion,
|
|
26066
|
+
avail.availableVersion
|
|
26067
|
+
) : [],
|
|
26068
|
+
...tpl.meta.purchaseUrl ? { purchaseUrl: tpl.meta.purchaseUrl } : {}
|
|
26069
|
+
});
|
|
26070
|
+
}
|
|
26071
|
+
return out;
|
|
26072
|
+
} catch {
|
|
26073
|
+
return [];
|
|
26074
|
+
}
|
|
26075
|
+
}
|
|
26076
|
+
var init_recap = __esm({
|
|
26077
|
+
"src/lib/licensing/recap.ts"() {
|
|
26078
|
+
"use strict";
|
|
26079
|
+
init_ainative_paths();
|
|
26080
|
+
init_catalog();
|
|
26081
|
+
init_update();
|
|
26082
|
+
init_install_state();
|
|
26083
|
+
}
|
|
26084
|
+
});
|
|
26085
|
+
|
|
26017
26086
|
// src/lib/packs/update.ts
|
|
26018
26087
|
var update_exports = {};
|
|
26019
26088
|
__export(update_exports, {
|
|
@@ -26021,15 +26090,15 @@ __export(update_exports, {
|
|
|
26021
26090
|
packUpdateAvailability: () => packUpdateAvailability,
|
|
26022
26091
|
updatePack: () => updatePack
|
|
26023
26092
|
});
|
|
26024
|
-
import
|
|
26025
|
-
import
|
|
26026
|
-
import
|
|
26093
|
+
import fs22 from "fs";
|
|
26094
|
+
import path23 from "path";
|
|
26095
|
+
import semver3 from "semver";
|
|
26027
26096
|
function packUpdateAvailability(appId, opts2 = {}) {
|
|
26028
26097
|
const appsDir = opts2.appsDir ?? getAinativeAppsDir();
|
|
26029
26098
|
const installedVersion = readInstallState(appsDir, appId)?.packVersion ?? null;
|
|
26030
26099
|
const template = findPackTemplate(appId, { templatesDir: opts2.templatesDir });
|
|
26031
26100
|
const availableVersion = template?.meta?.version ?? null;
|
|
26032
|
-
const updateAvailable = availableVersion !== null &&
|
|
26101
|
+
const updateAvailable = availableVersion !== null && semver3.valid(availableVersion) !== null && (installedVersion === null || semver3.valid(installedVersion) === null || semver3.compare(availableVersion, installedVersion) > 0);
|
|
26033
26102
|
return { installedVersion, availableVersion, updateAvailable };
|
|
26034
26103
|
}
|
|
26035
26104
|
async function updatePack(id, options = {}) {
|
|
@@ -26057,13 +26126,13 @@ async function updatePack(id, options = {}) {
|
|
|
26057
26126
|
);
|
|
26058
26127
|
}
|
|
26059
26128
|
const coreVersion = options.coreVersion ?? relayCoreVersion();
|
|
26060
|
-
if (pack.meta.relayCore && !
|
|
26129
|
+
if (pack.meta.relayCore && !semver3.satisfies(coreVersion, pack.meta.relayCore)) {
|
|
26061
26130
|
throw new PackValidationError(
|
|
26062
26131
|
`Pack ${pack.meta.id}@${pack.meta.version} requires relay-core ${pack.meta.relayCore}, but this install is ${coreVersion}.`
|
|
26063
26132
|
);
|
|
26064
26133
|
}
|
|
26065
26134
|
const newVersion = pack.meta.version;
|
|
26066
|
-
if (previousVersion !== null &&
|
|
26135
|
+
if (previousVersion !== null && semver3.valid(previousVersion) && semver3.valid(newVersion) && semver3.compare(newVersion, previousVersion) <= 0) {
|
|
26067
26136
|
return {
|
|
26068
26137
|
packId: id,
|
|
26069
26138
|
previousVersion,
|
|
@@ -26088,8 +26157,21 @@ async function updatePack(id, options = {}) {
|
|
|
26088
26157
|
} catch (err2) {
|
|
26089
26158
|
if (err2 instanceof PackLicenseError2) {
|
|
26090
26159
|
const renew = pack.meta.purchaseUrl ? `renew at ${pack.meta.purchaseUrl}` : `redeem one with: relay license add <path-or-url to your .license.json>`;
|
|
26160
|
+
let withheld = "";
|
|
26161
|
+
try {
|
|
26162
|
+
const { changelogWindow: changelogWindow2 } = await Promise.resolve().then(() => (init_recap(), recap_exports));
|
|
26163
|
+
const pending = changelogWindow2(
|
|
26164
|
+
pack.meta.changelog,
|
|
26165
|
+
previousVersion,
|
|
26166
|
+
newVersion
|
|
26167
|
+
);
|
|
26168
|
+
if (pending.length > 0) {
|
|
26169
|
+
withheld = `This update includes: ` + pending.map((p) => `v${p.version} \u2014 ${p.note}`).join("; ") + ` `;
|
|
26170
|
+
}
|
|
26171
|
+
} catch {
|
|
26172
|
+
}
|
|
26091
26173
|
throw new PackLicenseError2(
|
|
26092
|
-
`Your installed ${id} keeps working \u2014 nothing is locked. Updating to v${newVersion} needs an active license: ${renew}. (${err2.message})`,
|
|
26174
|
+
`Your installed ${id} keeps working \u2014 nothing is locked. Updating to v${newVersion} needs an active license: ${renew}. ` + withheld + `(${err2.message})`,
|
|
26093
26175
|
err2.reason
|
|
26094
26176
|
);
|
|
26095
26177
|
}
|
|
@@ -26097,7 +26179,7 @@ async function updatePack(id, options = {}) {
|
|
|
26097
26179
|
}
|
|
26098
26180
|
}
|
|
26099
26181
|
const resolved = resolvePackLayer(pack);
|
|
26100
|
-
const backupRoot =
|
|
26182
|
+
const backupRoot = path23.join(
|
|
26101
26183
|
appsDir,
|
|
26102
26184
|
id,
|
|
26103
26185
|
"backup",
|
|
@@ -26106,12 +26188,12 @@ async function updatePack(id, options = {}) {
|
|
|
26106
26188
|
const backedUp = [];
|
|
26107
26189
|
for (const file of resolved.files) {
|
|
26108
26190
|
const dest = artifactDestPath(file.relPath, profilesDir, blueprintsDir);
|
|
26109
|
-
if (!dest || !
|
|
26191
|
+
if (!dest || !fs22.existsSync(dest)) continue;
|
|
26110
26192
|
const recorded = state?.files[file.relPath];
|
|
26111
26193
|
if (recorded !== void 0 && hashFileSha256(dest) === recorded) continue;
|
|
26112
|
-
const backupPath =
|
|
26113
|
-
|
|
26114
|
-
|
|
26194
|
+
const backupPath = path23.join(backupRoot, file.relPath);
|
|
26195
|
+
fs22.mkdirSync(path23.dirname(backupPath), { recursive: true });
|
|
26196
|
+
fs22.copyFileSync(dest, backupPath);
|
|
26115
26197
|
backedUp.push(file.relPath);
|
|
26116
26198
|
}
|
|
26117
26199
|
const install = await installPack(packDir, {
|
|
@@ -26395,6 +26477,17 @@ async function runAdd2(source, io) {
|
|
|
26395
26477
|
return 1;
|
|
26396
26478
|
}
|
|
26397
26479
|
}
|
|
26480
|
+
async function pendingRecaps(entitlements, io) {
|
|
26481
|
+
try {
|
|
26482
|
+
const { entitledPackRecaps: entitledPackRecaps2 } = await Promise.resolve().then(() => (init_recap(), recap_exports));
|
|
26483
|
+
return entitledPackRecaps2(entitlements, {
|
|
26484
|
+
appsDir: io.appsDir,
|
|
26485
|
+
templatesDir: io.templatesDir
|
|
26486
|
+
}).filter((r) => r.pending.length > 0);
|
|
26487
|
+
} catch {
|
|
26488
|
+
return [];
|
|
26489
|
+
}
|
|
26490
|
+
}
|
|
26398
26491
|
async function runStatus(io) {
|
|
26399
26492
|
try {
|
|
26400
26493
|
const { listLicenses: listLicenses2 } = await Promise.resolve().then(() => (init_store(), store_exports));
|
|
@@ -26418,6 +26511,16 @@ async function runStatus(io) {
|
|
|
26418
26511
|
io.log(
|
|
26419
26512
|
` Status: ${lic.valid ? "valid" : `invalid \u2014 ${lic.reason ?? "corrupt entry"}`}`
|
|
26420
26513
|
);
|
|
26514
|
+
const recaps = await pendingRecaps(lic.entitlements, io);
|
|
26515
|
+
if (lic.valid && recaps.length > 0) {
|
|
26516
|
+
io.log(` Included in your term, waiting to install:`);
|
|
26517
|
+
for (const r of recaps) {
|
|
26518
|
+
for (const p of r.pending) {
|
|
26519
|
+
io.log(` ${r.packName} v${p.version} \u2014 ${p.note}`);
|
|
26520
|
+
}
|
|
26521
|
+
io.log(` \u2192 relay pack update ${r.packId}`);
|
|
26522
|
+
}
|
|
26523
|
+
}
|
|
26421
26524
|
if (lic.valid && lic.expiresAt) {
|
|
26422
26525
|
const daysLeft = Math.floor(
|
|
26423
26526
|
(new Date(lic.expiresAt).getTime() - now.getTime()) / DAY_MS
|
|
@@ -26426,6 +26529,23 @@ async function runStatus(io) {
|
|
|
26426
26529
|
io.log(
|
|
26427
26530
|
` \u26A0 Renewal: expires in ${daysLeft} day${daysLeft === 1 ? "" : "s"}. Your installed packs are yours forever; renewing keeps new premium packs and updates flowing.`
|
|
26428
26531
|
);
|
|
26532
|
+
const latest = recaps.map((r) => ({ r, p: r.pending[r.pending.length - 1] })).filter((x) => x.p);
|
|
26533
|
+
for (const { r, p } of latest) {
|
|
26534
|
+
io.log(
|
|
26535
|
+
` This license year delivered ${r.packName} v${p.version} \u2014 ${p.note}`
|
|
26536
|
+
);
|
|
26537
|
+
}
|
|
26538
|
+
}
|
|
26539
|
+
}
|
|
26540
|
+
if (!lic.valid && /expired/i.test(lic.reason ?? "") && recaps.length > 0) {
|
|
26541
|
+
io.log(
|
|
26542
|
+
` Your installed packs keep working \u2014 nothing is locked. Renewing unlocks:`
|
|
26543
|
+
);
|
|
26544
|
+
for (const r of recaps) {
|
|
26545
|
+
for (const p of r.pending) {
|
|
26546
|
+
io.log(` ${r.packName} v${p.version} \u2014 ${p.note}`);
|
|
26547
|
+
}
|
|
26548
|
+
if (r.purchaseUrl) io.log(` \u2192 renew at ${r.purchaseUrl}`);
|
|
26429
26549
|
}
|
|
26430
26550
|
}
|
|
26431
26551
|
io.log("");
|
|
@@ -26796,10 +26916,10 @@ import { existsSync as existsSync4, renameSync, cpSync, rmSync as rmSync2, readF
|
|
|
26796
26916
|
import { join as join6 } from "path";
|
|
26797
26917
|
import { homedir as homedir2 } from "os";
|
|
26798
26918
|
import Database from "better-sqlite3";
|
|
26799
|
-
function hasSqliteHeader(
|
|
26919
|
+
function hasSqliteHeader(path24) {
|
|
26800
26920
|
const SQLITE_MAGIC = "SQLite format 3\0";
|
|
26801
26921
|
try {
|
|
26802
|
-
const header = readFileSync3(
|
|
26922
|
+
const header = readFileSync3(path24, { encoding: null });
|
|
26803
26923
|
return header.length >= 16 && header.subarray(0, 16).toString("binary") === SQLITE_MAGIC;
|
|
26804
26924
|
} catch {
|
|
26805
26925
|
return false;
|
package/package.json
CHANGED
package/src/app/packs/page.tsx
CHANGED
|
@@ -6,6 +6,7 @@ import { Boxes, Check, Lock, Package, TriangleAlert } from "lucide-react";
|
|
|
6
6
|
import { listApps } from "@/lib/apps/registry";
|
|
7
7
|
import { listPackTemplates, type PackTemplate } from "@/lib/packs/catalog";
|
|
8
8
|
import { packUpdateAvailability } from "@/lib/packs/update";
|
|
9
|
+
import { changelogWindow } from "@/lib/licensing/recap";
|
|
9
10
|
import { PackInstallButton } from "@/components/packs/pack-install-button";
|
|
10
11
|
import { PackUpdateButton } from "@/components/packs/pack-update-button";
|
|
11
12
|
|
|
@@ -150,11 +151,24 @@ function InstalledActions({ template }: { template: PackTemplate }) {
|
|
|
150
151
|
</Link>
|
|
151
152
|
</div>
|
|
152
153
|
{avail.updateAvailable && avail.availableVersion && (
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
154
|
+
<>
|
|
155
|
+
<PackUpdateButton
|
|
156
|
+
packId={template.id}
|
|
157
|
+
packName={template.meta!.name}
|
|
158
|
+
newVersion={avail.availableVersion}
|
|
159
|
+
/>
|
|
160
|
+
{/* Value-recap one-liner: what the pending update contains, from the
|
|
161
|
+
pack's own changelog — same source as license status + the 402. */}
|
|
162
|
+
{changelogWindow(
|
|
163
|
+
template.meta!.changelog,
|
|
164
|
+
avail.installedVersion,
|
|
165
|
+
avail.availableVersion
|
|
166
|
+
).map((p) => (
|
|
167
|
+
<p key={p.version} className="text-xs text-muted-foreground">
|
|
168
|
+
v{p.version} — {p.note}
|
|
169
|
+
</p>
|
|
170
|
+
))}
|
|
171
|
+
</>
|
|
158
172
|
)}
|
|
159
173
|
</div>
|
|
160
174
|
);
|
package/src/lib/licensing/cli.ts
CHANGED
|
@@ -14,6 +14,9 @@ export interface LicenseCommandIo {
|
|
|
14
14
|
dir?: string;
|
|
15
15
|
/** Injected clock (tests). */
|
|
16
16
|
now?: Date;
|
|
17
|
+
/** Override the recap surface's pack dirs (tests). */
|
|
18
|
+
appsDir?: string;
|
|
19
|
+
templatesDir?: string;
|
|
17
20
|
log: (message: string) => void;
|
|
18
21
|
error: (message: string) => void;
|
|
19
22
|
}
|
|
@@ -115,6 +118,25 @@ async function runAdd(
|
|
|
115
118
|
}
|
|
116
119
|
}
|
|
117
120
|
|
|
121
|
+
/**
|
|
122
|
+
* Fail-open recap lookup — the value-recap is decoration on this surface,
|
|
123
|
+
* never a gate; any fault degrades to "no recap", not an error.
|
|
124
|
+
*/
|
|
125
|
+
async function pendingRecaps(
|
|
126
|
+
entitlements: string[],
|
|
127
|
+
io: LicenseCommandIo
|
|
128
|
+
): Promise<import("./recap").PackRecap[]> {
|
|
129
|
+
try {
|
|
130
|
+
const { entitledPackRecaps } = await import("./recap");
|
|
131
|
+
return entitledPackRecaps(entitlements, {
|
|
132
|
+
appsDir: io.appsDir,
|
|
133
|
+
templatesDir: io.templatesDir,
|
|
134
|
+
}).filter((r) => r.pending.length > 0);
|
|
135
|
+
} catch {
|
|
136
|
+
return [];
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
118
140
|
async function runStatus(io: LicenseCommandIo): Promise<number> {
|
|
119
141
|
try {
|
|
120
142
|
const { listLicenses } = await import("./store");
|
|
@@ -141,6 +163,20 @@ async function runStatus(io: LicenseCommandIo): Promise<number> {
|
|
|
141
163
|
` Status: ${lic.valid ? "valid" : `invalid — ${lic.reason ?? "corrupt entry"}`}`
|
|
142
164
|
);
|
|
143
165
|
|
|
166
|
+
const recaps = await pendingRecaps(lic.entitlements, io);
|
|
167
|
+
|
|
168
|
+
// Value recap (PLG-4a): entitled updates this license already paid for,
|
|
169
|
+
// sitting uninstalled. Explicit invocation ⇒ informational, not a nag.
|
|
170
|
+
if (lic.valid && recaps.length > 0) {
|
|
171
|
+
io.log(` Included in your term, waiting to install:`);
|
|
172
|
+
for (const r of recaps) {
|
|
173
|
+
for (const p of r.pending) {
|
|
174
|
+
io.log(` ${r.packName} v${p.version} — ${p.note}`);
|
|
175
|
+
}
|
|
176
|
+
io.log(` → relay pack update ${r.packId}`);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
144
180
|
// D4: expiry warns about FUTURE premium installs/updates only — it
|
|
145
181
|
// never gates anything already installed, so this is a nudge, not a block.
|
|
146
182
|
if (lic.valid && lic.expiresAt) {
|
|
@@ -153,6 +189,29 @@ async function runStatus(io: LicenseCommandIo): Promise<number> {
|
|
|
153
189
|
`Your installed packs are yours forever; renewing keeps new premium ` +
|
|
154
190
|
`packs and updates flowing.`
|
|
155
191
|
);
|
|
192
|
+
// The generic promise gains this year's specific evidence.
|
|
193
|
+
const latest = recaps
|
|
194
|
+
.map((r) => ({ r, p: r.pending[r.pending.length - 1] }))
|
|
195
|
+
.filter((x) => x.p);
|
|
196
|
+
for (const { r, p } of latest) {
|
|
197
|
+
io.log(
|
|
198
|
+
` This license year delivered ${r.packName} v${p.version} — ${p.note}`
|
|
199
|
+
);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// Expired license, renewal-voiced (same voice as the update gate): what
|
|
205
|
+
// renewal unlocks, never a threat to installed content.
|
|
206
|
+
if (!lic.valid && /expired/i.test(lic.reason ?? "") && recaps.length > 0) {
|
|
207
|
+
io.log(
|
|
208
|
+
` Your installed packs keep working — nothing is locked. Renewing unlocks:`
|
|
209
|
+
);
|
|
210
|
+
for (const r of recaps) {
|
|
211
|
+
for (const p of r.pending) {
|
|
212
|
+
io.log(` ${r.packName} v${p.version} — ${p.note}`);
|
|
213
|
+
}
|
|
214
|
+
if (r.purchaseUrl) io.log(` → renew at ${r.purchaseUrl}`);
|
|
156
215
|
}
|
|
157
216
|
}
|
|
158
217
|
io.log("");
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import semver from "semver";
|
|
4
|
+
import { getAinativeAppsDir } from "@/lib/utils/ainative-paths";
|
|
5
|
+
import { listPackTemplates } from "@/lib/packs/catalog";
|
|
6
|
+
import { packUpdateAvailability } from "@/lib/packs/update";
|
|
7
|
+
import { readInstallState } from "@/lib/packs/install-state";
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Renewal value-recap — the ONE source every renewal surface reads:
|
|
11
|
+
* `license status`, the 402 update refusal, the /packs update card, and the
|
|
12
|
+
* canonical copy relayed for the Website renewal email. Reuses
|
|
13
|
+
* `packUpdateAvailability` for the version comparison (D7: never a second
|
|
14
|
+
* comparison source) and the pack.yaml `changelog` map for the words.
|
|
15
|
+
*
|
|
16
|
+
* Everything here is FAIL-OPEN (cli-startup-robustness rule): a corrupt
|
|
17
|
+
* sidecar, a missing template, or a changelog-less pack degrades to silence —
|
|
18
|
+
* a recap must never crash or block `license status`.
|
|
19
|
+
*
|
|
20
|
+
* Consumers dynamically import this module (TDR-032: keep the CLI's static
|
|
21
|
+
* startup graph free of pack/licensing code).
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
export interface PendingValue {
|
|
25
|
+
version: string;
|
|
26
|
+
note: string;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface PackRecap {
|
|
30
|
+
packId: string;
|
|
31
|
+
packName: string;
|
|
32
|
+
/** From the install-state sidecar; null = unknown (pre-0.21 install). */
|
|
33
|
+
installedVersion: string | null;
|
|
34
|
+
/** Sidecar install timestamp, when known. */
|
|
35
|
+
installedAt?: string;
|
|
36
|
+
availableVersion: string | null;
|
|
37
|
+
updateAvailable: boolean;
|
|
38
|
+
/** Changelog line for the version already installed — value received. */
|
|
39
|
+
received?: string;
|
|
40
|
+
/** Changelog lines in (installed, available], ascending — value pending. */
|
|
41
|
+
pending: PendingValue[];
|
|
42
|
+
purchaseUrl?: string;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Changelog lines in the half-open version window (fromExclusive, toInclusive],
|
|
47
|
+
* ascending. Invalid semver keys are skipped, never thrown on. A null `from`
|
|
48
|
+
* means "everything up to and including `to`".
|
|
49
|
+
*/
|
|
50
|
+
export function changelogWindow(
|
|
51
|
+
changelog: Record<string, string> | undefined,
|
|
52
|
+
fromExclusive: string | null,
|
|
53
|
+
toInclusive: string | null
|
|
54
|
+
): PendingValue[] {
|
|
55
|
+
if (!changelog || !toInclusive || !semver.valid(toInclusive)) return [];
|
|
56
|
+
const from = fromExclusive && semver.valid(fromExclusive) ? fromExclusive : null;
|
|
57
|
+
return Object.entries(changelog)
|
|
58
|
+
.filter(([version]) => semver.valid(version))
|
|
59
|
+
.filter(
|
|
60
|
+
([version]) =>
|
|
61
|
+
semver.compare(version, toInclusive) <= 0 &&
|
|
62
|
+
(from === null || semver.compare(version, from) > 0)
|
|
63
|
+
)
|
|
64
|
+
.sort(([a], [b]) => semver.compare(a, b))
|
|
65
|
+
.map(([version, note]) => ({ version, note }));
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Recaps for every INSTALLED bundled pack whose entitlement is covered by
|
|
70
|
+
* `entitlements`. Uninstalled entitled packs are deliberately absent — that
|
|
71
|
+
* is install-nudge territory (D6), not renewal recap.
|
|
72
|
+
*/
|
|
73
|
+
export function entitledPackRecaps(
|
|
74
|
+
entitlements: string[],
|
|
75
|
+
opts: { appsDir?: string; templatesDir?: string } = {}
|
|
76
|
+
): PackRecap[] {
|
|
77
|
+
try {
|
|
78
|
+
const appsDir = opts.appsDir ?? getAinativeAppsDir();
|
|
79
|
+
const covered = new Set(entitlements);
|
|
80
|
+
const out: PackRecap[] = [];
|
|
81
|
+
|
|
82
|
+
for (const tpl of listPackTemplates({ templatesDir: opts.templatesDir })) {
|
|
83
|
+
if (tpl.error || !tpl.meta?.entitlement) continue;
|
|
84
|
+
if (!covered.has(tpl.meta.entitlement)) continue;
|
|
85
|
+
if (!fs.existsSync(path.join(appsDir, tpl.id))) continue;
|
|
86
|
+
|
|
87
|
+
const avail = packUpdateAvailability(tpl.id, {
|
|
88
|
+
appsDir,
|
|
89
|
+
templatesDir: opts.templatesDir,
|
|
90
|
+
});
|
|
91
|
+
const state = readInstallState(appsDir, tpl.id);
|
|
92
|
+
const changelog = tpl.meta.changelog;
|
|
93
|
+
|
|
94
|
+
out.push({
|
|
95
|
+
packId: tpl.id,
|
|
96
|
+
packName: tpl.meta.name,
|
|
97
|
+
installedVersion: avail.installedVersion,
|
|
98
|
+
...(state?.installedAt ? { installedAt: state.installedAt } : {}),
|
|
99
|
+
availableVersion: avail.availableVersion,
|
|
100
|
+
updateAvailable: avail.updateAvailable,
|
|
101
|
+
...(avail.installedVersion && changelog?.[avail.installedVersion]
|
|
102
|
+
? { received: changelog[avail.installedVersion] }
|
|
103
|
+
: {}),
|
|
104
|
+
pending: avail.updateAvailable
|
|
105
|
+
? changelogWindow(
|
|
106
|
+
changelog,
|
|
107
|
+
avail.installedVersion,
|
|
108
|
+
avail.availableVersion
|
|
109
|
+
)
|
|
110
|
+
: [],
|
|
111
|
+
...(tpl.meta.purchaseUrl ? { purchaseUrl: tpl.meta.purchaseUrl } : {}),
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
return out;
|
|
115
|
+
} catch {
|
|
116
|
+
// Fail-open: a recap is decoration on the licensing surfaces, never a gate.
|
|
117
|
+
return [];
|
|
118
|
+
}
|
|
119
|
+
}
|
package/src/lib/packs/format.ts
CHANGED
|
@@ -51,6 +51,13 @@ export const PackManifestSchema = z
|
|
|
51
51
|
price: z.string().min(1).optional(),
|
|
52
52
|
/** Get-license CTA target on the locked card. */
|
|
53
53
|
purchaseUrl: z.url().optional(),
|
|
54
|
+
/**
|
|
55
|
+
* Per-version customer-voice recap, version → one line. The single source
|
|
56
|
+
* for every renewal value-recap surface (`license status`, the 402 update
|
|
57
|
+
* refusal, the /packs update card, the Website renewal email). Optional —
|
|
58
|
+
* but paid packs should carry it, or their renewal case argues generically.
|
|
59
|
+
*/
|
|
60
|
+
changelog: z.record(z.string(), z.string().min(1)).optional(),
|
|
54
61
|
/** Customer slugs seeded via ensureCustomer at install. */
|
|
55
62
|
customers: z.array(z.string()).default([]),
|
|
56
63
|
})
|
|
@@ -22,5 +22,18 @@ relayCore: ">=0.18.0"
|
|
|
22
22
|
entitlement: product:orionfold-relay
|
|
23
23
|
price: "$499/year"
|
|
24
24
|
purchaseUrl: https://orionfold.com/relay/
|
|
25
|
-
#
|
|
25
|
+
# Per-version customer-voice recap — the single source every renewal surface
|
|
26
|
+
# reads (license status, the update refusal, the /packs card, the renewal
|
|
27
|
+
# email). Add a line here with EVERY version bump; the template test suite
|
|
28
|
+
# requires an entry for the current version.
|
|
29
|
+
changelog:
|
|
30
|
+
"0.1.0": >-
|
|
31
|
+
Six chapters of agency operating system — the Finance Cockpit that closes
|
|
32
|
+
your month by itself, Intake Pipelines, the New-Business Machine,
|
|
33
|
+
client-safe Governance profiles, and the Commercial Real Estate renewal
|
|
34
|
+
engine.
|
|
35
|
+
"0.2.0": >-
|
|
36
|
+
The Nonprofit deep chapter — a grant pipeline that takes every opportunity
|
|
37
|
+
from fit-scored go/no-go through LOI, full application, and post-award
|
|
38
|
+
restricted-funds compliance with a reporting calendar.
|
|
26
39
|
customers: []
|
package/src/lib/packs/update.ts
CHANGED
|
@@ -179,9 +179,29 @@ export async function updatePack(
|
|
|
179
179
|
const renew = pack.meta.purchaseUrl
|
|
180
180
|
? `renew at ${pack.meta.purchaseUrl}`
|
|
181
181
|
: `redeem one with: relay license add <path-or-url to your .license.json>`;
|
|
182
|
+
// Value-recap voice: name what the withheld update contains, from
|
|
183
|
+
// the pack's own changelog (fail-open — silence if it has none).
|
|
184
|
+
let withheld = "";
|
|
185
|
+
try {
|
|
186
|
+
const { changelogWindow } = await import("@/lib/licensing/recap");
|
|
187
|
+
const pending = changelogWindow(
|
|
188
|
+
pack.meta.changelog,
|
|
189
|
+
previousVersion,
|
|
190
|
+
newVersion
|
|
191
|
+
);
|
|
192
|
+
if (pending.length > 0) {
|
|
193
|
+
withheld =
|
|
194
|
+
`This update includes: ` +
|
|
195
|
+
pending.map((p) => `v${p.version} — ${p.note}`).join("; ") +
|
|
196
|
+
` `;
|
|
197
|
+
}
|
|
198
|
+
} catch {
|
|
199
|
+
// recap is decoration on the refusal, never a second failure.
|
|
200
|
+
}
|
|
182
201
|
throw new PackLicenseError(
|
|
183
202
|
`Your installed ${id} keeps working — nothing is locked. ` +
|
|
184
203
|
`Updating to v${newVersion} needs an active license: ${renew}. ` +
|
|
204
|
+
withheld +
|
|
185
205
|
`(${err.message})`,
|
|
186
206
|
err.reason
|
|
187
207
|
);
|
|
@@ -53,7 +53,7 @@ import type { ScheduleSpec } from "@/lib/validators/schedule-spec";
|
|
|
53
53
|
// unfixed from 0.15.0 through 0.16.0 — treat the window test's failure as
|
|
54
54
|
// a release blocker, not noise). The 0.13→0.14 three-MINOR bridge is over;
|
|
55
55
|
// this is the standard 2-MINOR window now.
|
|
56
|
-
const SUPPORTED_API_VERSIONS = new Set([CURRENT_PLUGIN_API_VERSION, "0.
|
|
56
|
+
const SUPPORTED_API_VERSIONS = new Set([CURRENT_PLUGIN_API_VERSION, "0.21"]);
|
|
57
57
|
|
|
58
58
|
/** Test-helper export so the window-enforcement test can read state. */
|
|
59
59
|
export function isSupportedApiVersion(apiVersion: string): boolean {
|
|
@@ -6,7 +6,7 @@ import { z } from "zod";
|
|
|
6
6
|
// (a hardcoded copy there once drifted to "0.14" — scaffolded plugins would
|
|
7
7
|
// have been disabled on load the moment the window tightened). Bump on every
|
|
8
8
|
// MINOR release; api-version-window.test.ts fails if this goes stale.
|
|
9
|
-
export const CURRENT_PLUGIN_API_VERSION = "0.
|
|
9
|
+
export const CURRENT_PLUGIN_API_VERSION = "0.22";
|
|
10
10
|
|
|
11
11
|
// Shared capability tuple — single source of truth used by Zod schema and
|
|
12
12
|
// capability-check.ts hash derivation. Exported so consumers don't need a
|