@happyvertical/smrt-cli 0.52.0 → 0.53.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/AGENTS.md +1 -0
- package/README.md +1 -0
- package/dist/{ApiKey-BZmbOTnL-CMUo3WLf.js → ApiKey-BZmbOTnL-CNdRPt4L.js} +2 -2
- package/dist/ApiKeyCollection-Bfb_nw81-C41xeWjP.js +2 -0
- package/dist/AuditLogCollection-B1YonCAP-CuxT42P9.js +2 -0
- package/dist/{NostrIdentity-n70E0vck-BD7uutxz.js → NostrIdentity-n70E0vck-DyvmAKWA.js} +3 -3
- package/dist/NostrIdentityCollection-Ca74dkaL-BYOCASkU.js +2 -0
- package/dist/{OidcIdentity-Cb1bxreM-DY0aRLu-.js → OidcIdentity-Cb1bxreM-EFGNe-9i.js} +2 -2
- package/dist/OidcIdentityCollection-TV8VdK69-DMFHjlKf.js +2 -0
- package/dist/ProfileAssetCollection-B9hacUJa-B1DI2fld.js +2 -0
- package/dist/{ProfileCollection-DuJT7Mum-D_ScbSMu.js → ProfileCollection-DuJT7Mum-CjXI7sxS.js} +26 -26
- package/dist/ProfileLinkCollection-4uNURz32-BSG4g55p.js +2 -0
- package/dist/ProfileMetadataCollection-DQhbfEfa-BmQ59oGm.js +2 -0
- package/dist/{ProfileMetafieldCollection-CoBuM_5G-BDwo4x3z.js → ProfileMetafieldCollection-CoBuM_5G-Cid3HNFI.js} +1 -1
- package/dist/{ProfileRelationshipCollection-DUVgaIqf-BRrBeg1Q.js → ProfileRelationshipCollection-DUVgaIqf-D-VeP3Xp.js} +1 -1
- package/dist/{ProfileRelationshipTermCollection-DtA9OM23-D4WMfN9a.js → ProfileRelationshipTermCollection-DtA9OM23-BscMcndN.js} +1 -1
- package/dist/{ProfileRelationshipType-BRiRka6t-B32A4UFT.js → ProfileRelationshipType-BRiRka6t-BP33exAz.js} +1 -1
- package/dist/{ProfileRelationshipTypeCollection-BdXwd7Kf-BqQa8Awc.js → ProfileRelationshipTypeCollection-BdXwd7Kf-CSMHKaaN.js} +1 -1
- package/dist/cli-generator.d.ts.map +1 -1
- package/dist/commands/db-qualified-names.d.ts +39 -0
- package/dist/commands/db-qualified-names.d.ts.map +1 -0
- package/dist/commands/gnode.d.ts.map +1 -1
- package/dist/commands/index.d.ts +1 -0
- package/dist/commands/index.d.ts.map +1 -1
- package/dist/commands/mcp-apps-packaging.d.ts +24 -0
- package/dist/commands/mcp-apps-packaging.d.ts.map +1 -0
- package/dist/commands/utilities.d.ts.map +1 -1
- package/dist/{commands-BvcMfEoO.js → commands-DISJzBlT.js} +487 -6
- package/dist/{dist-Dm4q4uB4.js → dist-7S5I_gWk.js} +4 -4
- package/dist/{dist-BTxdRxKP.js → dist-BGMJi2g1.js} +2 -2
- package/dist/{dist-qJw7ALwV.js → dist-BZd_JjEL.js} +10 -6
- package/dist/{dist-vJJL0WMc.js → dist-Ws7B05wT.js} +26 -26
- package/dist/index.js +17 -11
- package/dist/{oidcProvisioningPrimitives-D9OYu5-F-D9DQdiwS.js → oidcProvisioningPrimitives-D9OYu5-F-BqS3T8Kt.js} +4 -4
- package/dist/resolveIdentity-B-PK6RwU-cgVjM6Kk.js +2 -0
- package/package.json +16 -15
- package/dist/ApiKeyCollection-Bfb_nw81-C3Xz7MUq.js +0 -2
- package/dist/AuditLogCollection-B1YonCAP-DmmboC_X.js +0 -2
- package/dist/NostrIdentityCollection-Ca74dkaL--KYlM_ou.js +0 -2
- package/dist/OidcIdentityCollection-TV8VdK69-DPeFJt3R.js +0 -2
- package/dist/ProfileAssetCollection-B9hacUJa-6R8lwjti.js +0 -2
- package/dist/ProfileLinkCollection-4uNURz32-lMKBjaik.js +0 -2
- package/dist/ProfileMetadataCollection-DQhbfEfa-B1QlmBkX.js +0 -2
- package/dist/resolveIdentity-B-PK6RwU-Dvqeq9-I.js +0 -2
|
@@ -2,7 +2,7 @@ import { t as resolveManifestEntryPackageName } from "./index.js";
|
|
|
2
2
|
import { t as DEFAULT_CLI_CONFIG } from "./config-hqNo4lzH.js";
|
|
3
3
|
import { createRequire } from "node:module";
|
|
4
4
|
import * as fs from "node:fs";
|
|
5
|
-
import { existsSync, mkdirSync, readFileSync, realpathSync, writeFileSync } from "node:fs";
|
|
5
|
+
import { cpSync, existsSync, lstatSync, mkdirSync, readFileSync, readdirSync, realpathSync, writeFileSync } from "node:fs";
|
|
6
6
|
import * as path from "node:path";
|
|
7
7
|
import { basename, dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
|
|
8
8
|
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
@@ -21,9 +21,10 @@ import { execFileSync, execSync, spawn, spawnSync } from "node:child_process";
|
|
|
21
21
|
import https from "node:https";
|
|
22
22
|
import { homedir, tmpdir } from "node:os";
|
|
23
23
|
import { extract } from "tar";
|
|
24
|
+
import Ajv2020 from "ajv/dist/2020.js";
|
|
24
25
|
import { importWorkspaceModule } from "@happyvertical/smrt-core/utils/import-workspace-module";
|
|
25
26
|
import { renderForeignKeyAddStatements } from "@happyvertical/smrt-core/schema";
|
|
26
|
-
import { FRAMEWORK_BASE_TABLE_NAMES, buildIntegerWidthTableStatements, collectIntegerWidthTargets, collectNullEqualIndexTargets, dropFrameworkBaseTables, migrateNullEqualIndexes, nullEqualIndexStatements, parsePostgresTimeoutMs, planFrameworkBaseTableDrop, preflightIntegerWidthWidening, preflightNullEqualIndexes, widenIntegerColumnsToBigInt } from "@happyvertical/smrt-core/migrations";
|
|
27
|
+
import { FRAMEWORK_BASE_TABLE_NAMES, backfillLegacyQualifiedNames, buildIntegerWidthTableStatements, collectIntegerWidthTargets, collectNullEqualIndexTargets, countLegacyQualifiedNameReferences, dropFrameworkBaseTables, migrateNullEqualIndexes, nullEqualIndexStatements, parsePostgresTimeoutMs, planFrameworkBaseTableDrop, preflightIntegerWidthWidening, preflightNullEqualIndexes, widenIntegerColumnsToBigInt } from "@happyvertical/smrt-core/migrations";
|
|
27
28
|
import { buildArchitectureContext, buildKnowledgeIndex, buildReviewContext, checkKnowledgeFreshness, compactContextResult, diffKnowledgeIndex, renderFreshnessResult, renderKnowledgeIndexMarkdown } from "@happyvertical/smrt-dev-mcp/knowledge";
|
|
28
29
|
import { RUNTIME_TOOLS } from "@happyvertical/smrt-dev-mcp/runtime";
|
|
29
30
|
//#region src/commands/db-command-utils.ts
|
|
@@ -5683,6 +5684,19 @@ async function loadGitTemplate(gitUrl) {
|
|
|
5683
5684
|
}
|
|
5684
5685
|
}
|
|
5685
5686
|
/**
|
|
5687
|
+
* Get the template directory path (for copying overlay files)
|
|
5688
|
+
*
|
|
5689
|
+
* Prefers the subdir-resolved `__templateRoot` so that subdirectory templates
|
|
5690
|
+
* (`github:user/repo/subdir`) overlay from the subdir rather than the repo root.
|
|
5691
|
+
* Falls back to `__tempDir` for configs loaded before the field existed.
|
|
5692
|
+
*/
|
|
5693
|
+
function getGitTemplateDir(config) {
|
|
5694
|
+
const gitConfig = config;
|
|
5695
|
+
const templateRoot = gitConfig.__templateRoot ?? gitConfig.__tempDir;
|
|
5696
|
+
if (!templateRoot) throw new Error("Template was not loaded from git repository");
|
|
5697
|
+
return templateRoot;
|
|
5698
|
+
}
|
|
5699
|
+
/**
|
|
5686
5700
|
* Cleanup temporary directory after template is used
|
|
5687
5701
|
*/
|
|
5688
5702
|
async function cleanupGitTemplate(config) {
|
|
@@ -6231,8 +6245,310 @@ async function processPlaceholders(outputDir, config, options) {
|
|
|
6231
6245
|
console.log(` Processed placeholders in ${replacedCount} files`);
|
|
6232
6246
|
}
|
|
6233
6247
|
//#endregion
|
|
6248
|
+
//#region src/commands/mcp-apps-packaging.ts
|
|
6249
|
+
/**
|
|
6250
|
+
* Portable MCP Apps plugin packaging helpers.
|
|
6251
|
+
*
|
|
6252
|
+
* These helpers intentionally validate distribution metadata only. Runtime
|
|
6253
|
+
* MCP extension metadata is owned by the v2 server adapter and is not an
|
|
6254
|
+
* install-plugin manifest.
|
|
6255
|
+
*/
|
|
6256
|
+
var requireSchema = createRequire(import.meta.url);
|
|
6257
|
+
var pluginSchema = requireSchema("@happyvertical/smrt-dev-mcp/schemas/agent-plugins-1.0.0/plugin.schema.json");
|
|
6258
|
+
var mcpSchema = requireSchema("@happyvertical/smrt-dev-mcp/schemas/agent-plugins-1.0.0/mcp.schema.json");
|
|
6259
|
+
var ajv = new Ajv2020({
|
|
6260
|
+
strict: true,
|
|
6261
|
+
allErrors: true
|
|
6262
|
+
});
|
|
6263
|
+
var validatePluginSchema = ajv.compile(pluginSchema);
|
|
6264
|
+
var validateMcpSchema = ajv.compile(mcpSchema);
|
|
6265
|
+
var PLUGIN_SCHEMA = "https://agent-plugins.org/schemas/1.0.0/plugin.schema.json";
|
|
6266
|
+
var MCP_SCHEMA = "https://agent-plugins.org/schemas/1.0.0/mcp.schema.json";
|
|
6267
|
+
var CANONICAL_SCOPE_REGISTRY = "@happyvertical:registry=https://npm.happyvertical.com/";
|
|
6268
|
+
var SECRET_KEY = /(?:api[_-]?key|secret|token|password|authorization|credential)/i;
|
|
6269
|
+
var ENVIRONMENT_CREDENTIAL_FILE = /^\.env(?:\.|$)/;
|
|
6270
|
+
var PRIVATE_KEY_CARRIER = /^(?:id_(?:rsa|dsa|ecdsa|ed25519)|private(?:[._-]key)?|.*\.key)$/i;
|
|
6271
|
+
var PRIVATE_KEY_HEADER = /-----BEGIN(?: [A-Z0-9]+)* PRIVATE KEY-----/;
|
|
6272
|
+
function add(result, code, message) {
|
|
6273
|
+
result.findings.push({
|
|
6274
|
+
code,
|
|
6275
|
+
message
|
|
6276
|
+
});
|
|
6277
|
+
}
|
|
6278
|
+
function readJson$1(path, result) {
|
|
6279
|
+
try {
|
|
6280
|
+
return JSON.parse(readFileSync(path, "utf8"));
|
|
6281
|
+
} catch (error) {
|
|
6282
|
+
add(result, "json-malformed", `${relative(process.cwd(), path)} is not valid JSON`);
|
|
6283
|
+
return;
|
|
6284
|
+
}
|
|
6285
|
+
}
|
|
6286
|
+
function isObject(value) {
|
|
6287
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
6288
|
+
}
|
|
6289
|
+
function safeRelativePath(value, field, root, result) {
|
|
6290
|
+
if (typeof value !== "string" || !value.startsWith("./") || value.includes("\\") || value.split("/").includes("..")) {
|
|
6291
|
+
add(result, "unsafe-path", `${field} must be a root-relative ./ path without parent segments`);
|
|
6292
|
+
return;
|
|
6293
|
+
}
|
|
6294
|
+
const target = resolve(root, value);
|
|
6295
|
+
if (!target.startsWith(`${root}${sep}`)) {
|
|
6296
|
+
add(result, "unsafe-path", `${field} resolves outside the plugin root`);
|
|
6297
|
+
return;
|
|
6298
|
+
}
|
|
6299
|
+
try {
|
|
6300
|
+
const stat = lstatSync(target);
|
|
6301
|
+
if (stat.isSymbolicLink() || !stat.isFile()) add(result, "invalid-resource", `${field} must name a regular file`);
|
|
6302
|
+
} catch {
|
|
6303
|
+
add(result, "missing-resource", `${field} does not exist`);
|
|
6304
|
+
}
|
|
6305
|
+
}
|
|
6306
|
+
function scanTree(root, current, result) {
|
|
6307
|
+
for (const entry of readdirSync(current, { withFileTypes: true })) {
|
|
6308
|
+
const path = join(current, entry.name);
|
|
6309
|
+
const display = relative(root, path);
|
|
6310
|
+
const stat = lstatSync(path);
|
|
6311
|
+
if (stat.isSymbolicLink()) add(result, "symlink", `${display} is a symbolic link; plugin packages must contain regular files`);
|
|
6312
|
+
else if (stat.isDirectory()) scanTree(root, path, result);
|
|
6313
|
+
else if (stat.isFile()) if (ENVIRONMENT_CREDENTIAL_FILE.test(entry.name) || PRIVATE_KEY_CARRIER.test(entry.name)) add(result, "secret-artifact", `${display} is a credential carrier and cannot be packaged`);
|
|
6314
|
+
else try {
|
|
6315
|
+
if (PRIVATE_KEY_HEADER.test(readFileSync(path, "utf8"))) add(result, "secret-artifact", `${display} contains a private key and cannot be packaged`);
|
|
6316
|
+
} catch {
|
|
6317
|
+
add(result, "artifact-unreadable", `${display} could not be inspected and cannot be packaged`);
|
|
6318
|
+
}
|
|
6319
|
+
}
|
|
6320
|
+
}
|
|
6321
|
+
function scanSecrets(value, source, result) {
|
|
6322
|
+
if (Array.isArray(value)) {
|
|
6323
|
+
value.forEach((item) => {
|
|
6324
|
+
scanSecrets(item, source, result);
|
|
6325
|
+
});
|
|
6326
|
+
return;
|
|
6327
|
+
}
|
|
6328
|
+
if (!isObject(value)) return;
|
|
6329
|
+
for (const [key, child] of Object.entries(value)) {
|
|
6330
|
+
if (SECRET_KEY.test(key) && typeof child === "string" && child.trim()) add(result, "secret-artifact", `${source} contains a credential-shaped ${key} field`);
|
|
6331
|
+
scanSecrets(child, source, result);
|
|
6332
|
+
}
|
|
6333
|
+
}
|
|
6334
|
+
function validateServerUrl(value, name, result) {
|
|
6335
|
+
if (typeof value !== "string") return add(result, "server-url", `mcpServers.${name}.url must be a string`);
|
|
6336
|
+
try {
|
|
6337
|
+
const url = new URL(value);
|
|
6338
|
+
const isLoopback = url.hostname === "127.0.0.1" || url.hostname === "localhost" || url.hostname === "[::1]";
|
|
6339
|
+
if (url.username || url.password) add(result, "server-url", `mcpServers.${name}.url must not contain credentials`);
|
|
6340
|
+
if (url.protocol !== "https:" && !(url.protocol === "http:" && isLoopback)) add(result, "server-url", `mcpServers.${name}.url must use HTTPS, except loopback development URLs`);
|
|
6341
|
+
} catch {
|
|
6342
|
+
add(result, "server-url", `mcpServers.${name}.url is not a URL`);
|
|
6343
|
+
}
|
|
6344
|
+
}
|
|
6345
|
+
/** Validate portable plugin files and the optional OpenAI install extension. */
|
|
6346
|
+
function validateMcpAppsPackage(rootPath) {
|
|
6347
|
+
const root = resolve(rootPath);
|
|
6348
|
+
const result = {
|
|
6349
|
+
findings: [],
|
|
6350
|
+
valid: false
|
|
6351
|
+
};
|
|
6352
|
+
try {
|
|
6353
|
+
scanTree(root, root, result);
|
|
6354
|
+
} catch {
|
|
6355
|
+
add(result, "plugin-root", "Plugin root must be a readable directory");
|
|
6356
|
+
return result;
|
|
6357
|
+
}
|
|
6358
|
+
const pluginPath = join(root, "plugin.json");
|
|
6359
|
+
const mcpPath = join(root, "mcp.json");
|
|
6360
|
+
if (!lstatExists(pluginPath)) add(result, "plugin-missing", "plugin.json is required at the plugin root");
|
|
6361
|
+
if (!lstatExists(mcpPath)) add(result, "mcp-missing", "mcp.json is required at the plugin root");
|
|
6362
|
+
const plugin = lstatExists(pluginPath) ? readJson$1(pluginPath, result) : void 0;
|
|
6363
|
+
const mcp = lstatExists(mcpPath) ? readJson$1(mcpPath, result) : void 0;
|
|
6364
|
+
if (plugin !== void 0 && !isObject(plugin)) add(result, "plugin-object", "plugin.json must contain an object");
|
|
6365
|
+
if (mcp !== void 0 && !isObject(mcp)) add(result, "mcp-object", "mcp.json must contain an object");
|
|
6366
|
+
if (plugin !== void 0 && !validatePluginSchema(plugin)) add(result, "plugin-format", "plugin.json violates the pinned Agent Plugins 1.0.0 schema");
|
|
6367
|
+
if (mcp !== void 0 && !validateMcpSchema(mcp)) add(result, "mcp-format", "mcp.json violates the pinned Agent Plugins 1.0.0 schema");
|
|
6368
|
+
if (isObject(plugin)) {
|
|
6369
|
+
scanSecrets(plugin, "plugin.json", result);
|
|
6370
|
+
if (plugin.$schema !== PLUGIN_SCHEMA) add(result, "plugin-schema", `plugin.json.$schema must be ${PLUGIN_SCHEMA}`);
|
|
6371
|
+
if (plugin.extensions !== void 0 && !isObject(plugin.extensions)) add(result, "extensions", "plugin.json.extensions must be an object");
|
|
6372
|
+
const openai = (isObject(plugin.extensions) ? plugin.extensions : void 0)?.["com.openai"];
|
|
6373
|
+
if (openai !== void 0 && !isObject(openai)) add(result, "openai-extension", "extensions.com.openai must be an object");
|
|
6374
|
+
if (isObject(openai)) {
|
|
6375
|
+
if (openai.apps !== void 0) safeRelativePath(openai.apps, "extensions.com.openai.apps", root, result);
|
|
6376
|
+
const ui = openai.interface;
|
|
6377
|
+
if (ui !== void 0 && !isObject(ui)) add(result, "openai-interface", "extensions.com.openai.interface must be an object");
|
|
6378
|
+
if (isObject(ui)) {
|
|
6379
|
+
for (const key of ["composerIcon", "logo"]) if (ui[key] !== void 0) safeRelativePath(ui[key], `extensions.com.openai.interface.${key}`, root, result);
|
|
6380
|
+
if (ui.screenshots !== void 0) if (!Array.isArray(ui.screenshots) || ui.screenshots.some((path) => typeof path !== "string")) add(result, "openai-screenshots", "interface.screenshots must be an array of paths");
|
|
6381
|
+
else ui.screenshots.forEach((path, index) => {
|
|
6382
|
+
safeRelativePath(path, `extensions.com.openai.interface.screenshots[${index}]`, root, result);
|
|
6383
|
+
});
|
|
6384
|
+
}
|
|
6385
|
+
}
|
|
6386
|
+
}
|
|
6387
|
+
if (isObject(mcp)) {
|
|
6388
|
+
scanSecrets(mcp, "mcp.json", result);
|
|
6389
|
+
if (mcp.$schema !== MCP_SCHEMA) add(result, "mcp-schema", `mcp.json.$schema must be ${MCP_SCHEMA}`);
|
|
6390
|
+
if (!isObject(mcp.mcpServers) || Object.keys(mcp.mcpServers).length === 0) add(result, "mcp-servers", "mcp.json requires a non-empty mcpServers object");
|
|
6391
|
+
else for (const [name, server] of Object.entries(mcp.mcpServers)) {
|
|
6392
|
+
if (!name.trim() || !isObject(server)) {
|
|
6393
|
+
add(result, "mcp-server", `mcpServers.${name || "<empty>"} must be an object with a name`);
|
|
6394
|
+
continue;
|
|
6395
|
+
}
|
|
6396
|
+
if (server.type !== "streamable-http") add(result, "mcp-transport", `mcpServers.${name}.type must be streamable-http`);
|
|
6397
|
+
validateServerUrl(server.url, name, result);
|
|
6398
|
+
}
|
|
6399
|
+
}
|
|
6400
|
+
result.valid = result.findings.length === 0;
|
|
6401
|
+
return result;
|
|
6402
|
+
}
|
|
6403
|
+
function lstatExists(path) {
|
|
6404
|
+
try {
|
|
6405
|
+
return lstatSync(path).isFile();
|
|
6406
|
+
} catch {
|
|
6407
|
+
return false;
|
|
6408
|
+
}
|
|
6409
|
+
}
|
|
6410
|
+
/**
|
|
6411
|
+
* Add the public, credential-free SMRT package route to a generated app.
|
|
6412
|
+
* A pre-existing scope route is an explicit consumer decision and must not be
|
|
6413
|
+
* silently replaced.
|
|
6414
|
+
*/
|
|
6415
|
+
function configureMcpAppsConsumerRegistry(appRoot) {
|
|
6416
|
+
const npmrcPath = join(resolve(appRoot), ".npmrc");
|
|
6417
|
+
let existing = "";
|
|
6418
|
+
try {
|
|
6419
|
+
const stat = lstatSync(npmrcPath);
|
|
6420
|
+
if (stat.isSymbolicLink() || !stat.isFile()) throw new Error("Generated application .npmrc must be a regular file");
|
|
6421
|
+
existing = readFileSync(npmrcPath, "utf8");
|
|
6422
|
+
} catch (error) {
|
|
6423
|
+
if (error.code !== "ENOENT") throw error;
|
|
6424
|
+
}
|
|
6425
|
+
const scopeLines = existing.split(/\r?\n/).filter((line) => /^\s*@happyvertical:registry(?:\s|=|$)/.test(line));
|
|
6426
|
+
if (scopeLines.length > 0) {
|
|
6427
|
+
for (const line of scopeLines) {
|
|
6428
|
+
const match = /^\s*@happyvertical:registry\s*=\s*(\S+?)(?:\s+[;#].*)?\s*$/.exec(line);
|
|
6429
|
+
if (!match || match[1] !== "https://npm.happyvertical.com/") throw new Error("Generated application already declares a different or malformed @happyvertical registry; refusing to replace it");
|
|
6430
|
+
}
|
|
6431
|
+
return;
|
|
6432
|
+
}
|
|
6433
|
+
writeFileSync(npmrcPath, `${existing}${existing && !existing.endsWith("\n") ? "\n" : ""}${CANONICAL_SCOPE_REGISTRY}\n`);
|
|
6434
|
+
}
|
|
6435
|
+
/** Copy the opt-in app route/resources and merge only its declared dependencies. */
|
|
6436
|
+
function addMcpAppsRuntime(sourceRoot, appRoot) {
|
|
6437
|
+
const resolvedSource = resolve(sourceRoot);
|
|
6438
|
+
const source = join(lstatSync(resolvedSource).isFile() ? dirname(resolvedSource) : resolvedSource, "mcp-apps-template");
|
|
6439
|
+
const dependencies = JSON.parse(readFileSync(join(source, "package.dependencies.json"), "utf8"));
|
|
6440
|
+
cpSync(join(source, "src"), join(resolve(appRoot), "src"), {
|
|
6441
|
+
recursive: true,
|
|
6442
|
+
force: false,
|
|
6443
|
+
errorOnExist: true
|
|
6444
|
+
});
|
|
6445
|
+
const packagePath = join(resolve(appRoot), "package.json");
|
|
6446
|
+
const packageJson = JSON.parse(readFileSync(packagePath, "utf8"));
|
|
6447
|
+
packageJson.dependencies = {
|
|
6448
|
+
...packageJson.dependencies,
|
|
6449
|
+
...dependencies
|
|
6450
|
+
};
|
|
6451
|
+
writeFileSync(packagePath, `${JSON.stringify(packageJson, null, 2)}\n`);
|
|
6452
|
+
}
|
|
6453
|
+
function assertMcpAppsPluginName(name) {
|
|
6454
|
+
if (!validatePluginSchema({
|
|
6455
|
+
$schema: PLUGIN_SCHEMA,
|
|
6456
|
+
name
|
|
6457
|
+
})) throw new Error("Plugin name must satisfy Agent Plugins 1.0.0 (1–64 lowercase letters, digits, dots or hyphens; no repeated separators).");
|
|
6458
|
+
}
|
|
6459
|
+
/** Create the minimal local portable package. It intentionally has no .app.json: registered IDs are host-owned. */
|
|
6460
|
+
function scaffoldMcpAppsPackage(rootPath, name, version = "0.1.0") {
|
|
6461
|
+
assertMcpAppsPluginName(name);
|
|
6462
|
+
const root = resolve(rootPath);
|
|
6463
|
+
mkdirSync(root, { recursive: true });
|
|
6464
|
+
const pluginPath = join(root, "plugin.json");
|
|
6465
|
+
const mcpPath = join(root, "mcp.json");
|
|
6466
|
+
if (lstatExists(pluginPath) || lstatExists(mcpPath)) throw new Error("Refusing to overwrite existing MCP Apps package files");
|
|
6467
|
+
writeFileSync(pluginPath, `${JSON.stringify({
|
|
6468
|
+
$schema: PLUGIN_SCHEMA,
|
|
6469
|
+
name,
|
|
6470
|
+
version,
|
|
6471
|
+
description: "SMRT MCP Apps package",
|
|
6472
|
+
extensions: { "com.openai": { interface: {
|
|
6473
|
+
displayName: name,
|
|
6474
|
+
shortDescription: "SMRT MCP Apps package"
|
|
6475
|
+
} } }
|
|
6476
|
+
}, null, 2)}\n`);
|
|
6477
|
+
writeFileSync(mcpPath, `${JSON.stringify({
|
|
6478
|
+
$schema: MCP_SCHEMA,
|
|
6479
|
+
mcpServers: { smrt: {
|
|
6480
|
+
type: "streamable-http",
|
|
6481
|
+
url: "http://127.0.0.1:3000/api/mcp"
|
|
6482
|
+
} }
|
|
6483
|
+
}, null, 2)}\n`);
|
|
6484
|
+
}
|
|
6485
|
+
var mcpAppsCommands = {
|
|
6486
|
+
"mcp-apps scaffold": {
|
|
6487
|
+
name: "mcp-apps scaffold",
|
|
6488
|
+
description: "Create opt-in portable MCP Apps plugin metadata for a local SMRT app",
|
|
6489
|
+
options: {
|
|
6490
|
+
"output-dir": {
|
|
6491
|
+
type: "string",
|
|
6492
|
+
description: "Plugin directory (defaults to ./mcp-apps)"
|
|
6493
|
+
},
|
|
6494
|
+
name: {
|
|
6495
|
+
type: "string",
|
|
6496
|
+
description: "Portable plugin name",
|
|
6497
|
+
default: "smrt-app"
|
|
6498
|
+
},
|
|
6499
|
+
version: {
|
|
6500
|
+
type: "string",
|
|
6501
|
+
description: "Portable plugin version",
|
|
6502
|
+
default: "0.1.0"
|
|
6503
|
+
}
|
|
6504
|
+
},
|
|
6505
|
+
handler: async (_args, options) => {
|
|
6506
|
+
const root = typeof options["output-dir"] === "string" ? options["output-dir"] : "./mcp-apps";
|
|
6507
|
+
scaffoldMcpAppsPackage(root, typeof options.name === "string" ? options.name : "smrt-app", typeof options.version === "string" ? options.version : "0.1.0");
|
|
6508
|
+
console.log(`Created portable MCP Apps metadata in ${root}.`);
|
|
6509
|
+
console.log("Mount and authorize the native v2 /api/mcp endpoint before changing the loopback URL.");
|
|
6510
|
+
}
|
|
6511
|
+
},
|
|
6512
|
+
"mcp-apps validate": {
|
|
6513
|
+
name: "mcp-apps validate",
|
|
6514
|
+
description: "Fail closed when portable MCP Apps package metadata is unsafe or incomplete",
|
|
6515
|
+
options: { "output-dir": {
|
|
6516
|
+
type: "string",
|
|
6517
|
+
description: "Plugin directory (defaults to ./mcp-apps)"
|
|
6518
|
+
} },
|
|
6519
|
+
handler: async (_args, options) => {
|
|
6520
|
+
const root = typeof options["output-dir"] === "string" ? options["output-dir"] : "./mcp-apps";
|
|
6521
|
+
const result = validateMcpAppsPackage(root);
|
|
6522
|
+
for (const finding of result.findings) console.error(`${finding.code}: ${finding.message}`);
|
|
6523
|
+
if (!result.valid) throw new Error(`MCP Apps package validation failed with ${result.findings.length} finding(s)`);
|
|
6524
|
+
console.log(`MCP Apps package at ${root} is valid.`);
|
|
6525
|
+
}
|
|
6526
|
+
}
|
|
6527
|
+
};
|
|
6528
|
+
//#endregion
|
|
6234
6529
|
//#region src/commands/gnode.ts
|
|
6235
6530
|
/**
|
|
6531
|
+
* Gnode CLI Commands
|
|
6532
|
+
*
|
|
6533
|
+
* Commands for creating and managing gnodes (federated local knowledge bases)
|
|
6534
|
+
*/
|
|
6535
|
+
var SCOPED_PACKAGE_NAME = /^@[a-z0-9][a-z0-9._-]*\/[a-z0-9][a-z0-9._-]*$/;
|
|
6536
|
+
function projectIdentity(name, outputDir) {
|
|
6537
|
+
const packagePath = join(outputDir, "package.json");
|
|
6538
|
+
if (existsSync(packagePath)) {
|
|
6539
|
+
const existing = JSON.parse(readFileSync(packagePath, "utf8"));
|
|
6540
|
+
if (typeof existing.name !== "string" || !SCOPED_PACKAGE_NAME.test(existing.name)) throw new Error("Generated applications require a scoped package identity (for example @smrt-app/my-app); rename the existing project before generating into it");
|
|
6541
|
+
return existing.name;
|
|
6542
|
+
}
|
|
6543
|
+
if (SCOPED_PACKAGE_NAME.test(name)) return name;
|
|
6544
|
+
const slug = name.toLowerCase().replace(/[^a-z0-9._-]+/g, "-").replace(/^-+|-+$/g, "");
|
|
6545
|
+
if (!slug) throw new Error("MCP Apps project name must contain a package slug");
|
|
6546
|
+
return `@smrt-app/${slug}`;
|
|
6547
|
+
}
|
|
6548
|
+
function mcpAppsPluginName(packageName) {
|
|
6549
|
+
return packageName.split("/").at(-1) ?? packageName;
|
|
6550
|
+
}
|
|
6551
|
+
/**
|
|
6236
6552
|
* Gnode commands for CLI
|
|
6237
6553
|
*/
|
|
6238
6554
|
var gnodeCommands = {
|
|
@@ -6266,6 +6582,10 @@ var gnodeCommands = {
|
|
|
6266
6582
|
type: "string",
|
|
6267
6583
|
description: "IANA timezone (e.g., \"America/Edmonton\")",
|
|
6268
6584
|
default: "America/Edmonton"
|
|
6585
|
+
},
|
|
6586
|
+
"mcp-apps": {
|
|
6587
|
+
type: "boolean",
|
|
6588
|
+
description: "Add opt-in portable MCP Apps plugin metadata under mcp-apps/"
|
|
6269
6589
|
}
|
|
6270
6590
|
},
|
|
6271
6591
|
handler: async (args, options) => {
|
|
@@ -6273,26 +6593,38 @@ var gnodeCommands = {
|
|
|
6273
6593
|
if (!name) throw new Error("Project name is required: smrt gnode create <name>");
|
|
6274
6594
|
const outputDir = options.outputDir || `./${name}`;
|
|
6275
6595
|
const templateName = options.template || "sveltekit";
|
|
6596
|
+
const mcpApps = options.mcpApps || options["mcp-apps"];
|
|
6597
|
+
const projectName = projectIdentity(name, outputDir);
|
|
6598
|
+
if (mcpApps) assertMcpAppsPluginName(mcpAppsPluginName(projectName));
|
|
6276
6599
|
const siteOptions = options.location || options.lat || options.lon ? {
|
|
6277
6600
|
location: options.location,
|
|
6278
6601
|
latitude: options.lat ? Number(options.lat) : void 0,
|
|
6279
6602
|
longitude: options.lon ? Number(options.lon) : void 0,
|
|
6280
6603
|
timezone: options.timezone
|
|
6281
6604
|
} : void 0;
|
|
6605
|
+
let gitConfig;
|
|
6282
6606
|
try {
|
|
6283
6607
|
console.log(`🔍 Resolving template: ${templateName}...`);
|
|
6284
6608
|
const source = await resolveTemplate(templateName);
|
|
6285
6609
|
console.log(`✓ Found template: ${source.type}:${source.location}\n`);
|
|
6286
6610
|
const config = await loadTemplate(source);
|
|
6611
|
+
if (source.type === "git") gitConfig = config;
|
|
6287
6612
|
await generate(source, config, {
|
|
6288
|
-
name,
|
|
6613
|
+
name: projectName,
|
|
6289
6614
|
template: templateName,
|
|
6290
6615
|
outputDir,
|
|
6291
6616
|
site: siteOptions
|
|
6292
6617
|
});
|
|
6293
|
-
if (
|
|
6618
|
+
if (mcpApps || config.name === "sveltekit") configureMcpAppsConsumerRegistry(outputDir);
|
|
6619
|
+
if (mcpApps) {
|
|
6620
|
+
addMcpAppsRuntime(source.type === "git" ? getGitTemplateDir(config) : source.resolved, outputDir);
|
|
6621
|
+
scaffoldMcpAppsPackage(`${outputDir}/mcp-apps`, mcpAppsPluginName(projectName));
|
|
6622
|
+
console.log("✓ Added MCP Apps metadata and the canonical @happyvertical package registry. Configure the authorized v2 MCP endpoint before deployment.");
|
|
6623
|
+
}
|
|
6294
6624
|
} catch (error) {
|
|
6295
6625
|
throw new Error(`Failed to create gnode: ${error instanceof Error ? error.message : "Unknown error"}`, { cause: error });
|
|
6626
|
+
} finally {
|
|
6627
|
+
if (gitConfig) await cleanupGitTemplate(gitConfig);
|
|
6296
6628
|
}
|
|
6297
6629
|
}
|
|
6298
6630
|
},
|
|
@@ -7077,7 +7409,7 @@ var dbMaterializeTenantHierarchyCommand = {
|
|
|
7077
7409
|
if (!config.database?.url || config.database.url === ":memory:") throw new Error("Database configuration required for db:materialize-tenant-hierarchy. Configure database.url in smrt.config.ts (or DATABASE_URL).");
|
|
7078
7410
|
const dbType = config.database.type || "sqlite";
|
|
7079
7411
|
const { getDatabase } = await import("@happyvertical/sql");
|
|
7080
|
-
const { materializeTenantHierarchy, TenantHierarchyMaterializationError } = await import("./dist-
|
|
7412
|
+
const { materializeTenantHierarchy, TenantHierarchyMaterializationError } = await import("./dist-Ws7B05wT.js");
|
|
7081
7413
|
db = await getDatabase({
|
|
7082
7414
|
type: dbType,
|
|
7083
7415
|
url: config.database.url
|
|
@@ -7286,7 +7618,7 @@ var dbMigrateLedgerAccountsCommand = {
|
|
|
7286
7618
|
if (dbType !== "postgres") throw new Error("db:migrate-ledger-accounts currently supports PostgreSQL only; no changes were made. See the smrt-ledgers README for SQLite.");
|
|
7287
7619
|
await autoDiscoverAndLoad();
|
|
7288
7620
|
const { getDatabase } = await import("@happyvertical/sql");
|
|
7289
|
-
const { migrateLedgerAccountsTable, planLedgerAccountsTableMove } = await import("./dist-
|
|
7621
|
+
const { migrateLedgerAccountsTable, planLedgerAccountsTableMove } = await import("./dist-7S5I_gWk.js");
|
|
7290
7622
|
db = await getDatabase({
|
|
7291
7623
|
type: dbType,
|
|
7292
7624
|
url: config.database.url
|
|
@@ -8354,6 +8686,139 @@ async function columnIsUuid(db, table, column) {
|
|
|
8354
8686
|
return rows[0]?.data_type === "uuid";
|
|
8355
8687
|
}
|
|
8356
8688
|
//#endregion
|
|
8689
|
+
//#region src/commands/db-qualified-names.ts
|
|
8690
|
+
/**
|
|
8691
|
+
* Stored references to deprecated qualified names (#3338).
|
|
8692
|
+
*
|
|
8693
|
+
* `smrt doctor --db` reports how many rows still store a name a class
|
|
8694
|
+
* declared in `@smrt({ previousQualifiedNames })` — the signal that an alias
|
|
8695
|
+
* can (or cannot yet) be removed. `smrt db:migrate-qualified-names` is the
|
|
8696
|
+
* opt-in, idempotent cleanup; nothing rewrites stored names automatically.
|
|
8697
|
+
*/
|
|
8698
|
+
async function connect() {
|
|
8699
|
+
const { getPackageConfig } = await import("@happyvertical/smrt-config");
|
|
8700
|
+
const { DEFAULT_CLI_CONFIG } = await import("./config-hqNo4lzH.js").then((n) => n.n);
|
|
8701
|
+
const config = getPackageConfig("cli", DEFAULT_CLI_CONFIG);
|
|
8702
|
+
if (!config.database?.url || config.database.url === ":memory:") throw new Error("No persistent database is configured. Set `database.url` in smrt.config.ts (or DATABASE_URL).");
|
|
8703
|
+
const dbType = config.database.type || "sqlite";
|
|
8704
|
+
const { getDatabase } = await import("@happyvertical/sql");
|
|
8705
|
+
return {
|
|
8706
|
+
db: await getDatabase({
|
|
8707
|
+
type: dbType,
|
|
8708
|
+
url: config.database.url
|
|
8709
|
+
}),
|
|
8710
|
+
dbType,
|
|
8711
|
+
url: config.database.url
|
|
8712
|
+
};
|
|
8713
|
+
}
|
|
8714
|
+
/**
|
|
8715
|
+
* Count stored deprecated qualified names in the configured database.
|
|
8716
|
+
* Never throws for an operational problem.
|
|
8717
|
+
*/
|
|
8718
|
+
async function runLegacyQualifiedNameReport(options = {}) {
|
|
8719
|
+
let db;
|
|
8720
|
+
try {
|
|
8721
|
+
if (options.discover ?? true) await autoDiscoverAndLoad();
|
|
8722
|
+
const connection = await connect();
|
|
8723
|
+
db = connection.db;
|
|
8724
|
+
return {
|
|
8725
|
+
report: await countLegacyQualifiedNameReferences(db, { engineHint: connection.dbType }),
|
|
8726
|
+
error: null
|
|
8727
|
+
};
|
|
8728
|
+
} catch (error) {
|
|
8729
|
+
return {
|
|
8730
|
+
report: null,
|
|
8731
|
+
error: redactConnectionStringsInText(error instanceof Error ? error.message : String(error))
|
|
8732
|
+
};
|
|
8733
|
+
} finally {
|
|
8734
|
+
await closeDatabaseConnection(db);
|
|
8735
|
+
}
|
|
8736
|
+
}
|
|
8737
|
+
/**
|
|
8738
|
+
* Render a legacy-name report as the lines `smrt doctor --db` and
|
|
8739
|
+
* `smrt db:migrate-qualified-names` print.
|
|
8740
|
+
*
|
|
8741
|
+
* A zero count is a removal signal only when it is global and complete: a
|
|
8742
|
+
* tenant-scoped count (`tenantId`) or one that skipped untenanted tables
|
|
8743
|
+
* never says the aliases are removable. Stored rows are also only half the
|
|
8744
|
+
* picture, so every report reminds the operator that source still naming an
|
|
8745
|
+
* old name must move first.
|
|
8746
|
+
*/
|
|
8747
|
+
function formatLegacyQualifiedNameReport(report, options = {}) {
|
|
8748
|
+
if (report.aliases.length === 0) return [" No class declares previousQualifiedNames."];
|
|
8749
|
+
const scope = options.tenantId !== void 0 ? ` for tenant ${options.tenantId}` : "";
|
|
8750
|
+
const lines = [` Declared aliases: ${report.aliases.length}`, ...report.aliases.map(({ alias, current }) => ` - ${alias} → ${current}`)];
|
|
8751
|
+
if (report.total > 0) {
|
|
8752
|
+
lines.push(` ⚠️ ${report.total} stored reference(s)${scope} still use a deprecated name:`);
|
|
8753
|
+
for (const reference of report.references) lines.push(` - ${reference.table}.${reference.column} = ${reference.alias}: ${reference.count} row(s)`);
|
|
8754
|
+
lines.push(" They keep resolving. Rewrite them early with `smrt db:migrate-qualified-names` (opt-in); keep the aliases until this count is zero.");
|
|
8755
|
+
} else if (options.tenantId !== void 0) lines.push(` ✅ No counted row${scope} uses a deprecated name. This is a tenant-scoped count, not a removal signal: other tenants and untenanted tables were not counted. Run without --tenant (or \`smrt doctor --db\`) before deciding to remove an alias.`);
|
|
8756
|
+
else if (report.untenantedTables.length > 0) lines.push(" ✅ No counted row uses a deprecated name, but some tables were skipped (below), so this is not a removal signal.");
|
|
8757
|
+
else lines.push(" ✅ No stored row in this database uses a deprecated name. The aliases may be removed in a breaking release once every deployment reports zero and no source still names them.");
|
|
8758
|
+
if (report.untenantedTables.length > 0) lines.push(` Skipped (no tenant column, not counted): ${report.untenantedTables.join(", ")}`);
|
|
8759
|
+
if (report.missingTables.length > 0) lines.push(` (not migrated in this database: ${report.missingTables.join(", ")})`);
|
|
8760
|
+
lines.push(" Before removing an alias, also update source still naming an old qualified name (@crossPackageRef / relationship targets, playbook step models) to the current name.");
|
|
8761
|
+
return lines;
|
|
8762
|
+
}
|
|
8763
|
+
/** `smrt db:migrate-qualified-names` — the opt-in rewrite. */
|
|
8764
|
+
var dbMigrateQualifiedNamesCommand = {
|
|
8765
|
+
name: "db:migrate-qualified-names",
|
|
8766
|
+
description: "Rewrite stored deprecated qualified names (declared via @smrt({ previousQualifiedNames })) to current names. Opt-in and idempotent (#3338).",
|
|
8767
|
+
aliases: ["migrate-qualified-names"],
|
|
8768
|
+
args: [],
|
|
8769
|
+
options: {
|
|
8770
|
+
"dry-run": {
|
|
8771
|
+
type: "boolean",
|
|
8772
|
+
description: "Report the rows that would be rewritten without writing.",
|
|
8773
|
+
default: false
|
|
8774
|
+
},
|
|
8775
|
+
tenant: {
|
|
8776
|
+
type: "string",
|
|
8777
|
+
description: "Rewrite only this tenant's rows (tables without a tenant column are skipped)."
|
|
8778
|
+
},
|
|
8779
|
+
force: {
|
|
8780
|
+
type: "boolean",
|
|
8781
|
+
description: "Run again even if this alias set was already recorded.",
|
|
8782
|
+
default: false
|
|
8783
|
+
}
|
|
8784
|
+
},
|
|
8785
|
+
handler: async (_args, options) => {
|
|
8786
|
+
let db;
|
|
8787
|
+
try {
|
|
8788
|
+
await autoDiscoverAndLoad();
|
|
8789
|
+
const connection = await connect();
|
|
8790
|
+
db = connection.db;
|
|
8791
|
+
console.log("\n🏷️ Deprecated qualified-name backfill\n");
|
|
8792
|
+
console.log(`✓ Connected to ${formatDatabaseDisplayUrl(connection.dbType, connection.url)}\n`);
|
|
8793
|
+
const result = await backfillLegacyQualifiedNames(db, {
|
|
8794
|
+
engineHint: connection.dbType,
|
|
8795
|
+
dryRun: options["dry-run"] === true,
|
|
8796
|
+
force: options.force === true,
|
|
8797
|
+
...options.tenant ? { tenantId: options.tenant } : {}
|
|
8798
|
+
});
|
|
8799
|
+
for (const line of formatLegacyQualifiedNameReport(result.before, { ...options.tenant ? { tenantId: options.tenant } : {} })) console.log(line);
|
|
8800
|
+
console.log();
|
|
8801
|
+
if (result.dryRun) {
|
|
8802
|
+
console.log("DRY RUN: no changes made.\n");
|
|
8803
|
+
return;
|
|
8804
|
+
}
|
|
8805
|
+
if (!result.ran) {
|
|
8806
|
+
console.log(result.before.aliases.length === 0 ? "Nothing to do.\n" : `Already applied (${result.backfillName}); pass --force to run again.\n`);
|
|
8807
|
+
return;
|
|
8808
|
+
}
|
|
8809
|
+
for (const reference of result.rewritten) console.log(`✓ ${reference.table}.${reference.column}: ${reference.count} row(s) ${reference.alias} → ${reference.current}`);
|
|
8810
|
+
for (const reference of result.skippedDuplicates) console.log(`⚠️ ${reference.table}.${reference.column}: ${reference.count} row(s) kept as ${reference.alias} — a row with the same identity already uses ${reference.current}. Resolve the duplicates, then re-run.`);
|
|
8811
|
+
if (result.skippedDuplicates.length > 0) process.exitCode = 1;
|
|
8812
|
+
console.log(result.recorded ? `\n✓ Recorded ${result.backfillName}.\n` : "\nNot recorded: references remain.\n");
|
|
8813
|
+
} catch (error) {
|
|
8814
|
+
console.error(`\n❌ Qualified-name backfill failed: ${redactConnectionStringsInText(error instanceof Error ? error.message : String(error))}\n`);
|
|
8815
|
+
process.exitCode = 1;
|
|
8816
|
+
} finally {
|
|
8817
|
+
await closeDatabaseConnection(db);
|
|
8818
|
+
}
|
|
8819
|
+
}
|
|
8820
|
+
};
|
|
8821
|
+
//#endregion
|
|
8357
8822
|
//#region src/commands/dev-knowledge.ts
|
|
8358
8823
|
/**
|
|
8359
8824
|
* dev:knowledge-* commands
|
|
@@ -9547,6 +10012,7 @@ function renderAgentSurfaceReport(report) {
|
|
|
9547
10012
|
* Utility commands for CLI
|
|
9548
10013
|
*/
|
|
9549
10014
|
var utilityCommands = {
|
|
10015
|
+
...mcpAppsCommands,
|
|
9550
10016
|
introspect: {
|
|
9551
10017
|
name: "introspect",
|
|
9552
10018
|
description: "Analyze project and discover SMRT objects",
|
|
@@ -10961,6 +11427,11 @@ export default testManifest;
|
|
|
10961
11427
|
check("SMRT objects discovered", false, void 0, "Could not discover manifests");
|
|
10962
11428
|
}
|
|
10963
11429
|
const runtimeCheck = await runRuntimeCheckSafely(cwd);
|
|
11430
|
+
const mcpAppsPath = resolve(cwd, "mcp-apps");
|
|
11431
|
+
if (existsSync(mcpAppsPath)) {
|
|
11432
|
+
const mcpApps = validateMcpAppsPackage(mcpAppsPath);
|
|
11433
|
+
check("Portable MCP Apps package", mcpApps.valid, mcpApps.findings.map((finding) => `${finding.code}: ${finding.message}`).join("; "));
|
|
11434
|
+
}
|
|
10964
11435
|
const projectManifestPath = resolve(cwd, ".smrt", "manifest.json");
|
|
10965
11436
|
const registerPath = resolve(cwd, ".smrt", "register.js");
|
|
10966
11437
|
if (existsSync(projectManifestPath)) try {
|
|
@@ -11026,6 +11497,15 @@ export default testManifest;
|
|
|
11026
11497
|
for (const line of formatPermissionsOutcome(permissions)) console.log(line);
|
|
11027
11498
|
if (!permissions.skipped) check("PostgreSQL role permissions", !permissions.error && permissions.plan?.diagnostics.length === 0, permissions.error ?? "PostgreSQL role permissions differ from the configured contract");
|
|
11028
11499
|
console.log();
|
|
11500
|
+
console.log("🏷️ Deprecated Qualified Names\n");
|
|
11501
|
+
const legacyNames = await runLegacyQualifiedNameReport({ discover: false });
|
|
11502
|
+
if (!legacyNames.report) check("Deprecated qualified-name references", false, legacyNames.error ?? "Could not count stored references");
|
|
11503
|
+
else {
|
|
11504
|
+
for (const line of formatLegacyQualifiedNameReport(legacyNames.report)) console.log(line);
|
|
11505
|
+
if (legacyNames.report.total > 0) warnings.push(`deprecated qualified names: ${legacyNames.report.total} stored reference(s)`);
|
|
11506
|
+
else passed.push("Deprecated qualified-name references");
|
|
11507
|
+
}
|
|
11508
|
+
console.log();
|
|
11029
11509
|
}
|
|
11030
11510
|
console.log("━".repeat(50));
|
|
11031
11511
|
console.log(`\n📊 Summary\n`);
|
|
@@ -11063,6 +11543,7 @@ export default testManifest;
|
|
|
11063
11543
|
"db:migrate-null-equal-indexes": dbMigrateNullEqualIndexesCommand,
|
|
11064
11544
|
"db:migrate-agent-schedule-slugs": dbMigrateAgentScheduleSlugsCommand,
|
|
11065
11545
|
"db:migrate-ledger-accounts": dbMigrateLedgerAccountsCommand,
|
|
11546
|
+
"db:migrate-qualified-names": dbMigrateQualifiedNamesCommand,
|
|
11066
11547
|
"db:materialize-tenant-hierarchy": dbMaterializeTenantHierarchyCommand,
|
|
11067
11548
|
"db:drop-framework-base-tables": dbDropFrameworkBaseTablesCommand,
|
|
11068
11549
|
"db:orphans": dbOrphansCommand,
|