busa-cli 0.19.1 → 0.20.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.js +1 -1
- package/dist/index.js +1 -1
- package/dist/{run--bA-jLSP.js → run-Cx9V5brn.js} +273 -113
- package/package.json +2 -2
package/dist/cli.js
CHANGED
package/dist/index.js
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
import { n as runCli, r as render, t as HELP } from "./run
|
|
1
|
+
import { n as runCli, r as render, t as HELP } from "./run-Cx9V5brn.js";
|
|
2
2
|
import { DEFAULT_BASE_URL, createBusabaseClient, normalizeBaseUrl } from "busabase-sdk";
|
|
3
3
|
export { DEFAULT_BASE_URL, HELP, createBusabaseClient, normalizeBaseUrl, render, runCli };
|
|
@@ -4992,6 +4992,23 @@ const userRefSchema = z.object({
|
|
|
4992
4992
|
image: z.string().nullable(),
|
|
4993
4993
|
role: z.string().nullable().optional()
|
|
4994
4994
|
});
|
|
4995
|
+
const sourceChannelSchema = z.enum([
|
|
4996
|
+
"web_ui",
|
|
4997
|
+
"browser",
|
|
4998
|
+
"openapi",
|
|
4999
|
+
"sdk",
|
|
5000
|
+
"cli",
|
|
5001
|
+
"mcp",
|
|
5002
|
+
"skill",
|
|
5003
|
+
"webhook",
|
|
5004
|
+
"automation",
|
|
5005
|
+
"import"
|
|
5006
|
+
]);
|
|
5007
|
+
const sourceAttributionSchema = z.object({
|
|
5008
|
+
displayName: z.string().nullable(),
|
|
5009
|
+
ownerName: z.string().nullable(),
|
|
5010
|
+
channel: sourceChannelSchema.nullable()
|
|
5011
|
+
});
|
|
4995
5012
|
const commitSchema = z.object({
|
|
4996
5013
|
id: z.string(),
|
|
4997
5014
|
baseId: z.string().nullable(),
|
|
@@ -5083,6 +5100,7 @@ const changeRequestSchema = z.object({
|
|
|
5083
5100
|
status: changeRequestStatusSchema,
|
|
5084
5101
|
submittedBy: z.string(),
|
|
5085
5102
|
submittedByUser: userRefSchema.nullable().optional().default(null),
|
|
5103
|
+
sourceAttribution: sourceAttributionSchema.nullable().optional(),
|
|
5086
5104
|
sourceMeta: z.record(z.string(), z.unknown()),
|
|
5087
5105
|
reviewPolicySnapshot: z.record(z.string(), z.unknown()),
|
|
5088
5106
|
mergeSummary: z.record(z.string(), z.unknown()),
|
|
@@ -5171,6 +5189,7 @@ z.object({
|
|
|
5171
5189
|
action: auditActionSchema,
|
|
5172
5190
|
actorId: z.string(),
|
|
5173
5191
|
actor: userRefSchema.nullable().optional().default(null),
|
|
5192
|
+
sourceAttribution: sourceAttributionSchema.nullable().optional(),
|
|
5174
5193
|
baseId: z.string().nullable(),
|
|
5175
5194
|
recordId: z.string().nullable(),
|
|
5176
5195
|
changeRequestId: z.string().nullable(),
|
|
@@ -5286,29 +5305,38 @@ z.object({
|
|
|
5286
5305
|
/** Opaque base64 cursor (`createdAt|id`) for keyset pagination. */
|
|
5287
5306
|
cursor: z.string().optional(),
|
|
5288
5307
|
status: z.array(changeRequestStatusSchema).optional(),
|
|
5289
|
-
mine: z.boolean().optional()
|
|
5308
|
+
mine: z.boolean().optional(),
|
|
5309
|
+
affectsNodeId: z.string().min(1).optional()
|
|
5290
5310
|
}).optional().default({ limit: 50 });
|
|
5291
5311
|
z.object({
|
|
5292
5312
|
changeRequests: z.array(changeRequestSchema),
|
|
5293
5313
|
nextCursor: z.string().nullable()
|
|
5294
5314
|
});
|
|
5295
|
-
|
|
5315
|
+
const changeRequestPageInputShape = {
|
|
5296
5316
|
page: z.coerce.number().int().min(1).optional().default(1),
|
|
5297
5317
|
pageSize: z.coerce.number().int().min(1).max(100).optional().default(50),
|
|
5298
5318
|
status: z.array(changeRequestStatusSchema).optional(),
|
|
5299
5319
|
mine: z.boolean().optional()
|
|
5320
|
+
};
|
|
5321
|
+
z.object({
|
|
5322
|
+
...changeRequestPageInputShape,
|
|
5323
|
+
affectsNodeId: z.string().min(1).optional()
|
|
5300
5324
|
}).optional().default({
|
|
5301
5325
|
page: 1,
|
|
5302
5326
|
pageSize: 50
|
|
5303
5327
|
});
|
|
5304
|
-
z.object({
|
|
5328
|
+
z.object(changeRequestPageInputShape).optional().default({
|
|
5329
|
+
page: 1,
|
|
5330
|
+
pageSize: 50
|
|
5331
|
+
});
|
|
5332
|
+
const listChangeRequestsPageResponseSchema = z.object({
|
|
5305
5333
|
changeRequests: z.array(changeRequestSchema),
|
|
5306
5334
|
total: z.number().int().nonnegative(),
|
|
5307
5335
|
totalPages: z.number().int().nonnegative(),
|
|
5308
5336
|
page: z.number().int().min(1),
|
|
5309
5337
|
pageSize: z.number().int().min(1).max(100)
|
|
5310
5338
|
});
|
|
5311
|
-
z.object({
|
|
5339
|
+
const changeRequestCountsSchema = z.object({
|
|
5312
5340
|
review: z.number().int().nonnegative(),
|
|
5313
5341
|
changes: z.number().int().nonnegative(),
|
|
5314
5342
|
created: z.number().int().nonnegative(),
|
|
@@ -5316,6 +5344,7 @@ z.object({
|
|
|
5316
5344
|
merged: z.number().int().nonnegative(),
|
|
5317
5345
|
rejected: z.number().int().nonnegative()
|
|
5318
5346
|
});
|
|
5347
|
+
listChangeRequestsPageResponseSchema.extend({ counts: changeRequestCountsSchema });
|
|
5319
5348
|
const SEARCH_SOURCES = [
|
|
5320
5349
|
"records",
|
|
5321
5350
|
"files",
|
|
@@ -5994,8 +6023,75 @@ const reportUnresolvableDocAssets = (slug, body, carried = /* @__PURE__ */ new S
|
|
|
5994
6023
|
if (unresolved.length === 0) return void 0;
|
|
5995
6024
|
return `Doc "${slug}" embeds ${unresolved.length} image(s) that this package does not carry (${unresolved.join(", ")}). Everything else about the doc is intact, but those images will not render on another host. Re-upload them in the Doc editor, or check that the source host can still serve them.`;
|
|
5996
6025
|
};
|
|
6026
|
+
//#endregion
|
|
6027
|
+
//#region ../../packages/busabase-package/src/record-dependencies.ts
|
|
6028
|
+
const recordIdentity = (baseSlug, recordKey) => `${baseSlug}\u0000${recordKey}`;
|
|
6029
|
+
const toRecordKeyArray = (value) => {
|
|
6030
|
+
if (Array.isArray(value)) return value.filter((item) => typeof item === "string" && item.length > 0);
|
|
6031
|
+
if (typeof value === "string" && value.length > 0) return [value];
|
|
6032
|
+
return [];
|
|
6033
|
+
};
|
|
6034
|
+
const collectBases = (nodes) => {
|
|
6035
|
+
const bases = [];
|
|
6036
|
+
for (const node of nodes) {
|
|
6037
|
+
if (node.type === "base") bases.push(node);
|
|
6038
|
+
if (node.type === "folder") bases.push(...collectBases(node.children));
|
|
6039
|
+
}
|
|
6040
|
+
return bases;
|
|
6041
|
+
};
|
|
6042
|
+
const label = (baseSlug, recordKey) => `${baseSlug}/${recordKey}`;
|
|
6043
|
+
/**
|
|
6044
|
+
* Order sample records so every required relation points at an already-created row.
|
|
6045
|
+
*
|
|
6046
|
+
* Optional relations remain a separate linking pass. Required relations cannot: the
|
|
6047
|
+
* Base rejects a record before that later pass if its required value is absent.
|
|
6048
|
+
*/
|
|
6049
|
+
const buildRecordCreateLayers = (nodes) => {
|
|
6050
|
+
const bases = collectBases(nodes);
|
|
6051
|
+
const basesBySlug = new Map(bases.map((base) => [base.slug, base]));
|
|
6052
|
+
const records = /* @__PURE__ */ new Map();
|
|
6053
|
+
for (const base of bases) for (const record of base.records) records.set(recordIdentity(base.slug, record.key), {
|
|
6054
|
+
base,
|
|
6055
|
+
record
|
|
6056
|
+
});
|
|
6057
|
+
const dependencies = /* @__PURE__ */ new Map();
|
|
6058
|
+
for (const planned of records.values()) {
|
|
6059
|
+
const id = recordIdentity(planned.base.slug, planned.record.key);
|
|
6060
|
+
const required = /* @__PURE__ */ new Set();
|
|
6061
|
+
for (const field of planned.base.base.fields) {
|
|
6062
|
+
if (field.type !== "relation" || !field.required) continue;
|
|
6063
|
+
const targetBaseSlug = field.options.targetBaseSlug;
|
|
6064
|
+
if (!targetBaseSlug || !basesBySlug.has(targetBaseSlug)) throw new Error(`Required relation "${planned.base.slug}.${field.slug}" targets missing Base "${targetBaseSlug || "(unset)"}". Nothing was installed.`);
|
|
6065
|
+
const keys = toRecordKeyArray(planned.record.fields[field.slug]);
|
|
6066
|
+
if (keys.length === 0) throw new Error(`Sample record "${label(planned.base.slug, planned.record.key)}" is missing required relation "${field.slug}". Nothing was installed.`);
|
|
6067
|
+
for (const key of keys) {
|
|
6068
|
+
const dependency = recordIdentity(targetBaseSlug, key);
|
|
6069
|
+
if (!records.has(dependency)) throw new Error(`Sample record "${label(planned.base.slug, planned.record.key)}" requires "${targetBaseSlug}/${key}" through relation "${field.slug}", but that target is not in this package. Nothing was installed.`);
|
|
6070
|
+
required.add(dependency);
|
|
6071
|
+
}
|
|
6072
|
+
}
|
|
6073
|
+
dependencies.set(id, required);
|
|
6074
|
+
}
|
|
6075
|
+
const remaining = new Map(records);
|
|
6076
|
+
const created = /* @__PURE__ */ new Set();
|
|
6077
|
+
const layers = [];
|
|
6078
|
+
while (remaining.size > 0) {
|
|
6079
|
+
const ready = [...remaining.entries()].filter(([id]) => [...dependencies.get(id) ?? []].every((dependency) => created.has(dependency))).sort(([, a], [, b]) => `${a.base.slug}\u0000${a.record.key}`.localeCompare(`${b.base.slug}\u0000${b.record.key}`, "en"));
|
|
6080
|
+
if (ready.length === 0) {
|
|
6081
|
+
const cycle = [...remaining.values()].map((item) => label(item.base.slug, item.record.key)).sort((a, b) => a.localeCompare(b, "en"));
|
|
6082
|
+
throw new Error(`Required sample relations contain a cycle involving: ${cycle.join(", ")}. Required relation targets must be creatable first. Nothing was installed.`);
|
|
6083
|
+
}
|
|
6084
|
+
layers.push(ready.map(([, planned]) => planned));
|
|
6085
|
+
for (const [id] of ready) {
|
|
6086
|
+
remaining.delete(id);
|
|
6087
|
+
created.add(id);
|
|
6088
|
+
}
|
|
6089
|
+
}
|
|
6090
|
+
return layers;
|
|
6091
|
+
};
|
|
5997
6092
|
const fieldKey = (baseSlug, fieldSlug) => `${baseSlug}\u0000${fieldSlug}`;
|
|
5998
6093
|
const applyInstall = async (client, plan, options) => {
|
|
6094
|
+
const recordCreateLayers = buildRecordCreateLayers(plan.tree.nodes);
|
|
5999
6095
|
const state = {
|
|
6000
6096
|
targetFolderNodeId: "",
|
|
6001
6097
|
created: {
|
|
@@ -6014,90 +6110,144 @@ const applyInstall = async (client, plan, options) => {
|
|
|
6014
6110
|
const bases = [];
|
|
6015
6111
|
const progress = options.onProgress ?? (() => {});
|
|
6016
6112
|
const app = resolveAppContext(plan, options);
|
|
6017
|
-
|
|
6018
|
-
|
|
6019
|
-
|
|
6020
|
-
|
|
6021
|
-
state.targetFolderNodeId =
|
|
6022
|
-
|
|
6023
|
-
|
|
6024
|
-
|
|
6025
|
-
|
|
6026
|
-
|
|
6027
|
-
|
|
6028
|
-
});
|
|
6029
|
-
state.created.folders++;
|
|
6030
|
-
}
|
|
6031
|
-
await createStructure(client, plan.tree.nodes, state.targetFolderNodeId, options, state, bases, fieldIds, docAssetIds, app);
|
|
6032
|
-
if (app?.rootSkill) {
|
|
6033
|
-
progress("Pass 1/5: installing the app skill…");
|
|
6034
|
-
await createFileTreeNode(client, {
|
|
6035
|
-
type: "skill",
|
|
6036
|
-
slug: app.rootSkill.slug,
|
|
6037
|
-
name: app.rootSkill.name,
|
|
6038
|
-
description: app.rootSkill.description,
|
|
6039
|
-
position: void 0,
|
|
6040
|
-
files: app.rootSkill.files
|
|
6041
|
-
}, state.targetFolderNodeId, options, state, app, { [TEMPLATE_SKILL_METADATA_KEY]: true });
|
|
6042
|
-
}
|
|
6043
|
-
const deferred = bases.filter((base) => base.node.base.fields.some(isDeferredField));
|
|
6044
|
-
if (deferred.length > 0) {
|
|
6045
|
-
progress("Pass 2/5: adding relation and AI fields…");
|
|
6046
|
-
for (const base of deferred) for (const field of base.node.base.fields.filter(isDeferredField)) {
|
|
6047
|
-
const updated = await client.bases.createField({
|
|
6048
|
-
baseId: base.baseId,
|
|
6049
|
-
slug: field.slug,
|
|
6050
|
-
name: field.name,
|
|
6051
|
-
type: field.type,
|
|
6052
|
-
required: field.required,
|
|
6053
|
-
options: toApiFieldOptions(field.options)
|
|
6054
|
-
});
|
|
6055
|
-
indexFields(fieldIds, base.node.slug, updated.fields);
|
|
6056
|
-
}
|
|
6057
|
-
}
|
|
6058
|
-
const patchable = bases.flatMap((base) => base.node.base.fields.filter(needsFieldIdPatch).map((field) => ({
|
|
6059
|
-
base,
|
|
6060
|
-
field
|
|
6061
|
-
})));
|
|
6062
|
-
if (patchable.length > 0) {
|
|
6063
|
-
progress("Pass 3/5: resolving inverse and AI source field references…");
|
|
6064
|
-
for (const { base, field } of patchable) {
|
|
6065
|
-
const fieldId = fieldIds.get(fieldKey(base.node.slug, field.slug));
|
|
6066
|
-
if (!fieldId) throw new Error(`Internal error: no field id for ${base.node.slug}.${field.slug} in pass 3.`);
|
|
6067
|
-
const patched = resolveFieldIdOptions(field, base.node.slug, fieldIds, state.warnings);
|
|
6068
|
-
if (!patched) continue;
|
|
6069
|
-
const changeRequest = await client.bases.fieldChangeRequest({
|
|
6070
|
-
operation: "update",
|
|
6071
|
-
baseId: base.baseId,
|
|
6072
|
-
fieldId,
|
|
6073
|
-
patch: { options: patched },
|
|
6074
|
-
message: `Wire up ${base.node.slug}.${field.slug} references`,
|
|
6113
|
+
let phase = "Pass 1/5 (structure)";
|
|
6114
|
+
try {
|
|
6115
|
+
const docAssetIds = await uploadDocAssets(client, plan.tree.assets ?? [], state, progress, options.serverUrl);
|
|
6116
|
+
progress("Pass 1/5: creating the node tree…");
|
|
6117
|
+
if (plan.existingTargetFolderNodeId) state.targetFolderNodeId = plan.existingTargetFolderNodeId;
|
|
6118
|
+
else {
|
|
6119
|
+
state.targetFolderNodeId = await createFolderNode(client, {
|
|
6120
|
+
parentNodeId: void 0,
|
|
6121
|
+
slug: plan.targetFolderSlug,
|
|
6122
|
+
name: plan.tree.manifest.name,
|
|
6123
|
+
description: plan.tree.manifest.description,
|
|
6075
6124
|
submittedBy: options.submittedBy,
|
|
6076
|
-
|
|
6125
|
+
metadata: app?.rootMetadata
|
|
6077
6126
|
});
|
|
6078
|
-
|
|
6127
|
+
state.created.folders++;
|
|
6079
6128
|
}
|
|
6080
|
-
|
|
6081
|
-
|
|
6082
|
-
|
|
6083
|
-
|
|
6084
|
-
|
|
6085
|
-
|
|
6086
|
-
|
|
6087
|
-
|
|
6088
|
-
|
|
6089
|
-
|
|
6090
|
-
|
|
6091
|
-
|
|
6092
|
-
|
|
6093
|
-
|
|
6129
|
+
await createStructure(client, plan.tree.nodes, state.targetFolderNodeId, options, state, bases, fieldIds, docAssetIds, app);
|
|
6130
|
+
if (app?.rootSkill) {
|
|
6131
|
+
progress("Pass 1/5: installing the app skill…");
|
|
6132
|
+
await createFileTreeNode(client, {
|
|
6133
|
+
type: "skill",
|
|
6134
|
+
slug: app.rootSkill.slug,
|
|
6135
|
+
name: app.rootSkill.name,
|
|
6136
|
+
description: app.rootSkill.description,
|
|
6137
|
+
position: void 0,
|
|
6138
|
+
files: app.rootSkill.files
|
|
6139
|
+
}, state.targetFolderNodeId, options, state, app, { [TEMPLATE_SKILL_METADATA_KEY]: true });
|
|
6140
|
+
}
|
|
6141
|
+
phase = "Pass 2/5 (relation and AI fields)";
|
|
6142
|
+
const deferred = bases.filter((base) => base.node.base.fields.some(isDeferredField));
|
|
6143
|
+
if (deferred.length > 0) {
|
|
6144
|
+
progress("Pass 2/5: adding relation and AI fields…");
|
|
6145
|
+
for (const base of deferred) for (const field of base.node.base.fields.filter(isDeferredField)) {
|
|
6146
|
+
const updated = await client.bases.createField({
|
|
6147
|
+
baseId: base.baseId,
|
|
6148
|
+
slug: field.slug,
|
|
6149
|
+
name: field.name,
|
|
6150
|
+
type: field.type,
|
|
6151
|
+
required: field.required,
|
|
6152
|
+
options: toApiFieldOptions(field.options)
|
|
6153
|
+
});
|
|
6154
|
+
indexFields(fieldIds, base.node.slug, updated.fields);
|
|
6155
|
+
}
|
|
6156
|
+
}
|
|
6157
|
+
phase = "Pass 3/5 (field reference wiring)";
|
|
6158
|
+
const patchable = bases.flatMap((base) => base.node.base.fields.filter(needsFieldIdPatch).map((field) => ({
|
|
6159
|
+
base,
|
|
6160
|
+
field
|
|
6161
|
+
})));
|
|
6162
|
+
if (patchable.length > 0) {
|
|
6163
|
+
progress("Pass 3/5: resolving inverse and AI source field references…");
|
|
6164
|
+
for (const { base, field } of patchable) {
|
|
6165
|
+
const fieldId = fieldIds.get(fieldKey(base.node.slug, field.slug));
|
|
6166
|
+
if (!fieldId) throw new Error(`Internal error: no field id for ${base.node.slug}.${field.slug} in pass 3.`);
|
|
6167
|
+
const patched = resolveFieldIdOptions(field, base.node.slug, fieldIds, state.warnings);
|
|
6168
|
+
if (!patched) continue;
|
|
6169
|
+
const changeRequest = await client.bases.fieldChangeRequest({
|
|
6170
|
+
operation: "update",
|
|
6171
|
+
baseId: base.baseId,
|
|
6172
|
+
fieldId,
|
|
6173
|
+
patch: { options: patched },
|
|
6174
|
+
message: `Wire up ${base.node.slug}.${field.slug} references`,
|
|
6175
|
+
submittedBy: options.submittedBy,
|
|
6176
|
+
autoMerge: true
|
|
6177
|
+
});
|
|
6178
|
+
if (changeRequest.status !== "merged") throw new Error(`Field reference wiring for ${base.node.slug}.${field.slug} was requested immediately but came back "${changeRequest.status}"; install cannot leave a Base's relation fields unwired.`);
|
|
6179
|
+
}
|
|
6180
|
+
}
|
|
6181
|
+
phase = "Pass 4/5 (sample records)";
|
|
6182
|
+
const recordIdsByKey = /* @__PURE__ */ new Map();
|
|
6183
|
+
if (recordCreateLayers.length > 0) {
|
|
6184
|
+
progress("Pass 4/5: creating records…");
|
|
6185
|
+
const basesBySlug = new Map(bases.map((base) => [base.node.slug, base]));
|
|
6186
|
+
for (const layer of recordCreateLayers) {
|
|
6187
|
+
const byBase = /* @__PURE__ */ new Map();
|
|
6188
|
+
for (const planned of layer) {
|
|
6189
|
+
const group = byBase.get(planned.base.slug) ?? [];
|
|
6190
|
+
group.push(planned);
|
|
6191
|
+
byBase.set(planned.base.slug, group);
|
|
6192
|
+
}
|
|
6193
|
+
for (const [baseSlug, planned] of [...byBase].sort(([a], [b]) => a.localeCompare(b, "en"))) {
|
|
6194
|
+
const base = basesBySlug.get(baseSlug);
|
|
6195
|
+
if (!base) throw new Error(`Internal error: no materialized Base for ${baseSlug} in pass 4.`);
|
|
6196
|
+
await createRecords(client, base, planned.map((item) => item.record), plan, options, state, recordIdsByKey, app);
|
|
6197
|
+
}
|
|
6198
|
+
}
|
|
6199
|
+
}
|
|
6200
|
+
phase = "Pass 5/5 (relation links)";
|
|
6201
|
+
const relationFields = /* @__PURE__ */ new Map();
|
|
6094
6202
|
for (const base of bases) {
|
|
6095
|
-
const
|
|
6096
|
-
if (
|
|
6097
|
-
|
|
6203
|
+
const fields = base.node.base.fields.filter((field) => field.type === "relation");
|
|
6204
|
+
if (fields.length > 0) relationFields.set(base.node.slug, fields);
|
|
6205
|
+
}
|
|
6206
|
+
if (relationFields.size > 0) {
|
|
6207
|
+
progress("Pass 5/5: linking relations…");
|
|
6208
|
+
for (const base of bases) {
|
|
6209
|
+
const fields = relationFields.get(base.node.slug);
|
|
6210
|
+
if (!fields) continue;
|
|
6211
|
+
await linkRelations(client, base, fields, recordIdsByKey, options, state);
|
|
6212
|
+
}
|
|
6098
6213
|
}
|
|
6214
|
+
return state;
|
|
6215
|
+
} catch (error) {
|
|
6216
|
+
const created = Object.entries(state.created).filter(([, count]) => count > 0).map(([kind, count]) => `${count} ${kind}`).join(", ");
|
|
6217
|
+
const partial = created ? `Created before failure: ${created}. Target folder: ${plan.targetFolderSlug}.` : "No package resources were created.";
|
|
6218
|
+
const pending = state.pendingChangeRequests ? ` ${state.pendingChangeRequests} ChangeRequest(s) may still be pending.` : "";
|
|
6219
|
+
const reason = error instanceof Error ? error.message : String(error);
|
|
6220
|
+
const message = `Install stopped during ${phase}. ${partial}${pending} Cause: ${reason}`;
|
|
6221
|
+
const details = {
|
|
6222
|
+
phase,
|
|
6223
|
+
targetFolderSlug: plan.targetFolderSlug,
|
|
6224
|
+
created: { ...state.created },
|
|
6225
|
+
pendingChangeRequests: state.pendingChangeRequests
|
|
6226
|
+
};
|
|
6227
|
+
if (error instanceof Error) {
|
|
6228
|
+
error.message = message;
|
|
6229
|
+
throw attachInstallFailureDetails(error, details);
|
|
6230
|
+
}
|
|
6231
|
+
throw attachInstallFailureDetails(new Error(message), details);
|
|
6099
6232
|
}
|
|
6100
|
-
|
|
6233
|
+
};
|
|
6234
|
+
/**
|
|
6235
|
+
* A property rather than a subclass: the whole point of the rethrow above is to
|
|
6236
|
+
* keep the original error's prototype (an `ORPCError`'s `code`/`status`/`data`
|
|
6237
|
+
* are what make a permission or quota failure actionable), so the diagnostics
|
|
6238
|
+
* have to ride along on it instead of replacing it.
|
|
6239
|
+
*/
|
|
6240
|
+
const INSTALL_FAILURE_DETAILS_KEY = "busabaseInstallFailure";
|
|
6241
|
+
const attachInstallFailureDetails = (error, details) => {
|
|
6242
|
+
try {
|
|
6243
|
+
Object.defineProperty(error, INSTALL_FAILURE_DETAILS_KEY, {
|
|
6244
|
+
value: details,
|
|
6245
|
+
enumerable: false,
|
|
6246
|
+
configurable: true,
|
|
6247
|
+
writable: true
|
|
6248
|
+
});
|
|
6249
|
+
} catch {}
|
|
6250
|
+
return error;
|
|
6101
6251
|
};
|
|
6102
6252
|
const resolveAppContext = (plan, options) => {
|
|
6103
6253
|
if (!plan.isTemplate) return void 0;
|
|
@@ -6453,28 +6603,39 @@ const resolveFieldIdOptions = (field, baseSlug, fieldIds, warnings) => {
|
|
|
6453
6603
|
const indexFields = (fieldIds, baseSlug, fields) => {
|
|
6454
6604
|
for (const field of fields) fieldIds.set(fieldKey(baseSlug, field.slug), field.id);
|
|
6455
6605
|
};
|
|
6456
|
-
/**
|
|
6457
|
-
|
|
6458
|
-
|
|
6606
|
+
/**
|
|
6607
|
+
* Values the server computes and optional relations are omitted in pass 4.
|
|
6608
|
+
* Required relations are resolved to canonical ids once their dependency layer exists.
|
|
6609
|
+
*/
|
|
6610
|
+
const toCreatableFields = (node, fields, recordIdsByKey) => {
|
|
6611
|
+
const fieldBySlug = new Map(node.base.fields.map((field) => [field.slug, field]));
|
|
6459
6612
|
const result = {};
|
|
6460
6613
|
for (const [slug, value] of Object.entries(fields)) {
|
|
6461
|
-
const
|
|
6462
|
-
if (!
|
|
6463
|
-
if (type === "relation")
|
|
6464
|
-
|
|
6465
|
-
|
|
6614
|
+
const field = fieldBySlug.get(slug);
|
|
6615
|
+
if (!field) continue;
|
|
6616
|
+
if (field.type === "relation") {
|
|
6617
|
+
if (!field.required) continue;
|
|
6618
|
+
const targetBaseSlug = field.options.targetBaseSlug;
|
|
6619
|
+
const keys = toRecordKeyArray(value);
|
|
6620
|
+
const targetIds = keys.map((key) => targetBaseSlug ? recordIdsByKey.get(recordIdentity(targetBaseSlug, key)) : void 0).filter((id) => Boolean(id));
|
|
6621
|
+
if (targetIds.length !== keys.length) throw new Error(`Internal error: required relation ${node.slug}.${slug} was scheduled before its targets existed.`);
|
|
6622
|
+
result[slug] = targetIds;
|
|
6623
|
+
continue;
|
|
6624
|
+
}
|
|
6625
|
+
if (PACKAGE_COMPUTED_FIELD_TYPES.includes(field.type)) continue;
|
|
6626
|
+
if (field.type === "attachment") continue;
|
|
6466
6627
|
result[slug] = value;
|
|
6467
6628
|
}
|
|
6468
6629
|
return result;
|
|
6469
6630
|
};
|
|
6470
|
-
const createRecords = async (client, base, plan, options, state, recordIdsByKey, app) => {
|
|
6631
|
+
const createRecords = async (client, base, recordsToCreate, plan, options, state, recordIdsByKey, app) => {
|
|
6471
6632
|
const mergeRecords = options.autoMerge || (app?.mergeSampleRecords ?? false);
|
|
6472
|
-
const records = [...
|
|
6633
|
+
const records = [...recordsToCreate].sort((a, b) => a.key.localeCompare(b.key, "en"));
|
|
6473
6634
|
for (let offset = 0; offset < records.length; offset += 200) {
|
|
6474
6635
|
const batch = records.slice(offset, offset + 200);
|
|
6475
6636
|
const changeRequest = await client.bases.createBulkChangeRequest({
|
|
6476
6637
|
baseId: base.baseId,
|
|
6477
|
-
records: batch.map((record) => toCreatableFields(base.node, record.fields)),
|
|
6638
|
+
records: batch.map((record) => toCreatableFields(base.node, record.fields, recordIdsByKey)),
|
|
6478
6639
|
message: `Install ${batch.length} record(s) into ${base.node.slug}`,
|
|
6479
6640
|
submittedBy: options.submittedBy,
|
|
6480
6641
|
idempotencyKey: batchIdempotencyKey(plan.tree.manifest.name, batch[0].key),
|
|
@@ -6487,7 +6648,7 @@ const createRecords = async (client, base, plan, options, state, recordIdsByKey,
|
|
|
6487
6648
|
const created = (await approveAndMerge(client, changeRequest.id)).changeRequest.operations.filter((operation) => operation.operation === "record_create").sort((a, b) => a.position - b.position);
|
|
6488
6649
|
for (const [index, record] of batch.entries()) {
|
|
6489
6650
|
const recordId = created[index]?.mergedRecordId;
|
|
6490
|
-
if (recordId) recordIdsByKey.set(record.key, recordId);
|
|
6651
|
+
if (recordId) recordIdsByKey.set(recordIdentity(base.node.slug, record.key), recordId);
|
|
6491
6652
|
}
|
|
6492
6653
|
state.created.records += batch.length;
|
|
6493
6654
|
}
|
|
@@ -6502,30 +6663,31 @@ const createRecords = async (client, base, plan, options, state, recordIdsByKey,
|
|
|
6502
6663
|
* so the same package always produces the same batches and therefore the same keys.
|
|
6503
6664
|
*/
|
|
6504
6665
|
const batchIdempotencyKey = (packageName, firstRecordKey) => `pkg:${packageName}:${firstRecordKey}`;
|
|
6505
|
-
const linkRelations = async (client, base,
|
|
6666
|
+
const linkRelations = async (client, base, relationFields, recordIdsByKey, options, state) => {
|
|
6506
6667
|
for (const record of base.node.records) {
|
|
6507
6668
|
const relationValues = {};
|
|
6508
|
-
let
|
|
6509
|
-
for (const
|
|
6510
|
-
const value = record.fields[slug];
|
|
6511
|
-
const keys =
|
|
6669
|
+
let needsUpdate = false;
|
|
6670
|
+
for (const field of relationFields) {
|
|
6671
|
+
const value = record.fields[field.slug];
|
|
6672
|
+
const keys = toRecordKeyArray(value);
|
|
6512
6673
|
if (keys.length === 0) continue;
|
|
6513
|
-
const
|
|
6514
|
-
const
|
|
6515
|
-
|
|
6674
|
+
const targetBaseSlug = field.options.targetBaseSlug;
|
|
6675
|
+
const targetIds = keys.map((key) => targetBaseSlug ? recordIdsByKey.get(recordIdentity(targetBaseSlug, key)) : void 0).filter((id) => Boolean(id));
|
|
6676
|
+
const unresolved = keys.filter((key) => !targetBaseSlug || !recordIdsByKey.has(recordIdentity(targetBaseSlug, key)));
|
|
6677
|
+
if (unresolved.length > 0) state.warnings.push(`Record "${record.key}" in base "${base.node.slug}" links field "${field.slug}" to ${unresolved.length} record(s) that are not in target Base "${targetBaseSlug || "(unset)"}" — those links were skipped.`);
|
|
6516
6678
|
if (targetIds.length > 0) {
|
|
6517
|
-
relationValues[slug] = targetIds;
|
|
6518
|
-
|
|
6679
|
+
relationValues[field.slug] = targetIds;
|
|
6680
|
+
if (!field.required) needsUpdate = true;
|
|
6519
6681
|
}
|
|
6520
6682
|
}
|
|
6521
|
-
if (!
|
|
6522
|
-
const recordId = recordIdsByKey.get(record.key);
|
|
6683
|
+
if (!needsUpdate) continue;
|
|
6684
|
+
const recordId = recordIdsByKey.get(recordIdentity(base.node.slug, record.key));
|
|
6523
6685
|
if (!recordId) continue;
|
|
6524
6686
|
const changeRequest = await client.records.changeRequest({
|
|
6525
6687
|
operation: "update",
|
|
6526
6688
|
recordId,
|
|
6527
6689
|
fields: {
|
|
6528
|
-
...toCreatableFields(base.node, record.fields),
|
|
6690
|
+
...toCreatableFields(base.node, record.fields, recordIdsByKey),
|
|
6529
6691
|
...relationValues
|
|
6530
6692
|
},
|
|
6531
6693
|
message: `Link relations for ${base.node.slug}`,
|
|
@@ -6536,11 +6698,6 @@ const linkRelations = async (client, base, relationFieldSlugs, recordIdsByKey, o
|
|
|
6536
6698
|
await approveAndMerge(client, changeRequest.id);
|
|
6537
6699
|
}
|
|
6538
6700
|
};
|
|
6539
|
-
const toKeyArray = (value) => {
|
|
6540
|
-
if (Array.isArray(value)) return value.filter((item) => typeof item === "string" && item.length > 0);
|
|
6541
|
-
if (typeof value === "string" && value.length > 0) return [value];
|
|
6542
|
-
return [];
|
|
6543
|
-
};
|
|
6544
6701
|
/** Merging requires an `approved` status, so every auto-merge is review-then-merge. */
|
|
6545
6702
|
const approveAndMerge = async (client, changeRequestId) => {
|
|
6546
6703
|
const reviewResult = (await client.changeRequests.review({
|
|
@@ -7681,6 +7838,7 @@ const buildInstallPlan = (tree, target, options = {}) => {
|
|
|
7681
7838
|
planned = applyRenames(prefixed, collisions, target.existingNodeSlugsByType);
|
|
7682
7839
|
for (const [installedSlug, originalSlug] of collectSlugRewrites(tree, planned)) resourceKeysBySlug[installedSlug] = originalSlug;
|
|
7683
7840
|
}
|
|
7841
|
+
buildRecordCreateLayers(planned.nodes);
|
|
7684
7842
|
collectWarnings(planned, warnings);
|
|
7685
7843
|
const counts = countTree(planned);
|
|
7686
7844
|
if (counts.files > 5e3) throw new Error(`Package has ${counts.files} files, above the ${PACKAGE_MAX_FILE_COUNT}-file limit. Nothing was installed.`);
|
|
@@ -9188,6 +9346,7 @@ function resolveConfig(opts) {
|
|
|
9188
9346
|
webUrl: process.env.BUSABASE_WEB_URL ?? file.BUSABASE_WEB_URL ?? baseUrl,
|
|
9189
9347
|
apiKey: opts.apiKey ?? process.env.BUSABASE_API_KEY ?? file.BUSABASE_API_KEY,
|
|
9190
9348
|
spaceId: opts.spaceId ?? process.env.BUSABASE_SPACE_ID ?? file.BUSABASE_SPACE_ID,
|
|
9349
|
+
sourceChannel: "cli",
|
|
9191
9350
|
output: opts.output ?? configuredOutput() ?? "text",
|
|
9192
9351
|
fetch: withRetry(fetch, { retries: opts.retries ?? DEFAULT_RETRIES })
|
|
9193
9352
|
};
|
|
@@ -9334,7 +9493,8 @@ async function rawFetch(config, method, path, body) {
|
|
|
9334
9493
|
headers: {
|
|
9335
9494
|
...body !== void 0 ? { "content-type": "application/json" } : {},
|
|
9336
9495
|
...config.apiKey ? { authorization: `Bearer ${config.apiKey}` } : {},
|
|
9337
|
-
...config.spaceId ? { "x-busabase-space": config.spaceId } : {}
|
|
9496
|
+
...config.spaceId ? { "x-busabase-space": config.spaceId } : {},
|
|
9497
|
+
"x-busabase-channel": "cli"
|
|
9338
9498
|
},
|
|
9339
9499
|
body: body !== void 0 ? JSON.stringify(body) : void 0
|
|
9340
9500
|
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "busa-cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.20.0",
|
|
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.20.0",
|
|
37
37
|
"commander": "^14.0.0",
|
|
38
38
|
"tar-stream": "^3.1.7",
|
|
39
39
|
"yaml": "^2.9.0",
|