@happyvertical/smrt-cli 0.55.9 → 0.55.11
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 +11 -19
- package/README.md +4 -0
- package/agents/cookbook-apply.md +65 -0
- package/agents/kitchen.md +111 -0
- package/agents/process-fixture-isolation.md +29 -0
- package/cookbooks/bakery.cookbook.json +187 -0
- package/cookbooks/mechanic.cookbook.json +191 -0
- package/cookbooks/welder.cookbook.json +177 -0
- package/cookbooks/yoga-studio.cookbook.json +173 -0
- package/dist/{AuditLogCollection-WDHL-_AF-HQQINXuT.js → AuditLogCollection-WDHL-_AF-ClQNu_VR.js} +1 -1
- package/dist/{OidcIdentity-BUblgQLs-B3dRU6nj.js → OidcIdentity-BUblgQLs-br8RyLkA.js} +2 -2
- package/dist/{OidcIdentityCollection-ClOyaHeF-C7-jxx9o.js → OidcIdentityCollection-ClOyaHeF-BpQXB3mz.js} +1 -1
- package/dist/{ProfileAssetCollection-CRMcjLJ--CQ4QjtjK.js → ProfileAssetCollection-CRMcjLJ--C5ytBKfF.js} +3 -3
- package/dist/{ProfileCollection-C4INKh7Y-C22q_JqL.js → ProfileCollection-C4INKh7Y-CfkyJilm.js} +17 -17
- package/dist/{ProfileLinkCollection-DFfZRsgv-BsRRI9lh.js → ProfileLinkCollection-DFfZRsgv-MQ2zhD9z.js} +2 -2
- package/dist/{ProfileMetadataCollection-BIYKy4Y3-C6hpFkOa.js → ProfileMetadataCollection-BIYKy4Y3-DXu5L2yx.js} +1 -1
- package/dist/{ProfileMetafieldCollection-Buz7gzag-BLXQjqBG.js → ProfileMetafieldCollection-Buz7gzag-C2tZc1PF.js} +1 -1
- package/dist/{ProfileRelationshipCollection-C4g_QLI4-DNywujKX.js → ProfileRelationshipCollection-C4g_QLI4-vTnkAIAh.js} +1 -1
- package/dist/{ProfileTypeCollection-B7kwi1mU-DPXH67Rh.js → ProfileTypeCollection-B7kwi1mU-KwPGcUkI.js} +2 -2
- package/dist/apply-BNDIdixP.js +947 -0
- package/dist/cli-generator.d.ts.map +1 -1
- package/dist/commands/cookbook/apply.d.ts +102 -0
- package/dist/commands/cookbook/apply.d.ts.map +1 -0
- package/dist/commands/cookbook/recipe-index.d.ts +108 -0
- package/dist/commands/cookbook/recipe-index.d.ts.map +1 -0
- package/dist/commands/cookbook/registry.d.ts +69 -0
- package/dist/commands/cookbook/registry.d.ts.map +1 -0
- package/dist/commands/cookbook/shared.d.ts +23 -0
- package/dist/commands/cookbook/shared.d.ts.map +1 -0
- package/dist/commands/cookbook/validate.d.ts +25 -0
- package/dist/commands/cookbook/validate.d.ts.map +1 -0
- package/dist/commands/cookbook.d.ts +6 -0
- package/dist/commands/cookbook.d.ts.map +1 -0
- package/dist/commands/index.d.ts +2 -0
- package/dist/commands/index.d.ts.map +1 -1
- package/dist/commands/kitchen/ai.d.ts +34 -0
- package/dist/commands/kitchen/ai.d.ts.map +1 -0
- package/dist/commands/kitchen/apply.d.ts +52 -0
- package/dist/commands/kitchen/apply.d.ts.map +1 -0
- package/dist/commands/kitchen/mcp/catalog.d.ts +39 -0
- package/dist/commands/kitchen/mcp/catalog.d.ts.map +1 -0
- package/dist/commands/kitchen/mcp/server.d.ts +15 -0
- package/dist/commands/kitchen/mcp/server.d.ts.map +1 -0
- package/dist/commands/kitchen/mcp/tools.d.ts +68 -0
- package/dist/commands/kitchen/mcp/tools.d.ts.map +1 -0
- package/dist/commands/kitchen/planner.d.ts +71 -0
- package/dist/commands/kitchen/planner.d.ts.map +1 -0
- package/dist/commands/kitchen/run.d.ts +40 -0
- package/dist/commands/kitchen/run.d.ts.map +1 -0
- package/dist/commands/kitchen/server.d.ts +39 -0
- package/dist/commands/kitchen/server.d.ts.map +1 -0
- package/dist/commands/kitchen.d.ts +7 -0
- package/dist/commands/kitchen.d.ts.map +1 -0
- package/dist/{commands-NBDTnB3h.js → commands-pbRVQvix.js} +759 -9
- package/dist/dist-BsRSQ_hE.js +4117 -0
- package/dist/{dist-DuVZ0o9F.js → dist-CYVIKmvy.js} +4 -4
- package/dist/dist-D_WqjwkC.js +21 -0
- package/dist/{dist-FZLsgj__.js → dist-DxkSjKQQ.js} +2 -2
- package/dist/dist-ffh0LeC4.js +1788 -0
- package/dist/index.js +44 -16
- package/dist/{oidcProvisioningPrimitives-DNP9EZlU-CbUV0zfC.js → oidcProvisioningPrimitives-DNP9EZlU-CBYwksp0.js} +3 -3
- package/dist/{resolveIdentity-CkyqvctR-qw-nqxJ3.js → resolveIdentity-CkyqvctR-B5btFsQd.js} +14 -14
- package/dist/server-Dp096tZj.js +898 -0
- package/package.json +13 -10
- package/dist/dist-B-kbXFDM.js +0 -4113
- package/dist/dist-BKuPI8_N.js +0 -1788
- package/dist/dist-DqmJkgkG.js +0 -21
|
@@ -1,28 +1,30 @@
|
|
|
1
1
|
import { t as resolveManifestEntryPackageName } from "./index.js";
|
|
2
2
|
import { a as redactDatabaseUrlsInText, i as quoteIdentifier, r as formatDatabaseDisplayUrl, t as closeDatabaseConnection } from "./db-command-utils-Cn8tjy7N.js";
|
|
3
|
+
import { a as readCookbookSource, d as downloadPackage, f as extractTarball, g as applyCookbook, h as COOKBOOK_CONFIG_FILE, i as printApplyResult, m as ApplyError, n as asList, o as validateCookbookSource, p as lookupPackage, r as cliVersion, t as createCookbookApplier } from "./apply-BNDIdixP.js";
|
|
3
4
|
import { t as DEFAULT_CLI_CONFIG } from "./config-CdJRSMRi.js";
|
|
4
5
|
import { addMcpAppsRuntime, assertMcpAppsPluginName, configureMcpAppsConsumerRegistry, mcpAppsCommands, scaffoldMcpAppsPackage, validateMcpAppsPackage } from "./mcp-apps-packaging-CixRnxz_.js";
|
|
5
6
|
import { createRequire } from "node:module";
|
|
6
7
|
import * as fs from "node:fs";
|
|
7
|
-
import { existsSync, mkdirSync, readFileSync, realpathSync, writeFileSync } from "node:fs";
|
|
8
|
+
import { createReadStream, existsSync, mkdirSync, mkdtempSync, readFileSync, readdirSync, realpathSync, renameSync, rmSync, statSync, writeFileSync } from "node:fs";
|
|
8
9
|
import * as path from "node:path";
|
|
9
|
-
import { basename, dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
|
|
10
|
+
import { basename, dirname, extname, isAbsolute, join, normalize, relative, resolve, sep } from "node:path";
|
|
10
11
|
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
11
12
|
import { ObjectRegistry, SchemaComparer, checkLiveSchemaParity, collectForeignKeyOrphanCounts, createQualifiedName, ensureDeferredSystemTableCompatibility, generateDDLForEngine, getClassName, isQualifiedName, isSmrtCollectionExtendsName, migratePostgresSystemTimestamps, parseQualifiedName, planForeignKeyCreation, planPostgresSystemTimestampMigrations } from "@happyvertical/smrt-core";
|
|
12
13
|
import { loadExternalManifestSync } from "@happyvertical/smrt-core/manifest";
|
|
13
|
-
import { execFileSync, execSync, spawn, spawnSync } from "node:child_process";
|
|
14
|
+
import { execFile, execFileSync, execSync, spawn, spawnSync } from "node:child_process";
|
|
14
15
|
import { homedir, tmpdir } from "node:os";
|
|
15
|
-
import { createHash } from "node:crypto";
|
|
16
|
+
import { createHash, randomBytes, timingSafeEqual } from "node:crypto";
|
|
17
|
+
import { extract } from "tar";
|
|
16
18
|
import { access, cp, mkdir, readFile, rm, writeFile } from "node:fs/promises";
|
|
17
19
|
import { createLogger } from "@happyvertical/logger";
|
|
18
20
|
import glob from "fast-glob";
|
|
19
|
-
import { getPackageConfig } from "@happyvertical/smrt-config";
|
|
21
|
+
import { getPackageConfig, loadConfig, toAIClientOptions, tryResolveConfiguredAIProvider } from "@happyvertical/smrt-config";
|
|
20
22
|
import { toSnakeCase as toSnakeCase$1 } from "@happyvertical/smrt-core/utils";
|
|
21
23
|
import { checkKnowledgeGraphFreshness, discoverScopedPackageDirectories, readAgentModuleDocs, readPackageAgentDoc, resolveAgentModuleDocPaths } from "@happyvertical/smrt-core/knowledge";
|
|
22
24
|
import { MCPGenerator } from "@happyvertical/smrt-core/generators";
|
|
23
25
|
import { generateDeclarationsFromCLI } from "@happyvertical/smrt-core/prebuild";
|
|
24
26
|
import https from "node:https";
|
|
25
|
-
import {
|
|
27
|
+
import { createServer } from "node:http";
|
|
26
28
|
import { importWorkspaceModule } from "@happyvertical/smrt-core/utils/import-workspace-module";
|
|
27
29
|
import { renderForeignKeyAddStatements } from "@happyvertical/smrt-core/schema";
|
|
28
30
|
import { FRAMEWORK_BASE_TABLE_NAMES, backfillLegacyQualifiedNames, buildIntegerWidthTableStatements, collectIntegerWidthTargets, collectNullEqualIndexTargets, countLegacyQualifiedNameReferences, dropFrameworkBaseTables, migrateNullEqualIndexes, nullEqualIndexStatements, parsePostgresTimeoutMs, planFrameworkBaseTableDrop, preflightIntegerWidthWidening, preflightNullEqualIndexes, widenIntegerColumnsToBigInt } from "@happyvertical/smrt-core/migrations";
|
|
@@ -175,6 +177,143 @@ var configExportCommand = {
|
|
|
175
177
|
}
|
|
176
178
|
};
|
|
177
179
|
//#endregion
|
|
180
|
+
//#region src/commands/cookbook.ts
|
|
181
|
+
/**
|
|
182
|
+
* `smrt cookbook validate|apply` (#3748). See `agents/cookbook.md`.
|
|
183
|
+
*/
|
|
184
|
+
async function loadAndValidate(source, options, dir) {
|
|
185
|
+
return validateCookbookSource(await readCookbookSource(source), {
|
|
186
|
+
dir,
|
|
187
|
+
manifests: asList(options.manifests),
|
|
188
|
+
registry: !options["no-registry"]
|
|
189
|
+
});
|
|
190
|
+
}
|
|
191
|
+
function printReport(source, report, json) {
|
|
192
|
+
if (json) {
|
|
193
|
+
console.log(JSON.stringify({
|
|
194
|
+
ok: report.ok,
|
|
195
|
+
source,
|
|
196
|
+
errors: report.errors,
|
|
197
|
+
warnings: report.warnings,
|
|
198
|
+
packages: report.packages
|
|
199
|
+
}, null, 2));
|
|
200
|
+
return;
|
|
201
|
+
}
|
|
202
|
+
for (const warning of report.warnings) console.warn(`warning: ${warning}`);
|
|
203
|
+
if (report.ok) {
|
|
204
|
+
console.log(`${source} is a valid cookbook (${report.packages.length} package(s))`);
|
|
205
|
+
return;
|
|
206
|
+
}
|
|
207
|
+
console.error(`${source} is not a valid cookbook:`);
|
|
208
|
+
for (const error of report.errors) console.error(` - ${error}`);
|
|
209
|
+
}
|
|
210
|
+
var sharedOptions = {
|
|
211
|
+
json: {
|
|
212
|
+
type: "boolean",
|
|
213
|
+
description: "Print machine-readable JSON",
|
|
214
|
+
default: false
|
|
215
|
+
},
|
|
216
|
+
manifests: {
|
|
217
|
+
type: "string",
|
|
218
|
+
description: "Extra package manifests (file or directory); repeatable. Searched before the workspace, node_modules and the registry",
|
|
219
|
+
multiple: true
|
|
220
|
+
},
|
|
221
|
+
"no-registry": {
|
|
222
|
+
type: "boolean",
|
|
223
|
+
description: "Do not fetch manifests of uninstalled packages from the registry",
|
|
224
|
+
default: false
|
|
225
|
+
}
|
|
226
|
+
};
|
|
227
|
+
var cookbookCommands = {
|
|
228
|
+
"cookbook validate": {
|
|
229
|
+
name: "cookbook validate",
|
|
230
|
+
description: "Validate a cookbook (file or URL) against cookbook/v1 and the recipe manifests",
|
|
231
|
+
args: ["file"],
|
|
232
|
+
options: sharedOptions,
|
|
233
|
+
handler: async (args, options) => {
|
|
234
|
+
const source = args[0];
|
|
235
|
+
if (!source) {
|
|
236
|
+
console.error("Usage: smrt cookbook validate <file|url> [--json]");
|
|
237
|
+
process.exit(2);
|
|
238
|
+
}
|
|
239
|
+
let report;
|
|
240
|
+
try {
|
|
241
|
+
report = await loadAndValidate(source, options, process.cwd());
|
|
242
|
+
} catch (error) {
|
|
243
|
+
console.error(error instanceof Error ? error.message : String(error));
|
|
244
|
+
process.exit(1);
|
|
245
|
+
}
|
|
246
|
+
printReport(source, report, options.json);
|
|
247
|
+
if (!report.ok) process.exit(1);
|
|
248
|
+
}
|
|
249
|
+
},
|
|
250
|
+
"cookbook apply": {
|
|
251
|
+
name: "cookbook apply",
|
|
252
|
+
description: "Create a project from a cookbook (smrt-start template), or update one with --into",
|
|
253
|
+
args: ["file", "dir"],
|
|
254
|
+
options: {
|
|
255
|
+
...sharedOptions,
|
|
256
|
+
template: {
|
|
257
|
+
type: "string",
|
|
258
|
+
description: "Project template: a local path, github:owner/repo[#ref], or a git URL (default github:happyvertical/smrt-start)"
|
|
259
|
+
},
|
|
260
|
+
into: {
|
|
261
|
+
type: "boolean",
|
|
262
|
+
description: "Apply to an existing project (dir, default: current directory)",
|
|
263
|
+
default: false
|
|
264
|
+
},
|
|
265
|
+
"dry-run": {
|
|
266
|
+
type: "boolean",
|
|
267
|
+
description: "Print the planned changes without writing anything",
|
|
268
|
+
default: false
|
|
269
|
+
},
|
|
270
|
+
"no-install": {
|
|
271
|
+
type: "boolean",
|
|
272
|
+
description: "Skip the package install",
|
|
273
|
+
default: false
|
|
274
|
+
}
|
|
275
|
+
},
|
|
276
|
+
handler: async (args, options) => {
|
|
277
|
+
const [source, dir] = args;
|
|
278
|
+
if (!source) {
|
|
279
|
+
console.error("Usage: smrt cookbook apply <file|url> [dir] [--into] [--template <t>] [--dry-run] [--no-install]");
|
|
280
|
+
process.exit(2);
|
|
281
|
+
}
|
|
282
|
+
try {
|
|
283
|
+
const report = await loadAndValidate(source, options, resolve(dir ?? "."));
|
|
284
|
+
if (!report.ok || !report.cookbook) {
|
|
285
|
+
printReport(source, report, options.json);
|
|
286
|
+
process.exit(1);
|
|
287
|
+
}
|
|
288
|
+
const result = applyCookbook({
|
|
289
|
+
cookbook: report.cookbook,
|
|
290
|
+
packages: report.packages,
|
|
291
|
+
dir,
|
|
292
|
+
into: options.into,
|
|
293
|
+
template: options.template,
|
|
294
|
+
dryRun: options["dry-run"],
|
|
295
|
+
install: !options["no-install"],
|
|
296
|
+
cliVersion: cliVersion()
|
|
297
|
+
});
|
|
298
|
+
if (options.json) {
|
|
299
|
+
console.log(JSON.stringify({
|
|
300
|
+
...result,
|
|
301
|
+
warnings: report.warnings
|
|
302
|
+
}, null, 2));
|
|
303
|
+
return;
|
|
304
|
+
}
|
|
305
|
+
printApplyResult(result, report.warnings);
|
|
306
|
+
} catch (error) {
|
|
307
|
+
if (error instanceof ApplyError) {
|
|
308
|
+
console.error(error.message);
|
|
309
|
+
process.exit(1);
|
|
310
|
+
}
|
|
311
|
+
throw error;
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
};
|
|
316
|
+
//#endregion
|
|
178
317
|
//#region src/discovery/manifest-discovery.ts
|
|
179
318
|
/**
|
|
180
319
|
* Manifest Discovery System
|
|
@@ -6768,6 +6907,617 @@ var initCommands = { init: {
|
|
|
6768
6907
|
}
|
|
6769
6908
|
} };
|
|
6770
6909
|
//#endregion
|
|
6910
|
+
//#region src/commands/kitchen/planner.ts
|
|
6911
|
+
/**
|
|
6912
|
+
* Locating the planner for `smrt kitchen` (#3750).
|
|
6913
|
+
*
|
|
6914
|
+
* The planner package (`@happyvertical/smrt-planner`) carries a ~70 MB static
|
|
6915
|
+
* app, so the CLI does not depend on it. It is found, in order:
|
|
6916
|
+
*
|
|
6917
|
+
* 1. `--planner <dir>`: a packaged checkout (`pnpm package` in the planner repo).
|
|
6918
|
+
* 2. A cached copy under the user cache dir, one directory per version.
|
|
6919
|
+
* 3. The registry that owns the `@happyvertical` scope: the newest version is
|
|
6920
|
+
* downloaded and extracted into the cache (no install, no dependencies).
|
|
6921
|
+
*
|
|
6922
|
+
* If none is available the error says how to fix it.
|
|
6923
|
+
*/
|
|
6924
|
+
var PLANNER_PACKAGE = "@happyvertical/smrt-planner";
|
|
6925
|
+
var PlannerError = class extends Error {};
|
|
6926
|
+
/**
|
|
6927
|
+
* Where downloaded planners live: `$SMRT_CACHE_DIR/planner`, else the platform
|
|
6928
|
+
* user cache (`$XDG_CACHE_HOME`, `~/Library/Caches`, `%LOCALAPPDATA%`, `~/.cache`)
|
|
6929
|
+
* `/smrt/planner`. Each version is a subdirectory.
|
|
6930
|
+
*/
|
|
6931
|
+
function plannerCacheDir(env = process.env) {
|
|
6932
|
+
if (env.SMRT_CACHE_DIR) return join(env.SMRT_CACHE_DIR, "planner");
|
|
6933
|
+
return join(env.XDG_CACHE_HOME || (process.platform === "darwin" ? join(homedir(), "Library", "Caches") : process.platform === "win32" ? env.LOCALAPPDATA || join(homedir(), "AppData", "Local") : join(homedir(), ".cache")), "smrt", "planner");
|
|
6934
|
+
}
|
|
6935
|
+
function exportTarget(value) {
|
|
6936
|
+
if (typeof value === "string") return value;
|
|
6937
|
+
if (value && typeof value === "object") {
|
|
6938
|
+
const record = value;
|
|
6939
|
+
return exportTarget(record.default ?? record.import);
|
|
6940
|
+
}
|
|
6941
|
+
}
|
|
6942
|
+
/** Validate a package directory as a packaged planner. */
|
|
6943
|
+
function inspectPlanner(root, source) {
|
|
6944
|
+
const dir = resolve(root);
|
|
6945
|
+
let pkg;
|
|
6946
|
+
try {
|
|
6947
|
+
pkg = JSON.parse(readFileSync(join(dir, "package.json"), "utf-8"));
|
|
6948
|
+
} catch {
|
|
6949
|
+
throw new PlannerError(`${dir} is not a planner package (no readable package.json)`);
|
|
6950
|
+
}
|
|
6951
|
+
if (pkg.name !== "@happyvertical/smrt-planner") throw new PlannerError(`${dir} is not ${PLANNER_PACKAGE} (package name is ${pkg.name ?? "missing"})`);
|
|
6952
|
+
const exportsMap = pkg.exports;
|
|
6953
|
+
const core = exportTarget(exportsMap?.["./core"]);
|
|
6954
|
+
const coreFile = core ? resolve(dir, core) : "";
|
|
6955
|
+
const appDir = join(dir, "app");
|
|
6956
|
+
if (!core || !existsSync(coreFile) || !existsSync(join(appDir, "index.html"))) throw new PlannerError(`${dir} is not packaged (missing ${!core || !existsSync(coreFile) ? "dist/core" : "app/index.html"}); run \`pnpm package\` in the planner checkout`);
|
|
6957
|
+
return {
|
|
6958
|
+
root: dir,
|
|
6959
|
+
appDir,
|
|
6960
|
+
coreFile,
|
|
6961
|
+
version: pkg.version,
|
|
6962
|
+
source
|
|
6963
|
+
};
|
|
6964
|
+
}
|
|
6965
|
+
var versionParts = (v) => v.split(/[.-]/).map((p) => /^\d+$/.test(p) ? Number(p) : p);
|
|
6966
|
+
function compareVersions(a, b) {
|
|
6967
|
+
const x = versionParts(a);
|
|
6968
|
+
const y = versionParts(b);
|
|
6969
|
+
for (let i = 0; i < Math.max(x.length, y.length); i += 1) {
|
|
6970
|
+
const l = x[i] ?? 0;
|
|
6971
|
+
const r = y[i] ?? 0;
|
|
6972
|
+
if (l === r) continue;
|
|
6973
|
+
if (typeof l === "number" && typeof r === "number") return l - r;
|
|
6974
|
+
return String(l) < String(r) ? -1 : 1;
|
|
6975
|
+
}
|
|
6976
|
+
return 0;
|
|
6977
|
+
}
|
|
6978
|
+
function cachedVersions(cache) {
|
|
6979
|
+
if (!existsSync(cache)) return [];
|
|
6980
|
+
return readdirSync(cache).filter((name) => existsSync(join(cache, name, "package", "package.json"))).sort(compareVersions);
|
|
6981
|
+
}
|
|
6982
|
+
function tryInspect(root) {
|
|
6983
|
+
try {
|
|
6984
|
+
return inspectPlanner(root, "cache");
|
|
6985
|
+
} catch {
|
|
6986
|
+
return;
|
|
6987
|
+
}
|
|
6988
|
+
}
|
|
6989
|
+
/** Find the planner: `--planner`, the cache, then the registry. */
|
|
6990
|
+
async function resolvePlanner(options = {}) {
|
|
6991
|
+
const log = options.log ?? (() => {});
|
|
6992
|
+
if (options.planner) return inspectPlanner(options.planner, "local");
|
|
6993
|
+
const cache = options.cacheDir ?? plannerCacheDir();
|
|
6994
|
+
const found = await lookupPackage(PLANNER_PACKAGE, {
|
|
6995
|
+
dir: options.dir,
|
|
6996
|
+
...options.registry
|
|
6997
|
+
});
|
|
6998
|
+
if (found?.version) {
|
|
6999
|
+
const target = join(cache, found.version);
|
|
7000
|
+
const cached = tryInspect(join(target, "package"));
|
|
7001
|
+
if (cached) return cached;
|
|
7002
|
+
log(`Downloading the planner ${found.version} (about 70 MB, cached after)…`);
|
|
7003
|
+
const tarball = await downloadPackage(found, options.registry?.fetchImpl);
|
|
7004
|
+
if (tarball) {
|
|
7005
|
+
mkdirSync(cache, { recursive: true });
|
|
7006
|
+
const work = mkdtempSync(join(cache, ".download-"));
|
|
7007
|
+
try {
|
|
7008
|
+
await extractTarball(tarball.data, work);
|
|
7009
|
+
rmSync(target, {
|
|
7010
|
+
recursive: true,
|
|
7011
|
+
force: true
|
|
7012
|
+
});
|
|
7013
|
+
renameSync(work, target);
|
|
7014
|
+
const installed = inspectPlanner(join(target, "package"), "registry");
|
|
7015
|
+
log(`Planner ${found.version} cached in ${target}`);
|
|
7016
|
+
return installed;
|
|
7017
|
+
} catch (error) {
|
|
7018
|
+
rmSync(work, {
|
|
7019
|
+
recursive: true,
|
|
7020
|
+
force: true
|
|
7021
|
+
});
|
|
7022
|
+
throw error instanceof PlannerError ? error : new PlannerError(`Could not unpack ${PLANNER_PACKAGE}@${found.version}: ${error instanceof Error ? error.message : String(error)}`);
|
|
7023
|
+
}
|
|
7024
|
+
}
|
|
7025
|
+
}
|
|
7026
|
+
for (const version of cachedVersions(cache).reverse()) {
|
|
7027
|
+
const cached = tryInspect(join(cache, version, "package"));
|
|
7028
|
+
if (cached) {
|
|
7029
|
+
log(`Could not reach the registry; using the cached planner ${version}`);
|
|
7030
|
+
return cached;
|
|
7031
|
+
}
|
|
7032
|
+
}
|
|
7033
|
+
throw new PlannerError(`The planner (${PLANNER_PACKAGE}) is not available: it is not in ${cache} and the registry for the @happyvertical scope did not provide it.\nCheck your network and .npmrc, or point at a packaged checkout with --planner <dir> (run \`pnpm package\` in the planner repo).`);
|
|
7034
|
+
}
|
|
7035
|
+
/** Import the planner's plain-Node `./core`. */
|
|
7036
|
+
async function loadPlannerCore(install) {
|
|
7037
|
+
try {
|
|
7038
|
+
return await import(
|
|
7039
|
+
/* @vite-ignore */
|
|
7040
|
+
pathToFileURL(install.coreFile).href
|
|
7041
|
+
);
|
|
7042
|
+
} catch (error) {
|
|
7043
|
+
throw new PlannerError(`Could not load the planner core from ${install.coreFile}: ${error instanceof Error ? error.message : String(error)}`);
|
|
7044
|
+
}
|
|
7045
|
+
}
|
|
7046
|
+
//#endregion
|
|
7047
|
+
//#region src/commands/kitchen/ai.ts
|
|
7048
|
+
/**
|
|
7049
|
+
* The language model behind `smrt kitchen`'s chat endpoint (#3750): the
|
|
7050
|
+
* user's configured `@happyvertical/ai` provider, resolved the way the rest of
|
|
7051
|
+
* s-m-r-t resolves it (the `ai` block of `smrt.config.ts`, then `SMRT_AI_*`,
|
|
7052
|
+
* then the provider's own key variable such as `OPENAI_API_KEY`).
|
|
7053
|
+
*/
|
|
7054
|
+
async function defaultCreateClient(options) {
|
|
7055
|
+
const { getAI } = await import("@happyvertical/ai");
|
|
7056
|
+
return getAI(options);
|
|
7057
|
+
}
|
|
7058
|
+
/**
|
|
7059
|
+
* The configured model, or null when no provider is configured (the kitchen
|
|
7060
|
+
* then runs the planner's in-browser model instead).
|
|
7061
|
+
*/
|
|
7062
|
+
async function resolveKitchenAI(options = {}) {
|
|
7063
|
+
const searchFrom = options.dir && existsSync(resolve(options.dir)) ? resolve(options.dir) : process.cwd();
|
|
7064
|
+
try {
|
|
7065
|
+
await loadConfig({ searchFrom });
|
|
7066
|
+
} catch {}
|
|
7067
|
+
const resolved = tryResolveConfiguredAIProvider({ env: options.env });
|
|
7068
|
+
if (!resolved?.provider) return null;
|
|
7069
|
+
const client = await (options.createClient ?? defaultCreateClient)(toAIClientOptions(resolved));
|
|
7070
|
+
return {
|
|
7071
|
+
label: `${resolved.provider}${resolved.model ? ` (${resolved.model})` : ""}`,
|
|
7072
|
+
async chat(messages) {
|
|
7073
|
+
return (await client.chat(messages, { temperature: 0 })).content;
|
|
7074
|
+
}
|
|
7075
|
+
};
|
|
7076
|
+
}
|
|
7077
|
+
//#endregion
|
|
7078
|
+
//#region src/commands/kitchen/server.ts
|
|
7079
|
+
/**
|
|
7080
|
+
* The local server behind `smrt kitchen` (#3750): the planner's static app,
|
|
7081
|
+
* the host-mode chat endpoint, and the one endpoint that receives the cookbook.
|
|
7082
|
+
*
|
|
7083
|
+
* It only ever listens on 127.0.0.1 (a random port) and every request must
|
|
7084
|
+
* carry a `Host` of this server, which defeats DNS rebinding. State-changing
|
|
7085
|
+
* requests are same-origin JSON; the cookbook also needs the one-time token.
|
|
7086
|
+
*/
|
|
7087
|
+
var CHAT_PATH = "/api/planner/chat";
|
|
7088
|
+
var COOKBOOK_PATH = "/api/kitchen/cookbook";
|
|
7089
|
+
var CHAT_LIMIT = 256 * 1024;
|
|
7090
|
+
var COOKBOOK_LIMIT = 2 * 1024 * 1024;
|
|
7091
|
+
var TYPES = {
|
|
7092
|
+
".html": "text/html; charset=utf-8",
|
|
7093
|
+
".js": "text/javascript; charset=utf-8",
|
|
7094
|
+
".mjs": "text/javascript; charset=utf-8",
|
|
7095
|
+
".css": "text/css; charset=utf-8",
|
|
7096
|
+
".json": "application/json; charset=utf-8",
|
|
7097
|
+
".webmanifest": "application/manifest+json",
|
|
7098
|
+
".svg": "image/svg+xml",
|
|
7099
|
+
".png": "image/png",
|
|
7100
|
+
".jpg": "image/jpeg",
|
|
7101
|
+
".jpeg": "image/jpeg",
|
|
7102
|
+
".webp": "image/webp",
|
|
7103
|
+
".gif": "image/gif",
|
|
7104
|
+
".ico": "image/x-icon",
|
|
7105
|
+
".wasm": "application/wasm",
|
|
7106
|
+
".woff": "font/woff",
|
|
7107
|
+
".woff2": "font/woff2",
|
|
7108
|
+
".txt": "text/plain; charset=utf-8",
|
|
7109
|
+
".map": "application/json"
|
|
7110
|
+
};
|
|
7111
|
+
var HttpError = class extends Error {
|
|
7112
|
+
status;
|
|
7113
|
+
constructor(status, message) {
|
|
7114
|
+
super(message);
|
|
7115
|
+
this.status = status;
|
|
7116
|
+
}
|
|
7117
|
+
};
|
|
7118
|
+
function sendJson(res, status, body) {
|
|
7119
|
+
const text = JSON.stringify(body);
|
|
7120
|
+
res.writeHead(status, {
|
|
7121
|
+
"content-type": "application/json; charset=utf-8",
|
|
7122
|
+
"content-length": Buffer.byteLength(text),
|
|
7123
|
+
"cache-control": "no-store"
|
|
7124
|
+
});
|
|
7125
|
+
res.end(text);
|
|
7126
|
+
}
|
|
7127
|
+
async function readBody(req, limit) {
|
|
7128
|
+
const chunks = [];
|
|
7129
|
+
let size = 0;
|
|
7130
|
+
for await (const chunk of req) {
|
|
7131
|
+
size += chunk.length;
|
|
7132
|
+
if (size > limit) throw new HttpError(413, "The request body is too large.");
|
|
7133
|
+
chunks.push(chunk);
|
|
7134
|
+
}
|
|
7135
|
+
return Buffer.concat(chunks).toString("utf-8");
|
|
7136
|
+
}
|
|
7137
|
+
async function startKitchenServer(options) {
|
|
7138
|
+
const log = options.log ?? (() => {});
|
|
7139
|
+
const token = options.token ?? randomBytes(32).toString("base64url");
|
|
7140
|
+
const mode = options.ai ? "host" : "browser";
|
|
7141
|
+
const appRoot = normalize(options.planner.appDir);
|
|
7142
|
+
let port = 0;
|
|
7143
|
+
let applying = false;
|
|
7144
|
+
let applied = false;
|
|
7145
|
+
let resolveApplied;
|
|
7146
|
+
const appliedPromise = new Promise((resolve) => {
|
|
7147
|
+
resolveApplied = resolve;
|
|
7148
|
+
});
|
|
7149
|
+
const origins = () => [`http://127.0.0.1:${port}`, `http://localhost:${port}`];
|
|
7150
|
+
const config = () => ({
|
|
7151
|
+
inference: mode === "host" ? {
|
|
7152
|
+
mode: "host",
|
|
7153
|
+
host: { endpoint: CHAT_PATH }
|
|
7154
|
+
} : { mode: "browser" },
|
|
7155
|
+
kitchen: {
|
|
7156
|
+
endpoint: COOKBOOK_PATH,
|
|
7157
|
+
token
|
|
7158
|
+
}
|
|
7159
|
+
});
|
|
7160
|
+
function checkToken(supplied) {
|
|
7161
|
+
if (!supplied) return false;
|
|
7162
|
+
const a = Buffer.from(supplied);
|
|
7163
|
+
const b = Buffer.from(token);
|
|
7164
|
+
return a.length === b.length && timingSafeEqual(a, b);
|
|
7165
|
+
}
|
|
7166
|
+
/** A browser POST states its origin; it must be this server's own. */
|
|
7167
|
+
function checkOrigin(req, required) {
|
|
7168
|
+
const origin = req.headers.origin;
|
|
7169
|
+
if (origin === void 0) {
|
|
7170
|
+
if (required) throw new HttpError(403, "Same-origin requests only.");
|
|
7171
|
+
return;
|
|
7172
|
+
}
|
|
7173
|
+
if (!origins().includes(origin)) throw new HttpError(403, "Same-origin requests only.");
|
|
7174
|
+
}
|
|
7175
|
+
function requireJson(req) {
|
|
7176
|
+
if (!/^application\/json\b/i.test(req.headers["content-type"] ?? "")) throw new HttpError(415, "Send application/json.");
|
|
7177
|
+
}
|
|
7178
|
+
async function chat(req, res) {
|
|
7179
|
+
if (!options.ai) throw new HttpError(503, "No AI provider is configured.");
|
|
7180
|
+
checkOrigin(req, false);
|
|
7181
|
+
requireJson(req);
|
|
7182
|
+
let body;
|
|
7183
|
+
try {
|
|
7184
|
+
body = JSON.parse(await readBody(req, CHAT_LIMIT));
|
|
7185
|
+
} catch (error) {
|
|
7186
|
+
if (error instanceof HttpError) throw error;
|
|
7187
|
+
throw new HttpError(400, "The body is not valid JSON.");
|
|
7188
|
+
}
|
|
7189
|
+
const parsed = options.core.parseHostRequest(body);
|
|
7190
|
+
if (!parsed.ok) throw new HttpError(400, parsed.error);
|
|
7191
|
+
const { system, messages } = options.core.buildHostPrompt(parsed.request);
|
|
7192
|
+
let text;
|
|
7193
|
+
try {
|
|
7194
|
+
text = await options.ai.chat([{
|
|
7195
|
+
role: "system",
|
|
7196
|
+
content: system
|
|
7197
|
+
}, ...messages]);
|
|
7198
|
+
} catch (error) {
|
|
7199
|
+
log(`The model could not answer: ${error instanceof Error ? error.message : String(error)}`);
|
|
7200
|
+
throw new HttpError(502, "The model could not answer.");
|
|
7201
|
+
}
|
|
7202
|
+
const reply = options.core.parseHostReply(text);
|
|
7203
|
+
if (reply.issues.length) log(`planner reply: ${reply.issues.join("; ")}`);
|
|
7204
|
+
sendJson(res, 200, reply.reply);
|
|
7205
|
+
}
|
|
7206
|
+
async function cookbook(req, res) {
|
|
7207
|
+
checkOrigin(req, true);
|
|
7208
|
+
requireJson(req);
|
|
7209
|
+
if (applied || !checkToken(req.headers["x-kitchen-token"])) throw new HttpError(401, "Missing or wrong kitchen token.");
|
|
7210
|
+
if (applying) throw new HttpError(409, "A cookbook is already being applied.");
|
|
7211
|
+
applying = true;
|
|
7212
|
+
try {
|
|
7213
|
+
const text = await readBody(req, COOKBOOK_LIMIT);
|
|
7214
|
+
log("Received a cookbook from the planner.");
|
|
7215
|
+
const outcome = await options.apply(text);
|
|
7216
|
+
if (!outcome.ok) {
|
|
7217
|
+
log(`Not applied: ${outcome.errors.join("; ")}`);
|
|
7218
|
+
sendJson(res, outcome.status, {
|
|
7219
|
+
ok: false,
|
|
7220
|
+
errors: outcome.errors
|
|
7221
|
+
});
|
|
7222
|
+
return;
|
|
7223
|
+
}
|
|
7224
|
+
applied = true;
|
|
7225
|
+
res.once("close", () => resolveApplied(outcome.body));
|
|
7226
|
+
sendJson(res, 200, outcome.body);
|
|
7227
|
+
} finally {
|
|
7228
|
+
applying = false;
|
|
7229
|
+
}
|
|
7230
|
+
}
|
|
7231
|
+
function serveStatic(req, res, path) {
|
|
7232
|
+
let relative;
|
|
7233
|
+
try {
|
|
7234
|
+
relative = decodeURIComponent(path);
|
|
7235
|
+
} catch {
|
|
7236
|
+
throw new HttpError(400, "Bad path.");
|
|
7237
|
+
}
|
|
7238
|
+
if (relative.includes("\0")) throw new HttpError(400, "Bad path.");
|
|
7239
|
+
let file = normalize(join(appRoot, relative));
|
|
7240
|
+
if (file !== appRoot && !file.startsWith(appRoot + sep)) throw new HttpError(403, "Forbidden.");
|
|
7241
|
+
let stat = statSync(file, { throwIfNoEntry: false });
|
|
7242
|
+
if (stat?.isDirectory()) {
|
|
7243
|
+
file = join(file, "index.html");
|
|
7244
|
+
stat = statSync(file, { throwIfNoEntry: false });
|
|
7245
|
+
}
|
|
7246
|
+
if (!stat?.isFile()) throw new HttpError(404, "Not found.");
|
|
7247
|
+
res.writeHead(200, {
|
|
7248
|
+
"content-type": TYPES[extname(file).toLowerCase()] ?? "application/octet-stream",
|
|
7249
|
+
"content-length": stat.size,
|
|
7250
|
+
"cache-control": path.includes("/_app/immutable/") ? "public, max-age=31536000, immutable" : "no-cache"
|
|
7251
|
+
});
|
|
7252
|
+
if (req.method === "HEAD") {
|
|
7253
|
+
res.end();
|
|
7254
|
+
return;
|
|
7255
|
+
}
|
|
7256
|
+
createReadStream(file).pipe(res);
|
|
7257
|
+
}
|
|
7258
|
+
const server = createServer(async (req, res) => {
|
|
7259
|
+
res.setHeader("x-content-type-options", "nosniff");
|
|
7260
|
+
res.setHeader("referrer-policy", "no-referrer");
|
|
7261
|
+
try {
|
|
7262
|
+
if (!origins().some((o) => o.endsWith(`//${req.headers.host}`))) throw new HttpError(403, "Unexpected Host.");
|
|
7263
|
+
const path = (req.url ?? "/").split("?")[0];
|
|
7264
|
+
const get = req.method === "GET" || req.method === "HEAD";
|
|
7265
|
+
if (path === "/planner.config.json" && get) sendJson(res, 200, config());
|
|
7266
|
+
else if (path === "/api/planner/chat" && req.method === "POST") await chat(req, res);
|
|
7267
|
+
else if (path === "/api/kitchen/cookbook" && req.method === "POST") await cookbook(req, res);
|
|
7268
|
+
else if (get) serveStatic(req, res, path);
|
|
7269
|
+
else throw new HttpError(405, "Method not allowed.");
|
|
7270
|
+
} catch (error) {
|
|
7271
|
+
const status = error instanceof HttpError ? error.status : 500;
|
|
7272
|
+
const message = error instanceof HttpError ? error.message : "Server error.";
|
|
7273
|
+
if (!(error instanceof HttpError)) log(`server error: ${error instanceof Error ? error.message : String(error)}`);
|
|
7274
|
+
if (res.headersSent) res.destroy();
|
|
7275
|
+
else if (req.url?.startsWith("/api/")) sendJson(res, status, {
|
|
7276
|
+
ok: false,
|
|
7277
|
+
error: message,
|
|
7278
|
+
errors: [message]
|
|
7279
|
+
});
|
|
7280
|
+
else {
|
|
7281
|
+
res.writeHead(status, { "content-type": "text/plain; charset=utf-8" });
|
|
7282
|
+
res.end(message);
|
|
7283
|
+
}
|
|
7284
|
+
}
|
|
7285
|
+
});
|
|
7286
|
+
await new Promise((resolve, reject) => {
|
|
7287
|
+
server.once("error", reject);
|
|
7288
|
+
server.listen(options.port ?? 0, "127.0.0.1", () => resolve());
|
|
7289
|
+
});
|
|
7290
|
+
port = server.address().port;
|
|
7291
|
+
return {
|
|
7292
|
+
url: `http://127.0.0.1:${port}/`,
|
|
7293
|
+
port,
|
|
7294
|
+
token,
|
|
7295
|
+
mode,
|
|
7296
|
+
applied: appliedPromise,
|
|
7297
|
+
close: () => new Promise((resolve) => {
|
|
7298
|
+
server.close(() => resolve());
|
|
7299
|
+
server.closeAllConnections();
|
|
7300
|
+
})
|
|
7301
|
+
};
|
|
7302
|
+
}
|
|
7303
|
+
//#endregion
|
|
7304
|
+
//#region src/commands/kitchen/run.ts
|
|
7305
|
+
/**
|
|
7306
|
+
* `smrt kitchen [dir]` (#3750): find the planner, serve it on localhost, let
|
|
7307
|
+
* the visitor chat their way to a cookbook, apply what they send into `dir`.
|
|
7308
|
+
*/
|
|
7309
|
+
/** The target must be new (absent or empty) or an existing kitchen project. */
|
|
7310
|
+
function checkTargetDir(dir) {
|
|
7311
|
+
if (!existsSync(dir) || readdirSync(dir).length === 0) return;
|
|
7312
|
+
if (existsSync(join(dir, "smrt.cookbook.json")) && existsSync(join(dir, "package.json"))) return;
|
|
7313
|
+
throw new PlannerError(`${dir} is not empty and holds no ${COOKBOOK_CONFIG_FILE}; pick an empty directory (smrt kitchen <dir>)`);
|
|
7314
|
+
}
|
|
7315
|
+
function openInBrowser(url) {
|
|
7316
|
+
const [command, args] = process.platform === "darwin" ? ["open", [url]] : process.platform === "win32" ? ["cmd", [
|
|
7317
|
+
"/c",
|
|
7318
|
+
"start",
|
|
7319
|
+
"",
|
|
7320
|
+
url
|
|
7321
|
+
]] : ["xdg-open", [url]];
|
|
7322
|
+
execFile(command, args, { windowsHide: true }, () => {});
|
|
7323
|
+
}
|
|
7324
|
+
/** Run the kitchen until a cookbook was applied. Returns what was written. */
|
|
7325
|
+
async function runKitchen(options) {
|
|
7326
|
+
const log = options.log ?? console.log;
|
|
7327
|
+
const dir = resolve(options.dir);
|
|
7328
|
+
checkTargetDir(dir);
|
|
7329
|
+
const planner = options.overrides?.planner ?? await resolvePlanner({
|
|
7330
|
+
planner: options.planner,
|
|
7331
|
+
dir,
|
|
7332
|
+
log
|
|
7333
|
+
});
|
|
7334
|
+
const core = options.overrides?.core ?? await loadPlannerCore(planner);
|
|
7335
|
+
const ai = options.overrides && "ai" in options.overrides ? options.overrides.ai ?? null : await resolveKitchenAI({ dir });
|
|
7336
|
+
const server = await startKitchenServer({
|
|
7337
|
+
planner,
|
|
7338
|
+
core,
|
|
7339
|
+
ai,
|
|
7340
|
+
apply: options.overrides?.apply ?? createCookbookApplier({
|
|
7341
|
+
dir,
|
|
7342
|
+
template: options.template,
|
|
7343
|
+
install: options.install,
|
|
7344
|
+
manifests: options.manifests,
|
|
7345
|
+
registry: options.registry,
|
|
7346
|
+
log
|
|
7347
|
+
}),
|
|
7348
|
+
port: options.port,
|
|
7349
|
+
token: options.overrides?.token,
|
|
7350
|
+
log
|
|
7351
|
+
});
|
|
7352
|
+
try {
|
|
7353
|
+
log(`Planner ${planner.version ?? ""} (${planner.source}) at ${server.url}`.replace(" ", " "));
|
|
7354
|
+
log(ai ? `Chat is answered by ${ai.label}.` : "No AI provider is configured (set an `ai` block in smrt.config.ts, SMRT_AI_PROVIDER, or e.g. OPENAI_API_KEY), so the planner runs its in-browser model. You can also build the cookbook by hand.");
|
|
7355
|
+
log(`Project directory: ${dir}`);
|
|
7356
|
+
log("Build your app in the planner, then choose \"Send to kitchen\". Ctrl-C to stop.");
|
|
7357
|
+
options.onReady?.(server);
|
|
7358
|
+
if (options.open) (options.overrides?.openBrowser ?? openInBrowser)(server.url);
|
|
7359
|
+
return await (options.stop ? Promise.race([server.applied, options.stop]) : server.applied);
|
|
7360
|
+
} finally {
|
|
7361
|
+
await server.close();
|
|
7362
|
+
}
|
|
7363
|
+
}
|
|
7364
|
+
//#endregion
|
|
7365
|
+
//#region src/commands/kitchen.ts
|
|
7366
|
+
/**
|
|
7367
|
+
* `smrt kitchen [dir]` and `smrt kitchen apply <url|file> [dir]` (#3750).
|
|
7368
|
+
* See `agents/kitchen.md`.
|
|
7369
|
+
*/
|
|
7370
|
+
/** Ctrl-C and SIGTERM stop the kitchen cleanly (the server is closed in `finally`). */
|
|
7371
|
+
function stopOnSignal() {
|
|
7372
|
+
let dispose = () => {};
|
|
7373
|
+
const stop = new Promise((_, reject) => {
|
|
7374
|
+
const onSignal = (signal) => {
|
|
7375
|
+
reject(new KitchenStopped(signal));
|
|
7376
|
+
};
|
|
7377
|
+
process.once("SIGINT", onSignal);
|
|
7378
|
+
process.once("SIGTERM", onSignal);
|
|
7379
|
+
dispose = () => {
|
|
7380
|
+
process.off("SIGINT", onSignal);
|
|
7381
|
+
process.off("SIGTERM", onSignal);
|
|
7382
|
+
};
|
|
7383
|
+
});
|
|
7384
|
+
stop.catch(() => {});
|
|
7385
|
+
return {
|
|
7386
|
+
stop,
|
|
7387
|
+
dispose
|
|
7388
|
+
};
|
|
7389
|
+
}
|
|
7390
|
+
var KitchenStopped = class extends Error {
|
|
7391
|
+
signal;
|
|
7392
|
+
constructor(signal) {
|
|
7393
|
+
super(`stopped by ${signal}`);
|
|
7394
|
+
this.signal = signal;
|
|
7395
|
+
}
|
|
7396
|
+
};
|
|
7397
|
+
var kitchenCommands = {
|
|
7398
|
+
kitchen: {
|
|
7399
|
+
name: "kitchen",
|
|
7400
|
+
description: "Run the planner on localhost, chat your way to a cookbook, and apply it into [dir]",
|
|
7401
|
+
args: ["dir"],
|
|
7402
|
+
options: {
|
|
7403
|
+
planner: {
|
|
7404
|
+
type: "string",
|
|
7405
|
+
description: "Use a packaged planner checkout (run `pnpm package` there) instead of the cached registry copy"
|
|
7406
|
+
},
|
|
7407
|
+
template: {
|
|
7408
|
+
type: "string",
|
|
7409
|
+
description: "Project template: a local path, github:owner/repo[#ref], or a git URL (default github:happyvertical/smrt-start)"
|
|
7410
|
+
},
|
|
7411
|
+
"no-open": {
|
|
7412
|
+
type: "boolean",
|
|
7413
|
+
description: "Do not open the browser; print the address only",
|
|
7414
|
+
default: false
|
|
7415
|
+
},
|
|
7416
|
+
"no-install": {
|
|
7417
|
+
type: "boolean",
|
|
7418
|
+
description: "Skip the package install after the project is written",
|
|
7419
|
+
default: false
|
|
7420
|
+
},
|
|
7421
|
+
manifests: {
|
|
7422
|
+
type: "string",
|
|
7423
|
+
description: "Extra package manifests (file or directory); repeatable. Searched before the workspace, node_modules and the registry",
|
|
7424
|
+
multiple: true
|
|
7425
|
+
},
|
|
7426
|
+
"no-registry": {
|
|
7427
|
+
type: "boolean",
|
|
7428
|
+
description: "Do not fetch manifests of uninstalled packages from the registry",
|
|
7429
|
+
default: false
|
|
7430
|
+
},
|
|
7431
|
+
port: {
|
|
7432
|
+
type: "number",
|
|
7433
|
+
description: "Listen on this port (default: a random free port)"
|
|
7434
|
+
}
|
|
7435
|
+
},
|
|
7436
|
+
handler: async (args, options) => {
|
|
7437
|
+
const dir = resolve(args[0] ?? ".");
|
|
7438
|
+
const { stop, dispose } = stopOnSignal();
|
|
7439
|
+
try {
|
|
7440
|
+
const applied = await runKitchen({
|
|
7441
|
+
dir,
|
|
7442
|
+
planner: options.planner,
|
|
7443
|
+
template: options.template,
|
|
7444
|
+
install: !options["no-install"],
|
|
7445
|
+
open: !options["no-open"],
|
|
7446
|
+
manifests: asList(options.manifests),
|
|
7447
|
+
registry: !options["no-registry"],
|
|
7448
|
+
port: options.port === void 0 ? void 0 : Number(options.port),
|
|
7449
|
+
stop
|
|
7450
|
+
});
|
|
7451
|
+
console.log(`\nDone: ${applied.mode === "new" ? "created" : "updated"} the project in ${applied.dir}`);
|
|
7452
|
+
if (applied.added.length) console.log(` packages added: ${applied.added.join(", ")}`);
|
|
7453
|
+
console.log("\nNext steps:");
|
|
7454
|
+
for (const step of applied.nextSteps) console.log(` ${step}`);
|
|
7455
|
+
console.log(" pnpm dev");
|
|
7456
|
+
} catch (error) {
|
|
7457
|
+
if (error instanceof KitchenStopped) {
|
|
7458
|
+
console.log("\nKitchen closed.");
|
|
7459
|
+
process.exitCode = 130;
|
|
7460
|
+
return;
|
|
7461
|
+
}
|
|
7462
|
+
if (error instanceof PlannerError || error instanceof ApplyError) {
|
|
7463
|
+
console.error(error.message);
|
|
7464
|
+
process.exit(1);
|
|
7465
|
+
}
|
|
7466
|
+
throw error;
|
|
7467
|
+
} finally {
|
|
7468
|
+
dispose();
|
|
7469
|
+
}
|
|
7470
|
+
}
|
|
7471
|
+
},
|
|
7472
|
+
"kitchen mcp": {
|
|
7473
|
+
name: "kitchen mcp",
|
|
7474
|
+
description: "Serve the Kitchen planning tools over stdio MCP for a local agent plugin (ChatGPT, Codex, Claude); no model or credential is used",
|
|
7475
|
+
args: [],
|
|
7476
|
+
options: {
|
|
7477
|
+
template: {
|
|
7478
|
+
type: "string",
|
|
7479
|
+
description: "Default project template for apply_plan (default github:happyvertical/smrt-start)"
|
|
7480
|
+
},
|
|
7481
|
+
manifests: {
|
|
7482
|
+
type: "string",
|
|
7483
|
+
description: "Extra package manifests (file or directory); repeatable. Searched before the workspace, node_modules and the registry",
|
|
7484
|
+
multiple: true
|
|
7485
|
+
},
|
|
7486
|
+
"no-registry": {
|
|
7487
|
+
type: "boolean",
|
|
7488
|
+
description: "Do not fetch manifests of uninstalled packages from the registry",
|
|
7489
|
+
default: false
|
|
7490
|
+
},
|
|
7491
|
+
packages: {
|
|
7492
|
+
type: "string",
|
|
7493
|
+
description: "Extra smrt packages whose recipes to offer (name or @scope/name); repeatable",
|
|
7494
|
+
multiple: true
|
|
7495
|
+
},
|
|
7496
|
+
library: {
|
|
7497
|
+
type: "string",
|
|
7498
|
+
description: "Directory of extra <id>.cookbook.json starting points; repeatable",
|
|
7499
|
+
multiple: true
|
|
7500
|
+
}
|
|
7501
|
+
},
|
|
7502
|
+
handler: async (_args, options) => {
|
|
7503
|
+
const { runKitchenMcp } = await import("./server-Dp096tZj.js");
|
|
7504
|
+
await runKitchenMcp({
|
|
7505
|
+
dir: process.cwd(),
|
|
7506
|
+
template: options.template,
|
|
7507
|
+
manifests: asList(options.manifests),
|
|
7508
|
+
registry: !options["no-registry"],
|
|
7509
|
+
packages: asList(options.packages),
|
|
7510
|
+
library: asList(options.library)
|
|
7511
|
+
});
|
|
7512
|
+
}
|
|
7513
|
+
},
|
|
7514
|
+
"kitchen apply": {
|
|
7515
|
+
...cookbookCommands["cookbook apply"],
|
|
7516
|
+
name: "kitchen apply",
|
|
7517
|
+
description: "Apply a cookbook made elsewhere (file, cookbook URL or plan.s-m-r-t.dev plan URL) to a new project; same as `cookbook apply`"
|
|
7518
|
+
}
|
|
7519
|
+
};
|
|
7520
|
+
//#endregion
|
|
6771
7521
|
//#region src/commands/playground.ts
|
|
6772
7522
|
/**
|
|
6773
7523
|
* Playground Commands
|
|
@@ -7137,7 +7887,7 @@ var dbMaterializeTenantHierarchyCommand = {
|
|
|
7137
7887
|
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).");
|
|
7138
7888
|
const dbType = config.database.type || "sqlite";
|
|
7139
7889
|
const { getDatabase } = await import("@happyvertical/sql");
|
|
7140
|
-
const { materializeTenantHierarchy, TenantHierarchyMaterializationError } = await import("./dist-
|
|
7890
|
+
const { materializeTenantHierarchy, TenantHierarchyMaterializationError } = await import("./dist-BsRSQ_hE.js");
|
|
7141
7891
|
db = await getDatabase({
|
|
7142
7892
|
type: dbType,
|
|
7143
7893
|
url: config.database.url
|
|
@@ -7346,7 +8096,7 @@ var dbMigrateLedgerAccountsCommand = {
|
|
|
7346
8096
|
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.");
|
|
7347
8097
|
await autoDiscoverAndLoad();
|
|
7348
8098
|
const { getDatabase } = await import("@happyvertical/sql");
|
|
7349
|
-
const { migrateLedgerAccountsTable, planLedgerAccountsTableMove } = await import("./dist-
|
|
8099
|
+
const { migrateLedgerAccountsTable, planLedgerAccountsTableMove } = await import("./dist-CYVIKmvy.js");
|
|
7350
8100
|
db = await getDatabase({
|
|
7351
8101
|
type: dbType,
|
|
7352
8102
|
url: config.database.url
|
|
@@ -11579,4 +12329,4 @@ var workbenchCommands = { "workbench:dev": {
|
|
|
11579
12329
|
}
|
|
11580
12330
|
} };
|
|
11581
12331
|
//#endregion
|
|
11582
|
-
export { dispatchCommands, docsCommands, generateCommands, gitCommands, gnodeCommands, initCommands, playgroundCommands, utilityCommands, workbenchCommands };
|
|
12332
|
+
export { cookbookCommands, dispatchCommands, docsCommands, generateCommands, gitCommands, gnodeCommands, initCommands, kitchenCommands, playgroundCommands, utilityCommands, workbenchCommands };
|