busa-cli 0.50.0 → 0.51.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/dist/cli.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { i as runCli, r as pkgVersion, t as checkForUpdate } from "./update-check-
|
|
2
|
+
import { i as runCli, r as pkgVersion, t as checkForUpdate } from "./update-check-BWyS6qOd.js";
|
|
3
3
|
//#region src/cli.ts
|
|
4
4
|
const exitCode = await runCli(process.argv.slice(2));
|
|
5
5
|
const notice = await checkForUpdate("busabase-cli", pkgVersion());
|
package/dist/index.js
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
import { a as render, i as runCli, n as HELP, r as pkgVersion, t as checkForUpdate } from "./update-check-
|
|
1
|
+
import { a as render, i as runCli, n as HELP, r as pkgVersion, t as checkForUpdate } from "./update-check-BWyS6qOd.js";
|
|
2
2
|
import { DEFAULT_BASE_URL, createBusabaseClient, normalizeBaseUrl } from "busabase-sdk";
|
|
3
3
|
export { DEFAULT_BASE_URL, HELP, checkForUpdate, createBusabaseClient, normalizeBaseUrl, pkgVersion, render, runCli };
|
|
@@ -103,6 +103,21 @@ const LocaleSchema = z.enum({
|
|
|
103
103
|
*/
|
|
104
104
|
const iStringRecordSchema = z.partialRecord(LocaleSchema, z.string());
|
|
105
105
|
const iStringSchema = z.union([z.string(), iStringRecordSchema]).describe("i18n string");
|
|
106
|
+
/**
|
|
107
|
+
* Quick parse to string
|
|
108
|
+
*
|
|
109
|
+
* @param str
|
|
110
|
+
* @param lang default en
|
|
111
|
+
* @returns
|
|
112
|
+
*/
|
|
113
|
+
const iStringParse = (str, lang = "en") => {
|
|
114
|
+
if (!str) return "";
|
|
115
|
+
if (typeof str === "string") return str;
|
|
116
|
+
const locales = Object.keys(str || {});
|
|
117
|
+
const result = str[locales.find((locale) => locale === lang) || locales.find((locale) => locale !== "en") || "en"];
|
|
118
|
+
if (result === void 0) console.warn(`iStringParse: ${JSON.stringify(str)} has no '${lang}' or other locales`);
|
|
119
|
+
return result || "";
|
|
120
|
+
};
|
|
106
121
|
//#endregion
|
|
107
122
|
//#region ../../packages/busabase-contract/src/contract/node-agent-prompt-schemas.ts
|
|
108
123
|
/**
|
|
@@ -5730,208 +5745,6 @@ z.object({
|
|
|
5730
5745
|
[TEMPLATE_SKILL_METADATA_KEY]: z.literal(true)
|
|
5731
5746
|
});
|
|
5732
5747
|
//#endregion
|
|
5733
|
-
//#region ../../packages/busabase-package/src/frontmatter.ts
|
|
5734
|
-
/**
|
|
5735
|
-
* YAML frontmatter for doc nodes (§6.3): `name`, `description?`, `position?` above
|
|
5736
|
-
* the markdown body.
|
|
5737
|
-
*
|
|
5738
|
-
* Writing is deterministic (§6.6): a fixed key order and LF endings, so re-exporting
|
|
5739
|
-
* an unchanged doc is byte-identical.
|
|
5740
|
-
*/
|
|
5741
|
-
const DELIMITER = "---";
|
|
5742
|
-
/**
|
|
5743
|
-
* The canonical on-disk body: LF endings, no leading or trailing blank lines. Applied
|
|
5744
|
-
* on both read and write so the round trip is a fixed point — without it, the blank
|
|
5745
|
-
* line the writer puts after the closing `---` and the file's trailing newline would
|
|
5746
|
-
* accumulate into the body on every export→install→export cycle.
|
|
5747
|
-
*
|
|
5748
|
-
* Leading/trailing blank lines around a doc body are therefore normalized away on the
|
|
5749
|
-
* first export; internal blank lines are untouched.
|
|
5750
|
-
*/
|
|
5751
|
-
const normalizeDocBody = (body) => body.replaceAll("\r\n", "\n").replace(/^\n+/, "").replace(/\n+$/, "");
|
|
5752
|
-
/** Split `---\n<yaml>\n---\n<body>`. A doc with no frontmatter is all body. */
|
|
5753
|
-
const parseFrontmatter = (text, filePath) => {
|
|
5754
|
-
const normalized = text.replaceAll("\r\n", "\n");
|
|
5755
|
-
if (!normalized.startsWith(`${DELIMITER}\n`)) return {
|
|
5756
|
-
data: {},
|
|
5757
|
-
body: normalizeDocBody(normalized)
|
|
5758
|
-
};
|
|
5759
|
-
const end = normalized.indexOf(`\n${DELIMITER}`, 3);
|
|
5760
|
-
if (end === -1) throw new Error(`${filePath} opens YAML frontmatter with "---" but never closes it. Add a closing "---" line.`);
|
|
5761
|
-
const rawYaml = normalized.slice(4, end);
|
|
5762
|
-
const body = normalizeDocBody(normalized.slice(end + 1 + 3));
|
|
5763
|
-
let data;
|
|
5764
|
-
try {
|
|
5765
|
-
data = parse(rawYaml);
|
|
5766
|
-
} catch (error) {
|
|
5767
|
-
throw new Error(`${filePath} has invalid YAML frontmatter: ${error.message}`);
|
|
5768
|
-
}
|
|
5769
|
-
if (data === null || data === void 0) return {
|
|
5770
|
-
data: {},
|
|
5771
|
-
body
|
|
5772
|
-
};
|
|
5773
|
-
if (typeof data !== "object" || Array.isArray(data)) throw new Error(`${filePath} frontmatter must be a YAML mapping (key: value pairs).`);
|
|
5774
|
-
return {
|
|
5775
|
-
data,
|
|
5776
|
-
body
|
|
5777
|
-
};
|
|
5778
|
-
};
|
|
5779
|
-
/** Serialize a doc to `---\n<frontmatter>\n---\n<body>` with a trailing newline. */
|
|
5780
|
-
const serializeDoc = (fields, body) => {
|
|
5781
|
-
const data = { name: fields.name };
|
|
5782
|
-
if (fields.description) data.description = fields.description;
|
|
5783
|
-
if (fields.position !== void 0) data.position = fields.position;
|
|
5784
|
-
const yaml = stringify(data, { lineWidth: 0 }).replace(/\n$/, "");
|
|
5785
|
-
return `${DELIMITER}\n${yaml}\n${DELIMITER}\n\n${normalizeDocBody(body)}\n`;
|
|
5786
|
-
};
|
|
5787
|
-
//#endregion
|
|
5788
|
-
//#region ../../packages/busabase-package/src/audit.ts
|
|
5789
|
-
/**
|
|
5790
|
-
* Is this package **fit to publish**?
|
|
5791
|
-
*
|
|
5792
|
-
* Deliberately a different question from {@link validateTemplate}, and the split is
|
|
5793
|
-
* the point rather than an organisational preference:
|
|
5794
|
-
*
|
|
5795
|
-
* - `validateTemplate` **classifies**. Its verdict changes what install *does* — a
|
|
5796
|
-
* Skill node, ownership stamps and a sample merge, versus a plain package install.
|
|
5797
|
-
* Four consumers must agree on it or the catalog lies about what an install does,
|
|
5798
|
-
* so it stays small and stable.
|
|
5799
|
-
* - `auditPackage` / `auditSkill` **advise**. Nothing behaves differently based on
|
|
5800
|
-
* their verdict; they exist for the author and the reviewer, and are free to grow.
|
|
5801
|
-
*
|
|
5802
|
-
* The practical consequence: a declared screenshot that is not on disk belongs here,
|
|
5803
|
-
* never in `validateTemplate`. Promoting it there would stop a template installing
|
|
5804
|
-
* over a missing image, which is the wrong lever entirely.
|
|
5805
|
-
*
|
|
5806
|
-
* Nothing here re-checks what `validateTemplate` already decides (declared resources,
|
|
5807
|
-
* the sample-row ceiling, name agreement between manifest and manual, an unambiguous
|
|
5808
|
-
* primary AirApp). Run both; they compose.
|
|
5809
|
-
*
|
|
5810
|
-
* Purely static: no install, no network, and none of the package's own code is
|
|
5811
|
-
* executed — safe to run in CI over a pull request from someone you do not know.
|
|
5812
|
-
*/
|
|
5813
|
-
/**
|
|
5814
|
-
* A materialized workspace id: busabase-core's `id()` is a three-letter prefix, the
|
|
5815
|
-
* creation time in base36, then seven random characters.
|
|
5816
|
-
*
|
|
5817
|
-
* The lookahead requiring a digit is what keeps ordinary words out — `recommendation`
|
|
5818
|
-
* has the right prefix and the wrong shape. A real id always carries its timestamp.
|
|
5819
|
-
*/
|
|
5820
|
-
const WORKSPACE_ID = /(?<![0-9a-z])(?:nod|bse|viw|rec|crq|ast|com|fom|shr|fvl|opr|cmt|rev|whd|aud)(?=[0-9a-z]{15}(?![0-9a-z]))(?=[0-9a-z]*\d)[0-9a-z]{15}/;
|
|
5821
|
-
const TEXT_EXTENSIONS = /\.(?:js|mjs|cjs|ts|mts|cts|json|ndjson|md|html|css|ya?ml|txt)$|(?:^|\/)\.env(?:\.|$)/;
|
|
5822
|
-
/**
|
|
5823
|
-
* Files that exist to *forbid* a string necessarily contain it. An AirApp's own
|
|
5824
|
-
* `scripts/check.mjs` asserts the obsolete API prefix is absent, and a test fixture
|
|
5825
|
-
* spells out the credential it proves gets rejected — scanning those turns a
|
|
5826
|
-
* correctly-guarded package into a failure.
|
|
5827
|
-
*/
|
|
5828
|
-
const IS_ASSERTION_CODE = /(?:^|\/)(?:scripts\/check\.mjs|tests?\/|[^/]*\.test\.[cm]?[jt]s)$/;
|
|
5829
|
-
/** Lockfiles carry integrity hashes that are not ours to interpret. */
|
|
5830
|
-
const NOT_SOURCE = /(?:^|\/)(?:package-lock\.json|pnpm-lock\.yaml|yarn\.lock|bun\.lockb)$/;
|
|
5831
|
-
const relativePaths = (files, root = "") => {
|
|
5832
|
-
const prefix = root ? `${root.replace(/\/+$/, "")}/` : "";
|
|
5833
|
-
const out = [];
|
|
5834
|
-
for (const key of files.keys()) if (!prefix || key.startsWith(prefix)) out.push(key.slice(prefix.length));
|
|
5835
|
-
return out;
|
|
5836
|
-
};
|
|
5837
|
-
const readText = (files, root, relative) => {
|
|
5838
|
-
const prefix = root ? `${root.replace(/\/+$/, "")}/` : "";
|
|
5839
|
-
return files.get(`${prefix}${relative}`)?.toString("utf8");
|
|
5840
|
-
};
|
|
5841
|
-
/**
|
|
5842
|
-
* Audit the package layer: identity, the assets the catalog promises, and anything
|
|
5843
|
-
* that must never be published.
|
|
5844
|
-
*
|
|
5845
|
-
* `files` is the flat map `readPackageTree` was given, not the tree. That is load-bearing:
|
|
5846
|
-
* `assets/screenshots/` holds no nodes and so never reaches the tree, and "the manifest
|
|
5847
|
-
* promises a screenshot that does not exist" is exactly a file-level question.
|
|
5848
|
-
*/
|
|
5849
|
-
const auditPackage = (tree, files, options = {}) => {
|
|
5850
|
-
const findings = [];
|
|
5851
|
-
const error = (rule, message) => findings.push({
|
|
5852
|
-
severity: "error",
|
|
5853
|
-
rule,
|
|
5854
|
-
message
|
|
5855
|
-
});
|
|
5856
|
-
const warn = (rule, message) => findings.push({
|
|
5857
|
-
severity: "warning",
|
|
5858
|
-
rule,
|
|
5859
|
-
message
|
|
5860
|
-
});
|
|
5861
|
-
const root = options.root ?? "";
|
|
5862
|
-
const paths = relativePaths(files, root);
|
|
5863
|
-
const present = new Set(paths);
|
|
5864
|
-
if (options.directoryName !== void 0 && options.directoryName !== tree.manifest.name) error("package/identity", `Directory is "${options.directoryName}" but busabase.json declares "${tree.manifest.name}". The directory name is the package's identity, not a label beside it.`);
|
|
5865
|
-
for (const screenshot of tree.manifest.template?.screenshots ?? []) if (!present.has(screenshot)) error("package/screenshot-missing", `busabase.json declares screenshot "${screenshot}", which is not in the package. The Template Center card would render a broken image.`);
|
|
5866
|
-
for (const relative of paths) {
|
|
5867
|
-
const base = relative.slice(relative.lastIndexOf("/") + 1);
|
|
5868
|
-
if (/^\.env(?!\.example$)(?:\.|$)/.test(base)) error("package/dotenv", `${relative} is part of the package. Credentials belong in Vault, never in something people install.`);
|
|
5869
|
-
if (/^(?:id_rsa|id_ed25519)$/.test(base) || /\.(?:pem|p12|pfx|keystore)$/.test(base)) error("package/key-material", `${relative} looks like key material and must not be published.`);
|
|
5870
|
-
if (/(?:^|\/)(?:node_modules|dist|build)\//.test(relative)) warn("package/build-output", `${relative} looks like installed or built output. Ship source; the installer builds nothing.`);
|
|
5871
|
-
}
|
|
5872
|
-
for (const relative of paths) {
|
|
5873
|
-
if (!TEXT_EXTENSIONS.test(relative)) continue;
|
|
5874
|
-
if (NOT_SOURCE.test(relative) || IS_ASSERTION_CODE.test(relative)) continue;
|
|
5875
|
-
const source = readText(files, root, relative);
|
|
5876
|
-
if (source === void 0) continue;
|
|
5877
|
-
const found = WORKSPACE_ID.exec(source);
|
|
5878
|
-
if (found) error("package/workspace-ids", `${relative} contains "${found[0]}", a materialized workspace id. Node, Base and Space ids are whoever last ran setup's runtime state — publishing them ships one person's workspace layout to everyone.`);
|
|
5879
|
-
}
|
|
5880
|
-
return findings;
|
|
5881
|
-
};
|
|
5882
|
-
/**
|
|
5883
|
-
* Audit the Skill layer: is the manual one an agent can actually act on?
|
|
5884
|
-
*
|
|
5885
|
-
* Runs for any directory carrying a root `SKILL.md` — package or not, template or not.
|
|
5886
|
-
* It reports nothing at all when there is no manual: a package without one is a
|
|
5887
|
-
* perfectly legitimate thing to publish, not a Skill that failed.
|
|
5888
|
-
*
|
|
5889
|
-
* The honest limit: a checker can tell you the frontmatter parses and the files the
|
|
5890
|
-
* manual points at exist. It cannot tell you the manual is *true*. Everything in
|
|
5891
|
-
* `SKILL.md` becomes instructions an agent acts on, and no static rule catches a
|
|
5892
|
-
* confident, wrong sentence about what a table means.
|
|
5893
|
-
*/
|
|
5894
|
-
const auditSkill = (files, options = {}) => {
|
|
5895
|
-
const findings = [];
|
|
5896
|
-
const error = (rule, message) => findings.push({
|
|
5897
|
-
severity: "error",
|
|
5898
|
-
rule,
|
|
5899
|
-
message
|
|
5900
|
-
});
|
|
5901
|
-
const warn = (rule, message) => findings.push({
|
|
5902
|
-
severity: "warning",
|
|
5903
|
-
rule,
|
|
5904
|
-
message
|
|
5905
|
-
});
|
|
5906
|
-
const root = options.root ?? "";
|
|
5907
|
-
const source = readText(files, root, PACKAGE_SKILL_ENTRY);
|
|
5908
|
-
if (source === void 0) return findings;
|
|
5909
|
-
let body = "";
|
|
5910
|
-
try {
|
|
5911
|
-
const parsed = parseFrontmatter(source, PACKAGE_SKILL_ENTRY);
|
|
5912
|
-
body = parsed.body;
|
|
5913
|
-
const frontmatter = SkillFrontmatterSchema$1.safeParse(parsed.data);
|
|
5914
|
-
if (!frontmatter.success) error("skill/frontmatter", `${PACKAGE_SKILL_ENTRY} frontmatter is not a valid Skill declaration: ${frontmatter.error.issues.map((issue) => `${issue.path.join(".") || "(root)"} ${issue.message}`).join("; ")}`);
|
|
5915
|
-
else if (frontmatter.data.description.trim() === "") error("skill/description", `${PACKAGE_SKILL_ENTRY} has no description. An agent chooses whether to reach for a Skill by reading it, so an empty one is not cosmetic — it is a Skill that never gets picked.`);
|
|
5916
|
-
} catch (cause) {
|
|
5917
|
-
error("skill/frontmatter", `${PACKAGE_SKILL_ENTRY}: ${cause.message}`);
|
|
5918
|
-
return findings;
|
|
5919
|
-
}
|
|
5920
|
-
const todos = (body.match(/^.*\bTODO\b.*$/gm) ?? []).length;
|
|
5921
|
-
if (todos > 0) error("skill/todo", `${PACKAGE_SKILL_ENTRY} still carries ${todos} TODO line(s). The export draft leaves them on purpose — an agent acts on what this file says, and a blank the author was meant to fill in is worse than nothing once it ships.`);
|
|
5922
|
-
const present = new Set(relativePaths(files, root));
|
|
5923
|
-
const sidecar = new RegExp(`\\]\\((\\.?/?(?:${PACKAGE_SIDECARS})/[^)\\s]+)\\)|\`((?:${PACKAGE_SIDECARS})/[^\`\\s]+)\``, "g");
|
|
5924
|
-
const seen = /* @__PURE__ */ new Set();
|
|
5925
|
-
for (const match of body.matchAll(sidecar)) {
|
|
5926
|
-
const target = (match[1] ?? match[2]).replace(/^\.?\//, "");
|
|
5927
|
-
if (seen.has(target)) continue;
|
|
5928
|
-
seen.add(target);
|
|
5929
|
-
if (!present.has(target)) warn("skill/missing-reference", `${PACKAGE_SKILL_ENTRY} points at "${target}", which the package does not ship. An agent told to read it has an instruction it cannot satisfy.`);
|
|
5930
|
-
}
|
|
5931
|
-
return findings;
|
|
5932
|
-
};
|
|
5933
|
-
const PACKAGE_SIDECARS = PACKAGE_SKILL_SIDECAR_DIRS.join("|");
|
|
5934
|
-
//#endregion
|
|
5935
5748
|
//#region ../../packages/busabase-contract/src/contract/auto-merge.ts
|
|
5936
5749
|
/**
|
|
5937
5750
|
* `autoMerge` on a destructive-but-reversible operation.
|
|
@@ -7160,9 +6973,27 @@ const PACKAGE_MAX_FILE_COUNT = 5e3;
|
|
|
7160
6973
|
const PACKAGE_MAX_RECORDS_PER_BASE = 5e4;
|
|
7161
6974
|
const PackageManifestSchema = z.object({
|
|
7162
6975
|
format: z.literal(PACKAGE_FORMAT),
|
|
7163
|
-
/**
|
|
6976
|
+
/**
|
|
6977
|
+
* Default `--into-folder` name. Not the repo name — a repo can host many
|
|
6978
|
+
* packages. This is the package's IDENTITY, not a label: it must equal the
|
|
6979
|
+
* on-disk directory name and the SKILL.md frontmatter name (enforced by
|
|
6980
|
+
* `audit.ts`'s "one identity" rule), and it is used as a slug — CLI sort,
|
|
6981
|
+
* `suggestSlug()`, the Dashboard's `/templates/:name` route, and the
|
|
6982
|
+
* install-target folder name all key off it verbatim. Deliberately NOT an
|
|
6983
|
+
* iString: none of those call sites expect a locale-keyed object, and
|
|
6984
|
+
* "the directory name is the package's identity, not a label beside it"
|
|
6985
|
+
* (audit.ts) is exactly the argument against ever making it one. A
|
|
6986
|
+
* human-facing translated title belongs in `description` below, not here.
|
|
6987
|
+
*/
|
|
7164
6988
|
name: z.string().min(1),
|
|
7165
|
-
|
|
6989
|
+
/**
|
|
6990
|
+
* The one-line blurb shown on a template's catalog card, and (via
|
|
6991
|
+
* `layout-read.ts`) the description on the Folder node created at install
|
|
6992
|
+
* time — Folder/node descriptions are already iString-capable text
|
|
6993
|
+
* columns, so this single field legitimately serves both. Plain string
|
|
6994
|
+
* stays valid forever; nothing that already writes a bare string breaks.
|
|
6995
|
+
*/
|
|
6996
|
+
description: iStringSchema.default(""),
|
|
7166
6997
|
/**
|
|
7167
6998
|
* Informational / reserved for a future marketplace index. The real version pin
|
|
7168
6999
|
* is always the git ref in the install URL.
|
|
@@ -7181,19 +7012,40 @@ const PackageManifestSchema = z.object({
|
|
|
7181
7012
|
*/
|
|
7182
7013
|
template: TemplateManifestSchema.optional()
|
|
7183
7014
|
});
|
|
7015
|
+
/**
|
|
7016
|
+
* Scenario Agent prompts for the node this sidecar describes — what a person can
|
|
7017
|
+
* ask an agent to DO with it, carried so it survives export → install.
|
|
7018
|
+
*
|
|
7019
|
+
* Same schema the node's own column and `busabase-cli nodes set-agent-prompts`
|
|
7020
|
+
* validate against (50 per node, 80 chars per label, 8 KiB per body per locale),
|
|
7021
|
+
* so a package cannot smuggle in a list the API would reject.
|
|
7022
|
+
*
|
|
7023
|
+
* Optional and absent by default: a package written before this field, or one
|
|
7024
|
+
* whose author wrote no prompts, is unchanged on disk and installs exactly as
|
|
7025
|
+
* before — the nodes then show their node type's default prompts, which is the
|
|
7026
|
+
* behaviour every package had until now.
|
|
7027
|
+
*
|
|
7028
|
+
* Distinct from `busabase.json`'s template-level `agentPrompts`: that one is a
|
|
7029
|
+
* flat list of strings for the Template Center card and the install-time "Ask
|
|
7030
|
+
* agent" action, about the template as a whole. This one is per node, structured,
|
|
7031
|
+
* and becomes that node's prompts in the dialog after install.
|
|
7032
|
+
*/
|
|
7033
|
+
const packageNodeAgentPromptsSchema = customAgentPromptsSchema.optional();
|
|
7184
7034
|
/** `_node.json` — marks a directory as a verbatim-content file-tree node. */
|
|
7185
7035
|
const PackageFileTreeNodeMetaSchema = z.object({
|
|
7186
7036
|
type: z.enum(PACKAGE_FILE_TREE_NODE_TYPES),
|
|
7187
7037
|
name: z.string().min(1),
|
|
7188
7038
|
description: z.string().default(""),
|
|
7189
|
-
position: z.number().int().optional()
|
|
7039
|
+
position: z.number().int().optional(),
|
|
7040
|
+
agentPrompts: packageNodeAgentPromptsSchema
|
|
7190
7041
|
});
|
|
7191
7042
|
/** `_folder.json` — optional; also how an empty folder survives git. */
|
|
7192
7043
|
const PackageFolderMetaSchema = z.object({
|
|
7193
7044
|
/** Defaults to the humanized slug when absent. */
|
|
7194
7045
|
name: z.string().optional(),
|
|
7195
7046
|
description: z.string().default(""),
|
|
7196
|
-
position: z.number().int().optional()
|
|
7047
|
+
position: z.number().int().optional(),
|
|
7048
|
+
agentPrompts: packageNodeAgentPromptsSchema
|
|
7197
7049
|
});
|
|
7198
7050
|
/** `<filename>.node.json` — optional display metadata for a `file` node. */
|
|
7199
7051
|
const PackageFileNodeMetaSchema = PackageFolderMetaSchema;
|
|
@@ -7218,7 +7070,8 @@ const PackageDocAssetMetaSchema = z.object({
|
|
|
7218
7070
|
const PackageDocFrontmatterSchema = z.object({
|
|
7219
7071
|
name: z.string().min(1),
|
|
7220
7072
|
description: z.string().default(""),
|
|
7221
|
-
position: z.number().int().optional()
|
|
7073
|
+
position: z.number().int().optional(),
|
|
7074
|
+
agentPrompts: packageNodeAgentPromptsSchema
|
|
7222
7075
|
});
|
|
7223
7076
|
/**
|
|
7224
7077
|
* Field options as carried by the package. Mirrors the API's `fieldOptionsSchema`
|
|
@@ -7338,7 +7191,8 @@ const PackageBaseSchema = z.object({
|
|
|
7338
7191
|
requiredApprovals: z.number()
|
|
7339
7192
|
}).optional(),
|
|
7340
7193
|
fields: z.array(PackageBaseFieldSchema).default([]),
|
|
7341
|
-
views: z.array(PackageViewSchema).default([])
|
|
7194
|
+
views: z.array(PackageViewSchema).default([]),
|
|
7195
|
+
agentPrompts: packageNodeAgentPromptsSchema
|
|
7342
7196
|
});
|
|
7343
7197
|
/**
|
|
7344
7198
|
* One record per line. `key` is a package-local stable identifier (export uses the
|
|
@@ -7354,84 +7208,30 @@ const PackageRecordLineSchema = z.object({
|
|
|
7354
7208
|
fields: z.record(z.string(), z.unknown()).default({})
|
|
7355
7209
|
});
|
|
7356
7210
|
//#endregion
|
|
7357
|
-
//#region ../../packages/busabase-package/src/
|
|
7358
|
-
const escapeLiteral = (value) => value.replace(/[.+^${}()|[\]\\]/g, "\\$&");
|
|
7211
|
+
//#region ../../packages/busabase-package/src/tree.ts
|
|
7359
7212
|
/**
|
|
7360
|
-
*
|
|
7213
|
+
* The in-memory `busabase-package@1` tree — the shape both `layout-read` (repo dir
|
|
7214
|
+
* → tree) and `layout-write` (space subtree → tree → repo dir) speak, and that
|
|
7215
|
+
* `plan`/`apply` consume. Plus the format's shared validation rules (§6.3).
|
|
7216
|
+
*/
|
|
7217
|
+
/**
|
|
7218
|
+
* A node's scenario prompts in a fixed key order — determinism (§6.6).
|
|
7361
7219
|
*
|
|
7362
|
-
*
|
|
7363
|
-
*
|
|
7364
|
-
*
|
|
7365
|
-
|
|
7366
|
-
|
|
7367
|
-
|
|
7368
|
-
|
|
7369
|
-
|
|
7370
|
-
if (char === "*") {
|
|
7371
|
-
if (pattern[index + 1] === "*") {
|
|
7372
|
-
index += 1;
|
|
7373
|
-
if (pattern[index + 1] === "/") {
|
|
7374
|
-
index += 1;
|
|
7375
|
-
source += "(?:.*/)?";
|
|
7376
|
-
} else source += ".*";
|
|
7377
|
-
} else source += "[^/]*";
|
|
7378
|
-
continue;
|
|
7379
|
-
}
|
|
7380
|
-
if (char === "?") {
|
|
7381
|
-
source += "[^/]";
|
|
7382
|
-
continue;
|
|
7383
|
-
}
|
|
7384
|
-
if (char === "/") {
|
|
7385
|
-
source += "/";
|
|
7386
|
-
continue;
|
|
7387
|
-
}
|
|
7388
|
-
source += escapeLiteral(char);
|
|
7389
|
-
}
|
|
7390
|
-
return new RegExp(`${anchored ? "^" : "^(?:.*/)?"}${source}(?:/.*)?$`);
|
|
7391
|
-
};
|
|
7392
|
-
const parseRule = (line) => {
|
|
7393
|
-
let pattern = line.trim();
|
|
7394
|
-
if (!pattern || pattern.startsWith("#")) return void 0;
|
|
7395
|
-
const negated = pattern.startsWith("!");
|
|
7396
|
-
if (negated) pattern = pattern.slice(1);
|
|
7397
|
-
const directoryOnly = pattern.endsWith("/");
|
|
7398
|
-
if (directoryOnly) pattern = pattern.slice(0, -1);
|
|
7399
|
-
const anchored = pattern.startsWith("/");
|
|
7400
|
-
if (anchored) pattern = pattern.slice(1);
|
|
7401
|
-
if (!pattern) return void 0;
|
|
7402
|
-
return {
|
|
7403
|
-
regex: patternToRegex(pattern, anchored),
|
|
7404
|
-
negated,
|
|
7405
|
-
directoryOnly
|
|
7406
|
-
};
|
|
7407
|
-
};
|
|
7408
|
-
const parseIgnoreFile = (contents) => {
|
|
7409
|
-
const rules = contents.split(/\r?\n/).map(parseRule).filter((rule) => rule !== void 0);
|
|
7410
|
-
return {
|
|
7411
|
-
isEmpty: rules.length === 0,
|
|
7412
|
-
ignores: (relativePath) => {
|
|
7413
|
-
let ignored = false;
|
|
7414
|
-
for (const rule of rules) {
|
|
7415
|
-
if (rule.directoryOnly && !rule.regex.test(`${relativePath}/`)) continue;
|
|
7416
|
-
if (!rule.regex.test(relativePath)) continue;
|
|
7417
|
-
ignored = !rule.negated;
|
|
7418
|
-
}
|
|
7419
|
-
return ignored;
|
|
7420
|
-
}
|
|
7421
|
-
};
|
|
7422
|
-
};
|
|
7423
|
-
/** A matcher that installs everything — used when no ignore file is present. */
|
|
7424
|
-
const IGNORE_NOTHING = {
|
|
7425
|
-
ignores: () => false,
|
|
7426
|
-
isEmpty: true
|
|
7427
|
-
};
|
|
7428
|
-
//#endregion
|
|
7429
|
-
//#region ../../packages/busabase-package/src/tree.ts
|
|
7430
|
-
/**
|
|
7431
|
-
* The in-memory `busabase-package@1` tree — the shape both `layout-read` (repo dir
|
|
7432
|
-
* → tree) and `layout-write` (space subtree → tree → repo dir) speak, and that
|
|
7433
|
-
* `plan`/`apply` consume. Plus the format's shared validation rules (§6.3).
|
|
7220
|
+
* Written through instead of spread verbatim because the list reaches the writer
|
|
7221
|
+
* from two directions: hand-authored on disk (whatever order the author typed)
|
|
7222
|
+
* and via a zod parse (schema order). Without this, re-exporting an unchanged
|
|
7223
|
+
* package would diff on key order alone.
|
|
7224
|
+
*
|
|
7225
|
+
* `intent` is omitted when absent rather than defaulted to `"change"` here — the
|
|
7226
|
+
* schema owns that default, and writing it out would silently rewrite every
|
|
7227
|
+
* author's file on the first round trip.
|
|
7434
7228
|
*/
|
|
7229
|
+
const serializeAgentPrompts = (prompts) => prompts?.length ? prompts.map((prompt) => ({
|
|
7230
|
+
key: prompt.key,
|
|
7231
|
+
...prompt.intent ? { intent: prompt.intent } : {},
|
|
7232
|
+
label: prompt.label,
|
|
7233
|
+
body: prompt.body
|
|
7234
|
+
})) : void 0;
|
|
7435
7235
|
/**
|
|
7436
7236
|
* Every creatable node type's slug is `/^[a-z0-9-]+$/` across the whole API
|
|
7437
7237
|
* (`docs.create`, `files.create`, `createFileTreeInputSchema`, `createBaseInputSchema`,
|
|
@@ -7548,27 +7348,298 @@ const walkNodes = function* (nodes) {
|
|
|
7548
7348
|
};
|
|
7549
7349
|
const collectBaseNodes = (nodes) => [...walkNodes(nodes)].filter((node) => node.type === "base");
|
|
7550
7350
|
//#endregion
|
|
7551
|
-
//#region ../../packages/busabase-package/src/
|
|
7351
|
+
//#region ../../packages/busabase-package/src/frontmatter.ts
|
|
7352
|
+
const DELIMITER = "---";
|
|
7353
|
+
/**
|
|
7354
|
+
* The canonical on-disk body: LF endings, no leading or trailing blank lines. Applied
|
|
7355
|
+
* on both read and write so the round trip is a fixed point — without it, the blank
|
|
7356
|
+
* line the writer puts after the closing `---` and the file's trailing newline would
|
|
7357
|
+
* accumulate into the body on every export→install→export cycle.
|
|
7358
|
+
*
|
|
7359
|
+
* Leading/trailing blank lines around a doc body are therefore normalized away on the
|
|
7360
|
+
* first export; internal blank lines are untouched.
|
|
7361
|
+
*/
|
|
7362
|
+
const normalizeDocBody = (body) => body.replaceAll("\r\n", "\n").replace(/^\n+/, "").replace(/\n+$/, "");
|
|
7363
|
+
/** Split `---\n<yaml>\n---\n<body>`. A doc with no frontmatter is all body. */
|
|
7364
|
+
const parseFrontmatter = (text, filePath) => {
|
|
7365
|
+
const normalized = text.replaceAll("\r\n", "\n");
|
|
7366
|
+
if (!normalized.startsWith(`${DELIMITER}\n`)) return {
|
|
7367
|
+
data: {},
|
|
7368
|
+
body: normalizeDocBody(normalized)
|
|
7369
|
+
};
|
|
7370
|
+
const end = normalized.indexOf(`\n${DELIMITER}`, 3);
|
|
7371
|
+
if (end === -1) throw new Error(`${filePath} opens YAML frontmatter with "---" but never closes it. Add a closing "---" line.`);
|
|
7372
|
+
const rawYaml = normalized.slice(4, end);
|
|
7373
|
+
const body = normalizeDocBody(normalized.slice(end + 1 + 3));
|
|
7374
|
+
let data;
|
|
7375
|
+
try {
|
|
7376
|
+
data = parse(rawYaml);
|
|
7377
|
+
} catch (error) {
|
|
7378
|
+
throw new Error(`${filePath} has invalid YAML frontmatter: ${error.message}`);
|
|
7379
|
+
}
|
|
7380
|
+
if (data === null || data === void 0) return {
|
|
7381
|
+
data: {},
|
|
7382
|
+
body
|
|
7383
|
+
};
|
|
7384
|
+
if (typeof data !== "object" || Array.isArray(data)) throw new Error(`${filePath} frontmatter must be a YAML mapping (key: value pairs).`);
|
|
7385
|
+
return {
|
|
7386
|
+
data,
|
|
7387
|
+
body
|
|
7388
|
+
};
|
|
7389
|
+
};
|
|
7390
|
+
/** Serialize a doc to `---\n<frontmatter>\n---\n<body>` with a trailing newline. */
|
|
7391
|
+
const serializeDoc = (fields, body) => {
|
|
7392
|
+
const data = { name: fields.name };
|
|
7393
|
+
if (fields.description) data.description = fields.description;
|
|
7394
|
+
if (fields.position !== void 0) data.position = fields.position;
|
|
7395
|
+
const agentPrompts = serializeAgentPrompts(fields.agentPrompts);
|
|
7396
|
+
if (agentPrompts) data.agentPrompts = agentPrompts;
|
|
7397
|
+
const yaml = stringify(data, { lineWidth: 0 }).replace(/\n$/, "");
|
|
7398
|
+
return `${DELIMITER}\n${yaml}\n${DELIMITER}\n\n${normalizeDocBody(body)}\n`;
|
|
7399
|
+
};
|
|
7400
|
+
//#endregion
|
|
7401
|
+
//#region ../../packages/busabase-package/src/audit.ts
|
|
7402
|
+
/**
|
|
7403
|
+
* Is this package **fit to publish**?
|
|
7404
|
+
*
|
|
7405
|
+
* Deliberately a different question from {@link validateTemplate}, and the split is
|
|
7406
|
+
* the point rather than an organisational preference:
|
|
7407
|
+
*
|
|
7408
|
+
* - `validateTemplate` **classifies**. Its verdict changes what install *does* — a
|
|
7409
|
+
* Skill node, ownership stamps and a sample merge, versus a plain package install.
|
|
7410
|
+
* Four consumers must agree on it or the catalog lies about what an install does,
|
|
7411
|
+
* so it stays small and stable.
|
|
7412
|
+
* - `auditPackage` / `auditSkill` **advise**. Nothing behaves differently based on
|
|
7413
|
+
* their verdict; they exist for the author and the reviewer, and are free to grow.
|
|
7414
|
+
*
|
|
7415
|
+
* The practical consequence: a declared screenshot that is not on disk belongs here,
|
|
7416
|
+
* never in `validateTemplate`. Promoting it there would stop a template installing
|
|
7417
|
+
* over a missing image, which is the wrong lever entirely.
|
|
7418
|
+
*
|
|
7419
|
+
* Nothing here re-checks what `validateTemplate` already decides (declared resources,
|
|
7420
|
+
* the sample-row ceiling, name agreement between manifest and manual, an unambiguous
|
|
7421
|
+
* primary AirApp). Run both; they compose.
|
|
7422
|
+
*
|
|
7423
|
+
* Purely static: no install, no network, and none of the package's own code is
|
|
7424
|
+
* executed — safe to run in CI over a pull request from someone you do not know.
|
|
7425
|
+
*/
|
|
7426
|
+
/**
|
|
7427
|
+
* A materialized workspace id: busabase-core's `id()` is a three-letter prefix, the
|
|
7428
|
+
* creation time in base36, then seven random characters.
|
|
7429
|
+
*
|
|
7430
|
+
* The lookahead requiring a digit is what keeps ordinary words out — `recommendation`
|
|
7431
|
+
* has the right prefix and the wrong shape. A real id always carries its timestamp.
|
|
7432
|
+
*/
|
|
7433
|
+
const WORKSPACE_ID = /(?<![0-9a-z])(?:nod|bse|viw|rec|crq|ast|com|fom|shr|fvl|opr|cmt|rev|whd|aud)(?=[0-9a-z]{15}(?![0-9a-z]))(?=[0-9a-z]*\d)[0-9a-z]{15}/;
|
|
7434
|
+
const TEXT_EXTENSIONS = /\.(?:js|mjs|cjs|ts|mts|cts|json|ndjson|md|html|css|ya?ml|txt)$|(?:^|\/)\.env(?:\.|$)/;
|
|
7435
|
+
/**
|
|
7436
|
+
* Files that exist to *forbid* a string necessarily contain it. An AirApp's own
|
|
7437
|
+
* `scripts/check.mjs` asserts the obsolete API prefix is absent, and a test fixture
|
|
7438
|
+
* spells out the credential it proves gets rejected — scanning those turns a
|
|
7439
|
+
* correctly-guarded package into a failure.
|
|
7440
|
+
*/
|
|
7441
|
+
const IS_ASSERTION_CODE = /(?:^|\/)(?:scripts\/check\.mjs|tests?\/|[^/]*\.test\.[cm]?[jt]s)$/;
|
|
7442
|
+
/** Lockfiles carry integrity hashes that are not ours to interpret. */
|
|
7443
|
+
const NOT_SOURCE = /(?:^|\/)(?:package-lock\.json|pnpm-lock\.yaml|yarn\.lock|bun\.lockb)$/;
|
|
7444
|
+
const relativePaths = (files, root = "") => {
|
|
7445
|
+
const prefix = root ? `${root.replace(/\/+$/, "")}/` : "";
|
|
7446
|
+
const out = [];
|
|
7447
|
+
for (const key of files.keys()) if (!prefix || key.startsWith(prefix)) out.push(key.slice(prefix.length));
|
|
7448
|
+
return out;
|
|
7449
|
+
};
|
|
7450
|
+
const readText = (files, root, relative) => {
|
|
7451
|
+
const prefix = root ? `${root.replace(/\/+$/, "")}/` : "";
|
|
7452
|
+
return files.get(`${prefix}${relative}`)?.toString("utf8");
|
|
7453
|
+
};
|
|
7454
|
+
/**
|
|
7455
|
+
* Audit the package layer: identity, the assets the catalog promises, and anything
|
|
7456
|
+
* that must never be published.
|
|
7457
|
+
*
|
|
7458
|
+
* `files` is the flat map `readPackageTree` was given, not the tree. That is load-bearing:
|
|
7459
|
+
* `assets/screenshots/` holds no nodes and so never reaches the tree, and "the manifest
|
|
7460
|
+
* promises a screenshot that does not exist" is exactly a file-level question.
|
|
7461
|
+
*/
|
|
7462
|
+
const auditPackage = (tree, files, options = {}) => {
|
|
7463
|
+
const findings = [];
|
|
7464
|
+
const error = (rule, message) => findings.push({
|
|
7465
|
+
severity: "error",
|
|
7466
|
+
rule,
|
|
7467
|
+
message
|
|
7468
|
+
});
|
|
7469
|
+
const warn = (rule, message) => findings.push({
|
|
7470
|
+
severity: "warning",
|
|
7471
|
+
rule,
|
|
7472
|
+
message
|
|
7473
|
+
});
|
|
7474
|
+
const root = options.root ?? "";
|
|
7475
|
+
const paths = relativePaths(files, root);
|
|
7476
|
+
const present = new Set(paths);
|
|
7477
|
+
if (options.directoryName !== void 0 && options.directoryName !== tree.manifest.name) error("package/identity", `Directory is "${options.directoryName}" but busabase.json declares "${tree.manifest.name}". The directory name is the package's identity, not a label beside it.`);
|
|
7478
|
+
for (const screenshot of tree.manifest.template?.screenshots ?? []) if (!present.has(screenshot)) error("package/screenshot-missing", `busabase.json declares screenshot "${screenshot}", which is not in the package. The Template Center card would render a broken image.`);
|
|
7479
|
+
for (const relative of paths) {
|
|
7480
|
+
const base = relative.slice(relative.lastIndexOf("/") + 1);
|
|
7481
|
+
if (/^\.env(?!\.example$)(?:\.|$)/.test(base)) error("package/dotenv", `${relative} is part of the package. Credentials belong in Vault, never in something people install.`);
|
|
7482
|
+
if (/^(?:id_rsa|id_ed25519)$/.test(base) || /\.(?:pem|p12|pfx|keystore)$/.test(base)) error("package/key-material", `${relative} looks like key material and must not be published.`);
|
|
7483
|
+
if (/(?:^|\/)(?:node_modules|dist|build)\//.test(relative)) warn("package/build-output", `${relative} looks like installed or built output. Ship source; the installer builds nothing.`);
|
|
7484
|
+
}
|
|
7485
|
+
for (const relative of paths) {
|
|
7486
|
+
if (!TEXT_EXTENSIONS.test(relative)) continue;
|
|
7487
|
+
if (NOT_SOURCE.test(relative) || IS_ASSERTION_CODE.test(relative)) continue;
|
|
7488
|
+
const source = readText(files, root, relative);
|
|
7489
|
+
if (source === void 0) continue;
|
|
7490
|
+
const found = WORKSPACE_ID.exec(source);
|
|
7491
|
+
if (found) error("package/workspace-ids", `${relative} contains "${found[0]}", a materialized workspace id. Node, Base and Space ids are whoever last ran setup's runtime state — publishing them ships one person's workspace layout to everyone.`);
|
|
7492
|
+
}
|
|
7493
|
+
if (!tree.rootSkill) warn("template/no-skill-node", `This package carries no ${PACKAGE_SKILL_ENTRY}, so nothing installs a manual beside its resources. An agent working in the installed folder has nothing to read, and the person who installed it has nothing to point one at.`);
|
|
7494
|
+
const promptless = nodesWithoutAgentPrompts(tree.nodes);
|
|
7495
|
+
if (promptless.length > 0) warn("template/node-without-prompts", `${promptless.length} installed node(s) carry no agentPrompts (${promptless.slice(0, 5).join(", ")}${promptless.length > 5 ? ", …" : ""}). They will show their node type's generic prompts, so nothing tells the user what THIS app is for. Write scenario prompts per node — see \`busabase-cli nodes set-agent-prompts\`.`);
|
|
7496
|
+
return findings;
|
|
7497
|
+
};
|
|
7552
7498
|
/**
|
|
7553
|
-
*
|
|
7554
|
-
* memory) → the in-memory tree. Used by `install`, and by the round-trip tests.
|
|
7499
|
+
* Node slugs (dotted by folder) with no scenario prompts of their own.
|
|
7555
7500
|
*
|
|
7556
|
-
*
|
|
7557
|
-
*
|
|
7558
|
-
*
|
|
7559
|
-
|
|
7560
|
-
|
|
7561
|
-
|
|
7501
|
+
* Folders are exempt: a folder is a container, and the useful prompts hang off the
|
|
7502
|
+
* things inside it. Everything else is something a person opens and then has to
|
|
7503
|
+
* work out what to ask for.
|
|
7504
|
+
*/
|
|
7505
|
+
const nodesWithoutAgentPrompts = (nodes, prefix = "") => {
|
|
7506
|
+
const out = [];
|
|
7507
|
+
for (const node of nodes) {
|
|
7508
|
+
const path = `${prefix}${node.slug}`;
|
|
7509
|
+
if (node.type === "folder") {
|
|
7510
|
+
out.push(...nodesWithoutAgentPrompts(node.children, `${path}/`));
|
|
7511
|
+
continue;
|
|
7512
|
+
}
|
|
7513
|
+
if (!node.agentPrompts?.length) out.push(path);
|
|
7514
|
+
}
|
|
7515
|
+
return out;
|
|
7516
|
+
};
|
|
7517
|
+
/**
|
|
7518
|
+
* Audit the Skill layer: is the manual one an agent can actually act on?
|
|
7562
7519
|
*
|
|
7563
|
-
*
|
|
7564
|
-
*
|
|
7520
|
+
* Runs for any directory carrying a root `SKILL.md` — package or not, template or not.
|
|
7521
|
+
* It reports nothing at all when there is no manual: a package without one is a
|
|
7522
|
+
* perfectly legitimate thing to publish, not a Skill that failed.
|
|
7565
7523
|
*
|
|
7566
|
-
*
|
|
7567
|
-
*
|
|
7568
|
-
*
|
|
7569
|
-
*
|
|
7570
|
-
* what keeps a template byte-identical to a plain package below the root.
|
|
7524
|
+
* The honest limit: a checker can tell you the frontmatter parses and the files the
|
|
7525
|
+
* manual points at exist. It cannot tell you the manual is *true*. Everything in
|
|
7526
|
+
* `SKILL.md` becomes instructions an agent acts on, and no static rule catches a
|
|
7527
|
+
* confident, wrong sentence about what a table means.
|
|
7571
7528
|
*/
|
|
7529
|
+
const auditSkill = (files, options = {}) => {
|
|
7530
|
+
const findings = [];
|
|
7531
|
+
const error = (rule, message) => findings.push({
|
|
7532
|
+
severity: "error",
|
|
7533
|
+
rule,
|
|
7534
|
+
message
|
|
7535
|
+
});
|
|
7536
|
+
const warn = (rule, message) => findings.push({
|
|
7537
|
+
severity: "warning",
|
|
7538
|
+
rule,
|
|
7539
|
+
message
|
|
7540
|
+
});
|
|
7541
|
+
const root = options.root ?? "";
|
|
7542
|
+
const source = readText(files, root, PACKAGE_SKILL_ENTRY);
|
|
7543
|
+
if (source === void 0) return findings;
|
|
7544
|
+
let body = "";
|
|
7545
|
+
try {
|
|
7546
|
+
const parsed = parseFrontmatter(source, PACKAGE_SKILL_ENTRY);
|
|
7547
|
+
body = parsed.body;
|
|
7548
|
+
const frontmatter = SkillFrontmatterSchema$1.safeParse(parsed.data);
|
|
7549
|
+
if (!frontmatter.success) error("skill/frontmatter", `${PACKAGE_SKILL_ENTRY} frontmatter is not a valid Skill declaration: ${frontmatter.error.issues.map((issue) => `${issue.path.join(".") || "(root)"} ${issue.message}`).join("; ")}`);
|
|
7550
|
+
else if (frontmatter.data.description.trim() === "") error("skill/description", `${PACKAGE_SKILL_ENTRY} has no description. An agent chooses whether to reach for a Skill by reading it, so an empty one is not cosmetic — it is a Skill that never gets picked.`);
|
|
7551
|
+
} catch (cause) {
|
|
7552
|
+
error("skill/frontmatter", `${PACKAGE_SKILL_ENTRY}: ${cause.message}`);
|
|
7553
|
+
return findings;
|
|
7554
|
+
}
|
|
7555
|
+
const todos = (body.match(/^.*\bTODO\b.*$/gm) ?? []).length;
|
|
7556
|
+
if (todos > 0) error("skill/todo", `${PACKAGE_SKILL_ENTRY} still carries ${todos} TODO line(s). The export draft leaves them on purpose — an agent acts on what this file says, and a blank the author was meant to fill in is worse than nothing once it ships.`);
|
|
7557
|
+
const present = new Set(relativePaths(files, root));
|
|
7558
|
+
const sidecar = new RegExp(`\\]\\((\\.?/?(?:${PACKAGE_SIDECARS})/[^)\\s]+)\\)|\`((?:${PACKAGE_SIDECARS})/[^\`\\s]+)\``, "g");
|
|
7559
|
+
const seen = /* @__PURE__ */ new Set();
|
|
7560
|
+
for (const match of body.matchAll(sidecar)) {
|
|
7561
|
+
const target = (match[1] ?? match[2]).replace(/^\.?\//, "");
|
|
7562
|
+
if (seen.has(target)) continue;
|
|
7563
|
+
seen.add(target);
|
|
7564
|
+
if (!present.has(target)) warn("skill/missing-reference", `${PACKAGE_SKILL_ENTRY} points at "${target}", which the package does not ship. An agent told to read it has an instruction it cannot satisfy.`);
|
|
7565
|
+
}
|
|
7566
|
+
return findings;
|
|
7567
|
+
};
|
|
7568
|
+
const PACKAGE_SIDECARS = PACKAGE_SKILL_SIDECAR_DIRS.join("|");
|
|
7569
|
+
//#endregion
|
|
7570
|
+
//#region ../../packages/busabase-package/src/ignore.ts
|
|
7571
|
+
const escapeLiteral = (value) => value.replace(/[.+^${}()|[\]\\]/g, "\\$&");
|
|
7572
|
+
/**
|
|
7573
|
+
* Translate one gitignore-ish pattern into a regex over a POSIX relative path.
|
|
7574
|
+
*
|
|
7575
|
+
* The three globs are handled in one pass so that `**` is recognised before the
|
|
7576
|
+
* `*` it starts with — splitting on `*` first would turn `**` into two
|
|
7577
|
+
* segment-local wildcards and quietly stop matching across directories.
|
|
7578
|
+
*/
|
|
7579
|
+
const patternToRegex = (pattern, anchored) => {
|
|
7580
|
+
let source = "";
|
|
7581
|
+
for (let index = 0; index < pattern.length; index += 1) {
|
|
7582
|
+
const char = pattern[index];
|
|
7583
|
+
if (char === "*") {
|
|
7584
|
+
if (pattern[index + 1] === "*") {
|
|
7585
|
+
index += 1;
|
|
7586
|
+
if (pattern[index + 1] === "/") {
|
|
7587
|
+
index += 1;
|
|
7588
|
+
source += "(?:.*/)?";
|
|
7589
|
+
} else source += ".*";
|
|
7590
|
+
} else source += "[^/]*";
|
|
7591
|
+
continue;
|
|
7592
|
+
}
|
|
7593
|
+
if (char === "?") {
|
|
7594
|
+
source += "[^/]";
|
|
7595
|
+
continue;
|
|
7596
|
+
}
|
|
7597
|
+
if (char === "/") {
|
|
7598
|
+
source += "/";
|
|
7599
|
+
continue;
|
|
7600
|
+
}
|
|
7601
|
+
source += escapeLiteral(char);
|
|
7602
|
+
}
|
|
7603
|
+
return new RegExp(`${anchored ? "^" : "^(?:.*/)?"}${source}(?:/.*)?$`);
|
|
7604
|
+
};
|
|
7605
|
+
const parseRule = (line) => {
|
|
7606
|
+
let pattern = line.trim();
|
|
7607
|
+
if (!pattern || pattern.startsWith("#")) return void 0;
|
|
7608
|
+
const negated = pattern.startsWith("!");
|
|
7609
|
+
if (negated) pattern = pattern.slice(1);
|
|
7610
|
+
const directoryOnly = pattern.endsWith("/");
|
|
7611
|
+
if (directoryOnly) pattern = pattern.slice(0, -1);
|
|
7612
|
+
const anchored = pattern.startsWith("/");
|
|
7613
|
+
if (anchored) pattern = pattern.slice(1);
|
|
7614
|
+
if (!pattern) return void 0;
|
|
7615
|
+
return {
|
|
7616
|
+
regex: patternToRegex(pattern, anchored),
|
|
7617
|
+
negated,
|
|
7618
|
+
directoryOnly
|
|
7619
|
+
};
|
|
7620
|
+
};
|
|
7621
|
+
const parseIgnoreFile = (contents) => {
|
|
7622
|
+
const rules = contents.split(/\r?\n/).map(parseRule).filter((rule) => rule !== void 0);
|
|
7623
|
+
return {
|
|
7624
|
+
isEmpty: rules.length === 0,
|
|
7625
|
+
ignores: (relativePath) => {
|
|
7626
|
+
let ignored = false;
|
|
7627
|
+
for (const rule of rules) {
|
|
7628
|
+
if (rule.directoryOnly && !rule.regex.test(`${relativePath}/`)) continue;
|
|
7629
|
+
if (!rule.regex.test(relativePath)) continue;
|
|
7630
|
+
ignored = !rule.negated;
|
|
7631
|
+
}
|
|
7632
|
+
return ignored;
|
|
7633
|
+
}
|
|
7634
|
+
};
|
|
7635
|
+
};
|
|
7636
|
+
/** A matcher that installs everything — used when no ignore file is present. */
|
|
7637
|
+
const IGNORE_NOTHING = {
|
|
7638
|
+
ignores: () => false,
|
|
7639
|
+
isEmpty: true
|
|
7640
|
+
};
|
|
7641
|
+
//#endregion
|
|
7642
|
+
//#region ../../packages/busabase-package/src/layout-read.ts
|
|
7572
7643
|
const decodeText = (bytes, filePath) => {
|
|
7573
7644
|
const text = bytes.toString("utf8");
|
|
7574
7645
|
if (text.includes("�")) throw new Error(`${filePath} is not valid UTF-8 text.`);
|
|
@@ -7623,6 +7694,20 @@ const zodParse = (schema, value, filePath) => {
|
|
|
7623
7694
|
}
|
|
7624
7695
|
return result.data;
|
|
7625
7696
|
};
|
|
7697
|
+
/**
|
|
7698
|
+
* A sidecar's `agentPrompts` as the tree carries it — as a spread, so a node that
|
|
7699
|
+
* has none carries no `agentPrompts` KEY at all rather than one set to `undefined`.
|
|
7700
|
+
*
|
|
7701
|
+
* `[]` and "absent" mean the same thing on disk (the node falls back to its type's
|
|
7702
|
+
* default prompts either way), so an empty list is dropped rather than
|
|
7703
|
+
* round-tripping as a sidecar key that changes nothing.
|
|
7704
|
+
*/
|
|
7705
|
+
/** `base.json` minus the node-level key, so the list lives in exactly one place. */
|
|
7706
|
+
const baseWithoutAgentPrompts = (base) => {
|
|
7707
|
+
const { agentPrompts: _agentPrompts, ...rest } = base;
|
|
7708
|
+
return rest;
|
|
7709
|
+
};
|
|
7710
|
+
const agentPromptsField = (prompts) => prompts?.length ? { agentPrompts: prompts } : {};
|
|
7626
7711
|
const listDir = (files, dir) => {
|
|
7627
7712
|
const fileNames = /* @__PURE__ */ new Set();
|
|
7628
7713
|
const dirNames = /* @__PURE__ */ new Set();
|
|
@@ -7666,7 +7751,7 @@ const readPackageTree = (files, options = {}) => {
|
|
|
7666
7751
|
const manifest = zodParse(PackageManifestSchema, rawManifest, manifestPath);
|
|
7667
7752
|
const contentDir = `${root}${PACKAGE_CONTENT_DIRNAME}/`;
|
|
7668
7753
|
const nodes = readNodes(files, contentDir);
|
|
7669
|
-
const rootSkill = readRootSkill(files, root, manifest.name, manifest.description);
|
|
7754
|
+
const rootSkill = readRootSkill(files, root, manifest.name, iStringParse(manifest.description, "en"));
|
|
7670
7755
|
return {
|
|
7671
7756
|
manifest,
|
|
7672
7757
|
nodes,
|
|
@@ -7795,6 +7880,7 @@ const readDirNode = (files, dir, slug) => {
|
|
|
7795
7880
|
name: meta.name,
|
|
7796
7881
|
description: meta.description,
|
|
7797
7882
|
position: meta.position,
|
|
7883
|
+
...agentPromptsField(meta.agentPrompts),
|
|
7798
7884
|
files: entries
|
|
7799
7885
|
};
|
|
7800
7886
|
}
|
|
@@ -7808,7 +7894,8 @@ const readDirNode = (files, dir, slug) => {
|
|
|
7808
7894
|
name: base.name,
|
|
7809
7895
|
description: base.description,
|
|
7810
7896
|
position: base.position,
|
|
7811
|
-
base,
|
|
7897
|
+
...agentPromptsField(base.agentPrompts),
|
|
7898
|
+
base: baseWithoutAgentPrompts(base),
|
|
7812
7899
|
records
|
|
7813
7900
|
};
|
|
7814
7901
|
}
|
|
@@ -7816,7 +7903,8 @@ const readDirNode = (files, dir, slug) => {
|
|
|
7816
7903
|
const meta = files.has(metaPath) ? zodParse(PackageFolderMetaSchema, parseJsonFile(files, metaPath), metaPath) : {
|
|
7817
7904
|
name: void 0,
|
|
7818
7905
|
description: "",
|
|
7819
|
-
position: void 0
|
|
7906
|
+
position: void 0,
|
|
7907
|
+
agentPrompts: void 0
|
|
7820
7908
|
};
|
|
7821
7909
|
return {
|
|
7822
7910
|
type: "folder",
|
|
@@ -7824,6 +7912,7 @@ const readDirNode = (files, dir, slug) => {
|
|
|
7824
7912
|
name: meta.name ?? humanizeSlug(slug),
|
|
7825
7913
|
description: meta.description,
|
|
7826
7914
|
position: meta.position,
|
|
7915
|
+
...agentPromptsField(meta.agentPrompts),
|
|
7827
7916
|
children: readNodes(files, dir)
|
|
7828
7917
|
};
|
|
7829
7918
|
};
|
|
@@ -7874,7 +7963,8 @@ const readFileNode = (files, dir, fileName) => {
|
|
|
7874
7963
|
const meta = files.has(sidecarPath) ? zodParse(PackageFileNodeMetaSchema, parseJsonFile(files, sidecarPath), sidecarPath) : {
|
|
7875
7964
|
name: void 0,
|
|
7876
7965
|
description: "",
|
|
7877
|
-
position: void 0
|
|
7966
|
+
position: void 0,
|
|
7967
|
+
agentPrompts: void 0
|
|
7878
7968
|
};
|
|
7879
7969
|
return {
|
|
7880
7970
|
type: "file",
|
|
@@ -7882,6 +7972,7 @@ const readFileNode = (files, dir, fileName) => {
|
|
|
7882
7972
|
name: meta.name ?? fileName,
|
|
7883
7973
|
description: meta.description,
|
|
7884
7974
|
position: meta.position,
|
|
7975
|
+
...agentPromptsField(meta.agentPrompts),
|
|
7885
7976
|
fileName,
|
|
7886
7977
|
mimeType: guessMimeType$1(fileName),
|
|
7887
7978
|
bytes
|
|
@@ -7902,6 +7993,7 @@ const readDocNode = (bytes, dir, fileName) => {
|
|
|
7902
7993
|
name: frontmatter.name,
|
|
7903
7994
|
description: frontmatter.description,
|
|
7904
7995
|
position: frontmatter.position,
|
|
7996
|
+
...agentPromptsField(frontmatter.agentPrompts),
|
|
7905
7997
|
body
|
|
7906
7998
|
};
|
|
7907
7999
|
};
|
|
@@ -8037,7 +8129,7 @@ const deriveSkillDraft = (tree) => {
|
|
|
8037
8129
|
const bases = nodes.filter((node) => node.type === "base");
|
|
8038
8130
|
const airapps = nodes.filter(isAirApp);
|
|
8039
8131
|
const name = tree.manifest.name;
|
|
8040
|
-
const description = tree.manifest.description || `TODO: one line on what ${name} does.`;
|
|
8132
|
+
const description = iStringParse(tree.manifest.description, "en") || `TODO: one line on what ${name} does.`;
|
|
8041
8133
|
const yamlString = (value) => JSON.stringify(value);
|
|
8042
8134
|
const resourceLines = bases.map((node) => ` - ${yamlString(node.slug)}`).join("\n");
|
|
8043
8135
|
const resourceDocs = bases.map((node) => {
|
|
@@ -8519,7 +8611,7 @@ const applyInstall = async (client, plan, options) => {
|
|
|
8519
8611
|
parentNodeId: void 0,
|
|
8520
8612
|
slug: plan.targetFolderSlug,
|
|
8521
8613
|
name: plan.tree.manifest.name,
|
|
8522
|
-
description: plan.tree.manifest.description,
|
|
8614
|
+
description: iStringParse(plan.tree.manifest.description, "en"),
|
|
8523
8615
|
submittedBy: options.submittedBy,
|
|
8524
8616
|
metadata: app?.rootMetadata
|
|
8525
8617
|
});
|
|
@@ -8751,6 +8843,37 @@ const stampResource = async (client, app, nodeId, installedSlug, state, extra =
|
|
|
8751
8843
|
}
|
|
8752
8844
|
};
|
|
8753
8845
|
/**
|
|
8846
|
+
* Write a node's carried scenario prompts onto the node install just created.
|
|
8847
|
+
*
|
|
8848
|
+
* After creation rather than inline, because the prompts live in their own
|
|
8849
|
+
* column with their own route — no create endpoint accepts them. That has one
|
|
8850
|
+
* visible consequence, and it is the reason for the warning below: on the
|
|
8851
|
+
* review-first path a Doc or File node is a PENDING change request with no node
|
|
8852
|
+
* id yet, so there is nothing to write them to. Structure (folders, Bases,
|
|
8853
|
+
* Skills/AirApps/Drives) is always materialized (see {@link createBase}), so the
|
|
8854
|
+
* nodes that carry most prompts are unaffected.
|
|
8855
|
+
*
|
|
8856
|
+
* Never fails the install, for the same reason {@link stampResource} does not:
|
|
8857
|
+
* the resources are already there and useful, and prompts can be re-applied with
|
|
8858
|
+
* `busabase-cli nodes set-agent-prompts` at any time.
|
|
8859
|
+
*/
|
|
8860
|
+
const applyAgentPrompts = async (client, nodeId, node, state) => {
|
|
8861
|
+
const agentPrompts = node.agentPrompts;
|
|
8862
|
+
if (!agentPrompts?.length) return;
|
|
8863
|
+
if (!nodeId) {
|
|
8864
|
+
state.warnings.push(`"${node.slug}" installs as a change request, so its ${agentPrompts.length} agent prompt(s) were not applied. After merging it, run \`busabase-cli nodes set-agent-prompts\` for that node, or re-install with --auto-merge.`);
|
|
8865
|
+
return;
|
|
8866
|
+
}
|
|
8867
|
+
try {
|
|
8868
|
+
await client.nodes.updateAgentPrompts({
|
|
8869
|
+
nodeId,
|
|
8870
|
+
agentPrompts
|
|
8871
|
+
});
|
|
8872
|
+
} catch (error) {
|
|
8873
|
+
state.warnings.push(`Could not set agent prompts on "${node.slug}": ${error.message}. The node installed fine; it will show its node type's default prompts.`);
|
|
8874
|
+
}
|
|
8875
|
+
};
|
|
8876
|
+
/**
|
|
8754
8877
|
* Re-create the package's carried Doc images on THIS host and return the
|
|
8755
8878
|
* old id → new id map the Doc bodies are rewritten through.
|
|
8756
8879
|
*
|
|
@@ -8784,21 +8907,24 @@ const createStructure = async (client, nodes, parentNodeId, options, state, base
|
|
|
8784
8907
|
submittedBy: options.submittedBy
|
|
8785
8908
|
});
|
|
8786
8909
|
state.created.folders++;
|
|
8910
|
+
await applyAgentPrompts(client, nodeId, node, state);
|
|
8787
8911
|
await createStructure(client, node.children, nodeId, options, state, bases, fieldIds, docAssetIds, app);
|
|
8788
8912
|
break;
|
|
8789
8913
|
}
|
|
8790
8914
|
case "doc": {
|
|
8791
8915
|
const warning = reportUnresolvableDocAssets(node.slug, node.body, new Set(docAssetIds.keys()));
|
|
8792
8916
|
if (warning) state.warnings.push(warning);
|
|
8793
|
-
|
|
8917
|
+
const result = await client.docs.create({
|
|
8794
8918
|
parentNodeId,
|
|
8795
8919
|
slug: node.slug,
|
|
8796
8920
|
name: node.name,
|
|
8797
8921
|
description: node.description,
|
|
8798
8922
|
body: rewriteDocAssetIds(node.body, docAssetIds),
|
|
8799
8923
|
autoMerge: options.autoMerge
|
|
8800
|
-
})
|
|
8924
|
+
});
|
|
8925
|
+
if (result.materialized) state.created.docs++;
|
|
8801
8926
|
else state.pendingChangeRequests++;
|
|
8927
|
+
await applyAgentPrompts(client, result.materialized ? result.node.id : void 0, node, state);
|
|
8802
8928
|
break;
|
|
8803
8929
|
}
|
|
8804
8930
|
case "base":
|
|
@@ -8817,15 +8943,17 @@ const createStructure = async (client, nodes, parentNodeId, options, state, base
|
|
|
8817
8943
|
state.warnings.push(asSkippableUpload(error, node.fileName));
|
|
8818
8944
|
break;
|
|
8819
8945
|
}
|
|
8820
|
-
|
|
8946
|
+
const result = await client.files.create({
|
|
8821
8947
|
parentNodeId,
|
|
8822
8948
|
slug: node.slug,
|
|
8823
8949
|
name: node.name,
|
|
8824
8950
|
description: node.description,
|
|
8825
8951
|
assetId: asset.assetId,
|
|
8826
8952
|
autoMerge: options.autoMerge
|
|
8827
|
-
})
|
|
8953
|
+
});
|
|
8954
|
+
if (result.materialized) state.created.files++;
|
|
8828
8955
|
else state.pendingChangeRequests++;
|
|
8956
|
+
await applyAgentPrompts(client, result.materialized ? result.node.id : void 0, node, state);
|
|
8829
8957
|
break;
|
|
8830
8958
|
}
|
|
8831
8959
|
}
|
|
@@ -8877,6 +9005,7 @@ const createBase = async (client, node, parentNodeId, options, state, bases, fie
|
|
|
8877
9005
|
});
|
|
8878
9006
|
indexFields(fieldIds, node.slug, result.fields);
|
|
8879
9007
|
await stampResource(client, app, result.nodeId, node.slug, state);
|
|
9008
|
+
await applyAgentPrompts(client, result.nodeId, node, state);
|
|
8880
9009
|
for (const view of node.base.views) {
|
|
8881
9010
|
if (!(await client.views.changeRequest({
|
|
8882
9011
|
operation: "create",
|
|
@@ -8911,7 +9040,7 @@ const createFileTreeNode = async (client, node, parentNodeId, options, state, ap
|
|
|
8911
9040
|
return;
|
|
8912
9041
|
}
|
|
8913
9042
|
}))).filter((entry) => entry !== void 0);
|
|
8914
|
-
|
|
9043
|
+
const result = await client.fileTrees.create({
|
|
8915
9044
|
type: node.type,
|
|
8916
9045
|
parentNodeId,
|
|
8917
9046
|
slug: node.slug,
|
|
@@ -8921,10 +9050,12 @@ const createFileTreeNode = async (client, node, parentNodeId, options, state, ap
|
|
|
8921
9050
|
...app ? { metadata: resourceStamp(app, node.slug, extraMetadata) } : {},
|
|
8922
9051
|
mergeMode: "replace",
|
|
8923
9052
|
autoMerge: options.autoMerge
|
|
8924
|
-
})
|
|
9053
|
+
});
|
|
9054
|
+
if (result.materialized) {
|
|
8925
9055
|
state.created.fileTreeNodes++;
|
|
8926
9056
|
state.created.files += node.files.length;
|
|
8927
9057
|
} else state.pendingChangeRequests++;
|
|
9058
|
+
await applyAgentPrompts(client, result.materialized ? result.node.id : void 0, node, state);
|
|
8928
9059
|
};
|
|
8929
9060
|
/**
|
|
8930
9061
|
* Thrown when the host can't accept an upload the normal way — see
|
|
@@ -9166,15 +9297,6 @@ const approveAndMerge = async (client, changeRequestId) => {
|
|
|
9166
9297
|
};
|
|
9167
9298
|
//#endregion
|
|
9168
9299
|
//#region ../../packages/busabase-package/src/collect.ts
|
|
9169
|
-
/**
|
|
9170
|
-
* `export`'s read half: a space subtree → the in-memory package tree, entirely via
|
|
9171
|
-
* the public read API. The inverse of `apply`.
|
|
9172
|
-
*
|
|
9173
|
-
* Rewrites the three id-bearing option keys back to slug references, drops everything
|
|
9174
|
-
* the format has no slot for (ids, history, permissions), carries the bytes behind
|
|
9175
|
-
* every Doc's inline images (see {@link collectDocAssets}), and enforces the format's
|
|
9176
|
-
* self-containment rule: a relation may not leave the package.
|
|
9177
|
-
*/
|
|
9178
9300
|
/** Find the subtree to export by node slug or id. */
|
|
9179
9301
|
const findSourceNode = (nodes, slugOrId) => {
|
|
9180
9302
|
const stack = [...nodes];
|
|
@@ -9191,7 +9313,8 @@ const collectPackageTree = async (client, root, options) => {
|
|
|
9191
9313
|
const { skillSource, rest } = partitionTemplateSkill(children);
|
|
9192
9314
|
const collectOptions = {
|
|
9193
9315
|
...options,
|
|
9194
|
-
baseResourceKeys: collectBaseResourceKeys(children)
|
|
9316
|
+
baseResourceKeys: collectBaseResourceKeys(children),
|
|
9317
|
+
agentPromptsUnavailable: options.agentPromptsUnavailable ?? { value: false }
|
|
9195
9318
|
};
|
|
9196
9319
|
const nodes = await collectNodes(client, rest, collectOptions);
|
|
9197
9320
|
const tree = {
|
|
@@ -9335,12 +9458,36 @@ const collectNodes = async (client, sources, options) => {
|
|
|
9335
9458
|
}
|
|
9336
9459
|
return sortNodes(nodes);
|
|
9337
9460
|
};
|
|
9461
|
+
/**
|
|
9462
|
+
* This node's scenario Agent prompts, read from the column they live in.
|
|
9463
|
+
*
|
|
9464
|
+
* A separate read per node because that is the only way to get them: they are
|
|
9465
|
+
* deliberately absent from `nodes.list` (50 prompts x 8 KiB per locale is why
|
|
9466
|
+
* they have their own column and their own route).
|
|
9467
|
+
*
|
|
9468
|
+
* Never fatal. A server that predates the route, or a node this key may read but
|
|
9469
|
+
* whose prompts it may not, costs the export those prompts and nothing else —
|
|
9470
|
+
* the package still installs, and its nodes fall back to their type's defaults,
|
|
9471
|
+
* exactly as every package did before the field existed.
|
|
9472
|
+
*/
|
|
9473
|
+
const collectAgentPrompts = async (client, source, options) => {
|
|
9474
|
+
if (options.agentPromptsUnavailable?.value) return void 0;
|
|
9475
|
+
try {
|
|
9476
|
+
const result = await client.nodes.getAgentPrompts({ nodeId: source.id });
|
|
9477
|
+
return result.agentPrompts?.length ? result.agentPrompts : void 0;
|
|
9478
|
+
} catch (error) {
|
|
9479
|
+
if (options.agentPromptsUnavailable) options.agentPromptsUnavailable.value = true;
|
|
9480
|
+
options.warn(`Could not read agent prompts for "${source.slug}": ${error.message}. Exported without them for this node and every node after it; the installed nodes will show their node type's default prompts.`);
|
|
9481
|
+
return;
|
|
9482
|
+
}
|
|
9483
|
+
};
|
|
9338
9484
|
const collectNode = async (client, source, options) => {
|
|
9339
9485
|
const common = {
|
|
9340
9486
|
slug: restoredNodeSlug(source),
|
|
9341
9487
|
name: source.name,
|
|
9342
9488
|
description: source.description ?? "",
|
|
9343
|
-
position: source.position
|
|
9489
|
+
position: source.position,
|
|
9490
|
+
agentPrompts: await collectAgentPrompts(client, source, options)
|
|
9344
9491
|
};
|
|
9345
9492
|
switch (source.type) {
|
|
9346
9493
|
case "folder": return {
|
|
@@ -9942,7 +10089,8 @@ const countTree = (tree) => {
|
|
|
9942
10089
|
const renderPlan = (plan) => {
|
|
9943
10090
|
const lines = [];
|
|
9944
10091
|
lines.push(`Package: ${plan.tree.manifest.name}`);
|
|
9945
|
-
|
|
10092
|
+
const description = iStringParse(plan.tree.manifest.description, "en");
|
|
10093
|
+
if (description) lines.push(` ${description}`);
|
|
9946
10094
|
lines.push("");
|
|
9947
10095
|
lines.push(`Target folder: ${plan.targetFolderSlug}`);
|
|
9948
10096
|
lines.push("");
|
|
@@ -10410,11 +10558,12 @@ const serializeView = (view) => compact({
|
|
|
10410
10558
|
fieldWidths: view.config.fieldWidths
|
|
10411
10559
|
})
|
|
10412
10560
|
});
|
|
10413
|
-
const serializeBase = (base, position) => toJsonFile(compact({
|
|
10561
|
+
const serializeBase = (base, position, agentPrompts) => toJsonFile(compact({
|
|
10414
10562
|
name: base.name,
|
|
10415
10563
|
description: base.description || void 0,
|
|
10416
10564
|
position,
|
|
10417
10565
|
reviewPolicy: base.reviewPolicy,
|
|
10566
|
+
agentPrompts: serializeAgentPrompts(agentPrompts),
|
|
10418
10567
|
fields: base.fields.map(serializeField),
|
|
10419
10568
|
views: base.views.map(serializeView)
|
|
10420
10569
|
}));
|
|
@@ -10488,10 +10637,11 @@ const renderNodes = (nodes, dir, files) => {
|
|
|
10488
10637
|
const renderNode = (node, dir, files) => {
|
|
10489
10638
|
switch (node.type) {
|
|
10490
10639
|
case "folder":
|
|
10491
|
-
if (node.children.length === 0 || node.description !== "" || node.position !== void 0 || node.name !== void 0) files.set(`${dir}${node.slug}/${PACKAGE_FOLDER_META_FILENAME}`, toJsonFile(compact({
|
|
10640
|
+
if (node.children.length === 0 || node.description !== "" || node.position !== void 0 || node.agentPrompts !== void 0 || node.name !== void 0) files.set(`${dir}${node.slug}/${PACKAGE_FOLDER_META_FILENAME}`, toJsonFile(compact({
|
|
10492
10641
|
name: node.name,
|
|
10493
10642
|
description: node.description || void 0,
|
|
10494
|
-
position: node.position
|
|
10643
|
+
position: node.position,
|
|
10644
|
+
agentPrompts: serializeAgentPrompts(node.agentPrompts)
|
|
10495
10645
|
})));
|
|
10496
10646
|
renderNodes(node.children, `${dir}${node.slug}/`, files);
|
|
10497
10647
|
return;
|
|
@@ -10499,11 +10649,12 @@ const renderNode = (node, dir, files) => {
|
|
|
10499
10649
|
files.set(`${dir}${node.slug}.md`, Buffer.from(serializeDoc({
|
|
10500
10650
|
name: node.name,
|
|
10501
10651
|
description: node.description,
|
|
10502
|
-
position: node.position
|
|
10652
|
+
position: node.position,
|
|
10653
|
+
agentPrompts: node.agentPrompts
|
|
10503
10654
|
}, node.body), "utf8"));
|
|
10504
10655
|
return;
|
|
10505
10656
|
case "base": {
|
|
10506
|
-
files.set(`${dir}${node.slug}/${PACKAGE_BASE_FILENAME}`, serializeBase(node.base, node.position));
|
|
10657
|
+
files.set(`${dir}${node.slug}/${PACKAGE_BASE_FILENAME}`, serializeBase(node.base, node.position, node.agentPrompts));
|
|
10507
10658
|
const records = serializeRecords(node.records);
|
|
10508
10659
|
if (records.byteLength > 0) files.set(`${dir}${node.slug}/${PACKAGE_RECORDS_FILENAME}`, records);
|
|
10509
10660
|
return;
|
|
@@ -10517,7 +10668,8 @@ const renderNode = (node, dir, files) => {
|
|
|
10517
10668
|
type: node.type,
|
|
10518
10669
|
name: node.name,
|
|
10519
10670
|
description: node.description || void 0,
|
|
10520
|
-
position: node.position
|
|
10671
|
+
position: node.position,
|
|
10672
|
+
agentPrompts: serializeAgentPrompts(node.agentPrompts)
|
|
10521
10673
|
})));
|
|
10522
10674
|
for (const entry of [...node.files].sort((a, b) => a.path.localeCompare(b.path, "en"))) {
|
|
10523
10675
|
assertSafeFilePath(entry.path, `${node.type} "${node.slug}"`);
|
|
@@ -10528,10 +10680,11 @@ const renderNode = (node, dir, files) => {
|
|
|
10528
10680
|
case "file":
|
|
10529
10681
|
assertSafeFilePath(node.fileName, `file node "${node.slug}"`);
|
|
10530
10682
|
files.set(`${dir}${node.fileName}`, node.bytes);
|
|
10531
|
-
if (node.name !== node.fileName || node.description !== "" || node.position !== void 0) files.set(`${dir}${node.fileName}${PACKAGE_NODE_META_SUFFIX}`, toJsonFile(compact({
|
|
10683
|
+
if (node.name !== node.fileName || node.description !== "" || node.position !== void 0 || node.agentPrompts !== void 0) files.set(`${dir}${node.fileName}${PACKAGE_NODE_META_SUFFIX}`, toJsonFile(compact({
|
|
10532
10684
|
name: node.name,
|
|
10533
10685
|
description: node.description || void 0,
|
|
10534
|
-
position: node.position
|
|
10686
|
+
position: node.position,
|
|
10687
|
+
agentPrompts: serializeAgentPrompts(node.agentPrompts)
|
|
10535
10688
|
})));
|
|
10536
10689
|
return;
|
|
10537
10690
|
}
|
|
@@ -10691,7 +10844,8 @@ const renderPackageChoices = (repoUrl, candidates, wanted) => {
|
|
|
10691
10844
|
candidate.primaryAirApp ? "app" : void 0
|
|
10692
10845
|
].filter(Boolean).join(" · ");
|
|
10693
10846
|
lines.push(` ${candidate.name} (${shape})`);
|
|
10694
|
-
|
|
10847
|
+
const description = iStringParse(candidate.description, "en");
|
|
10848
|
+
if (description) lines.push(` ${description}`);
|
|
10695
10849
|
for (const error of candidate.templateErrors) lines.push(` not a template: ${error}`);
|
|
10696
10850
|
}
|
|
10697
10851
|
lines.push("");
|
|
@@ -10742,7 +10896,7 @@ const runExport = async (client, nodeSlugOrId, options) => {
|
|
|
10742
10896
|
tree.rootSkill = {
|
|
10743
10897
|
slug: manifest.name,
|
|
10744
10898
|
name: manifest.name,
|
|
10745
|
-
description: manifest.description,
|
|
10899
|
+
description: iStringParse(manifest.description, "en"),
|
|
10746
10900
|
files: [{
|
|
10747
10901
|
path: PACKAGE_SKILL_ENTRY,
|
|
10748
10902
|
bytes: Buffer.from(deriveSkillDraft(tree), "utf8")
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "busa-cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.51.1",
|
|
4
4
|
"description": "Command-line client for the Busabase OpenAPI REST API. Talks to a local or remote `busabase server`. Short-name alias for busabase-cli.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"homepage": "https://github.com/busabase/busabase/tree/main/apps/busabase-cli",
|
|
@@ -33,7 +33,7 @@
|
|
|
33
33
|
"dependencies": {
|
|
34
34
|
"@zip.js/zip.js": "^2.8.20",
|
|
35
35
|
"busabase-package": "workspace:*",
|
|
36
|
-
"busabase-sdk": "0.
|
|
36
|
+
"busabase-sdk": "0.51.1",
|
|
37
37
|
"commander": "^14.0.0",
|
|
38
38
|
"qrcode": "^1.5.4",
|
|
39
39
|
"tar-stream": "^3.1.7",
|