@cyberxon/xon 0.3.0 → 0.4.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 +36 -12
- package/dist/deploy.js +16 -1
- package/dist/metadata-registry.js +54 -0
- package/dist/org-store.js +10 -2
- package/dist/org.js +3 -2
- package/dist/project.js +25 -2
- package/package.json +3 -2
- package/registry/metadataRegistry.json +5417 -0
package/dist/cli.js
CHANGED
|
@@ -10,8 +10,9 @@ import { cancelDeploy, COVERAGE_FORMATTERS, previewDeploy, quickDeploy, reportDe
|
|
|
10
10
|
import { previewRetrieve, resumeRetrieve, startRetrieve } from "./retrieve.js";
|
|
11
11
|
import { collectSourceFiles, writeSourceFiles } from "./source.js";
|
|
12
12
|
import { displayOrg, loginAccessToken, loginCredentials, loginJwt, loginSfdxUrl, loginWeb, logoutOrg, openInBrowser, openOrg } from "./org.js";
|
|
13
|
-
import { clearOrgs, listOrgs } from "./org-store.js";
|
|
13
|
+
import { clearOrgs, listOrgs, tryGetOrg } from "./org-store.js";
|
|
14
14
|
import { applyFlagsDir } from "./flags-dir.js";
|
|
15
|
+
import { formatMetadataFilter, parseMetadataFilter } from "./metadata-registry.js";
|
|
15
16
|
const cliVersion = JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf8")).version;
|
|
16
17
|
const program = new Command();
|
|
17
18
|
function output(value) {
|
|
@@ -30,7 +31,12 @@ async function apiClient(options) {
|
|
|
30
31
|
if (!baseUrl) {
|
|
31
32
|
throw new Error("An API base URL is required. Set XON_BASE_URL, pass --base-url, or run 'xon org login' inside a project to save one.");
|
|
32
33
|
}
|
|
33
|
-
|
|
34
|
+
let token = options.token ?? process.env.XON_TOKEN;
|
|
35
|
+
if (!token) {
|
|
36
|
+
const org = await tryGetOrg(options.targetOrg ?? project?.defaultOrg);
|
|
37
|
+
token = org?.token;
|
|
38
|
+
}
|
|
39
|
+
return new ApiClient({ baseUrl, token });
|
|
34
40
|
}
|
|
35
41
|
async function requireProjectRoot() {
|
|
36
42
|
try {
|
|
@@ -47,6 +53,20 @@ async function resolveTargetOrg(explicit) {
|
|
|
47
53
|
const project = await tryReadProject(".");
|
|
48
54
|
return project?.defaultOrg;
|
|
49
55
|
}
|
|
56
|
+
async function resolveSourceDirs(explicit) {
|
|
57
|
+
if (explicit.length > 0) {
|
|
58
|
+
return explicit;
|
|
59
|
+
}
|
|
60
|
+
const project = await tryReadProject(".");
|
|
61
|
+
return [project?.sourceDir ?? "src"];
|
|
62
|
+
}
|
|
63
|
+
async function resolveSourceDir(explicit) {
|
|
64
|
+
if (explicit) {
|
|
65
|
+
return explicit;
|
|
66
|
+
}
|
|
67
|
+
const project = await tryReadProject(".");
|
|
68
|
+
return project?.sourceDir ?? "src";
|
|
69
|
+
}
|
|
50
70
|
async function saveProjectLoginDefaults(baseUrl, orgAlias) {
|
|
51
71
|
const project = await tryReadProject(".");
|
|
52
72
|
if (!project) {
|
|
@@ -70,6 +90,7 @@ const parseWait = (value) => {
|
|
|
70
90
|
};
|
|
71
91
|
const DEFAULT_DEPLOY_WAIT_SECONDS = 33 * 60;
|
|
72
92
|
const collect = (value, previous) => previous.concat([value]);
|
|
93
|
+
const collectMetadata = (value, previous) => previous.concat([formatMetadataFilter(parseMetadataFilter(value))]);
|
|
73
94
|
function parseEnum(name, allowed) {
|
|
74
95
|
return (value) => {
|
|
75
96
|
if (!allowed.includes(value)) {
|
|
@@ -114,7 +135,7 @@ function addDeployStartFlags(cmd) {
|
|
|
114
135
|
.option("--json", "Format output as JSON", false)
|
|
115
136
|
.option("--junit", "Output JUnit test results", false)
|
|
116
137
|
.option("-x, --manifest <path>", "Full file path for manifest (package.xml) of components to deploy")
|
|
117
|
-
.option("-m, --metadata <name>", "Metadata component names to deploy (repeatable)",
|
|
138
|
+
.option("-m, --metadata <name>", "Metadata component names to deploy, e.g. ApexClass or ApexClass:MyClass (repeatable)", collectMetadata, [])
|
|
118
139
|
.option("--metadata-dir <path>", "Root of directory of metadata formatted files to deploy")
|
|
119
140
|
.option("--post-destructive-changes <path>", "File path for a manifest of components to delete after the deploy")
|
|
120
141
|
.option("--pre-destructive-changes <path>", "File path for a manifest of components to delete before the deploy")
|
|
@@ -140,8 +161,9 @@ project
|
|
|
140
161
|
.command("create <name>")
|
|
141
162
|
.description("Create a new Xon project")
|
|
142
163
|
.option("-d, --directory <path>", "Directory to create the project in", ".")
|
|
164
|
+
.option("-f, --full", "Scaffold the full force-app/main/default layout instead of a plain src/ folder", false)
|
|
143
165
|
.action(async (name, options) => {
|
|
144
|
-
const root = await createProject(options.directory, name);
|
|
166
|
+
const root = await createProject(options.directory, name, { full: options.full });
|
|
145
167
|
console.log(`Created project ${name} in ${root}`);
|
|
146
168
|
});
|
|
147
169
|
const projectDeploy = project.command("deploy").description("Deploy metadata between your project and an org");
|
|
@@ -155,7 +177,7 @@ addDeployStartFlags(projectDeploy.command("start").description("Deploy metadata
|
|
|
155
177
|
options = command.opts();
|
|
156
178
|
}
|
|
157
179
|
const client = await apiClient(options);
|
|
158
|
-
const sourceDirs = (
|
|
180
|
+
const sourceDirs = (await resolveSourceDirs(options.sourceDir)).map((dir) => path.resolve(dir));
|
|
159
181
|
const status = await startDeploy(client, sourceDirs, {
|
|
160
182
|
targetOrg: await resolveTargetOrg(options.targetOrg),
|
|
161
183
|
checkOnly: options.dryRun,
|
|
@@ -188,7 +210,7 @@ addDeployStartFlags(projectDeploy.command("validate").description("Validate a me
|
|
|
188
210
|
options = command.opts();
|
|
189
211
|
}
|
|
190
212
|
const client = await apiClient(options);
|
|
191
|
-
const sourceDirs = (
|
|
213
|
+
const sourceDirs = (await resolveSourceDirs(options.sourceDir)).map((dir) => path.resolve(dir));
|
|
192
214
|
const status = await startDeploy(client, sourceDirs, {
|
|
193
215
|
targetOrg: await resolveTargetOrg(options.targetOrg),
|
|
194
216
|
checkOnly: true,
|
|
@@ -255,10 +277,11 @@ projectDeploy
|
|
|
255
277
|
projectDeploy
|
|
256
278
|
.command("preview")
|
|
257
279
|
.description("Preview deployment details for the local project source")
|
|
258
|
-
.option("-d, --source-dir <dir>", "Directory of source to preview"
|
|
280
|
+
.option("-d, --source-dir <dir>", "Directory of source to preview")
|
|
259
281
|
.action(async (options) => {
|
|
260
282
|
await requireProjectRoot();
|
|
261
|
-
const
|
|
283
|
+
const sourceDir = await resolveSourceDir(options.sourceDir);
|
|
284
|
+
const files = await collectSourceFiles(path.resolve(sourceDir));
|
|
262
285
|
console.table(previewDeploy(files));
|
|
263
286
|
});
|
|
264
287
|
const projectRetrieve = project.command("retrieve").description("Retrieve metadata from an org into your project");
|
|
@@ -270,8 +293,8 @@ projectRetrieve
|
|
|
270
293
|
.option("-c, --ignore-conflicts", "Ignore conflicts and save files, even if they overwrite local changes", false)
|
|
271
294
|
.option("--json", "Format output as JSON", false)
|
|
272
295
|
.option("-x, --manifest <path>", "File path for the manifest (package.xml) that specifies the components to retrieve")
|
|
273
|
-
.option("-m, --metadata <name>", "Metadata component names to retrieve (repeatable)",
|
|
274
|
-
.option("-r, --output-dir <path>", "Directory root for the retrieved source files"
|
|
296
|
+
.option("-m, --metadata <name>", "Metadata component names to retrieve, e.g. ApexClass or ApexClass:MyClass (repeatable)", collectMetadata, [])
|
|
297
|
+
.option("-r, --output-dir <path>", "Directory root for the retrieved source files")
|
|
275
298
|
.option("-n, --package-name <name>", "Package names to retrieve (repeatable)", collect, [])
|
|
276
299
|
.option("--single-package", "Indicates that the zip file points to a directory structure for a single package", false)
|
|
277
300
|
.option("-d, --source-dir <path>", "File paths for source to retrieve from the org (repeatable)", collect, [])
|
|
@@ -304,8 +327,9 @@ projectRetrieve
|
|
|
304
327
|
result = await resumeRetrieve(client, result.id, options.wait);
|
|
305
328
|
}
|
|
306
329
|
if (result.success && result.files && (options.unzip || !options.targetMetadataDir)) {
|
|
307
|
-
await
|
|
308
|
-
|
|
330
|
+
const outputDir = await resolveSourceDir(options.outputDir);
|
|
331
|
+
await writeSourceFiles(path.resolve(outputDir), result.files);
|
|
332
|
+
console.log(`Retrieved ${result.files.length} file(s) into ${outputDir}`);
|
|
309
333
|
}
|
|
310
334
|
if (result.success && result.metadataZip && options.targetMetadataDir) {
|
|
311
335
|
const zipDir = path.resolve(options.targetMetadataDir);
|
package/dist/deploy.js
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { readFile } from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { findTypeBySuffix } from "./metadata-registry.js";
|
|
2
4
|
import { pollJob } from "./polling.js";
|
|
3
5
|
import { collectSourceFiles } from "./source.js";
|
|
4
6
|
export const TEST_LEVELS = ["NoTestRun", "RunSpecifiedTests", "RunLocalTests", "RunAllTestsInOrg", "RunRelevantTests"];
|
|
@@ -54,6 +56,19 @@ export async function cancelDeploy(client, id) {
|
|
|
54
56
|
export async function quickDeploy(client, id) {
|
|
55
57
|
return client.post(`/deployRequests/${encodeURIComponent(id)}/quickDeploy`, undefined);
|
|
56
58
|
}
|
|
59
|
+
function inferSuffix(filePath) {
|
|
60
|
+
const base = path.basename(filePath);
|
|
61
|
+
const metaMatch = base.match(/\.([A-Za-z0-9]+)-meta\.xml$/);
|
|
62
|
+
if (metaMatch) {
|
|
63
|
+
return metaMatch[1];
|
|
64
|
+
}
|
|
65
|
+
const dotIndex = base.lastIndexOf(".");
|
|
66
|
+
return dotIndex === -1 ? undefined : base.slice(dotIndex + 1);
|
|
67
|
+
}
|
|
57
68
|
export function previewDeploy(files) {
|
|
58
|
-
return files.map((file) =>
|
|
69
|
+
return files.map((file) => {
|
|
70
|
+
const suffix = inferSuffix(file.path);
|
|
71
|
+
const type = suffix ? findTypeBySuffix(suffix)?.name : undefined;
|
|
72
|
+
return { path: file.path, type: type ?? "Unknown", action: "Add" };
|
|
73
|
+
});
|
|
59
74
|
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { readFileSync } from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
4
|
+
function packageRoot() {
|
|
5
|
+
const dist = path.dirname(fileURLToPath(import.meta.url));
|
|
6
|
+
return path.dirname(dist);
|
|
7
|
+
}
|
|
8
|
+
let cached;
|
|
9
|
+
function load() {
|
|
10
|
+
if (cached) {
|
|
11
|
+
return cached;
|
|
12
|
+
}
|
|
13
|
+
const registryPath = path.join(packageRoot(), "registry", "metadataRegistry.json");
|
|
14
|
+
const raw = JSON.parse(readFileSync(registryPath, "utf8"));
|
|
15
|
+
const byName = new Map();
|
|
16
|
+
const byId = new Map();
|
|
17
|
+
for (const def of Object.values(raw.types)) {
|
|
18
|
+
byName.set(def.name.toLowerCase(), def);
|
|
19
|
+
byId.set(def.id, def);
|
|
20
|
+
if (def.children?.types) {
|
|
21
|
+
for (const childDef of Object.values(def.children.types)) {
|
|
22
|
+
const child = { ...childDef, parentId: def.id };
|
|
23
|
+
byName.set(child.name.toLowerCase(), child);
|
|
24
|
+
byId.set(child.id, child);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
cached = { raw, byName, byId };
|
|
29
|
+
return cached;
|
|
30
|
+
}
|
|
31
|
+
export function findTypeByName(name) {
|
|
32
|
+
return load().byName.get(name.trim().toLowerCase());
|
|
33
|
+
}
|
|
34
|
+
export function findTypeBySuffix(suffix) {
|
|
35
|
+
const { raw, byId } = load();
|
|
36
|
+
const typeId = raw.suffixes[suffix] ?? raw.suffixes[suffix.toLowerCase()];
|
|
37
|
+
return typeId ? byId.get(typeId) : undefined;
|
|
38
|
+
}
|
|
39
|
+
export function parseMetadataFilter(raw) {
|
|
40
|
+
const separatorIndex = raw.indexOf(":");
|
|
41
|
+
const typePart = (separatorIndex === -1 ? raw : raw.slice(0, separatorIndex)).trim();
|
|
42
|
+
const namePart = separatorIndex === -1 ? undefined : raw.slice(separatorIndex + 1).trim();
|
|
43
|
+
const def = findTypeByName(typePart);
|
|
44
|
+
if (!def) {
|
|
45
|
+
throw new Error(`Unknown metadata type '${typePart}'. Check the Metadata API name (e.g. ApexClass, CustomObject, Flow).`);
|
|
46
|
+
}
|
|
47
|
+
if (namePart !== undefined && namePart.length === 0) {
|
|
48
|
+
throw new Error(`Invalid --metadata value '${raw}': expected '${def.name}' or '${def.name}:<name>'.`);
|
|
49
|
+
}
|
|
50
|
+
return { type: def.name, name: namePart };
|
|
51
|
+
}
|
|
52
|
+
export function formatMetadataFilter(filter) {
|
|
53
|
+
return filter.name ? `${filter.type}:${filter.name}` : filter.type;
|
|
54
|
+
}
|
package/dist/org-store.js
CHANGED
|
@@ -13,8 +13,8 @@ async function readStore() {
|
|
|
13
13
|
}
|
|
14
14
|
}
|
|
15
15
|
async function writeStore(data) {
|
|
16
|
-
await mkdir(storeDir, { recursive: true });
|
|
17
|
-
await writeFile(storePath, `${JSON.stringify(data, null, 2)}\n
|
|
16
|
+
await mkdir(storeDir, { recursive: true, mode: 0o700 });
|
|
17
|
+
await writeFile(storePath, `${JSON.stringify(data, null, 2)}\n`, { mode: 0o600 });
|
|
18
18
|
}
|
|
19
19
|
export async function saveOrg(record, setDefault) {
|
|
20
20
|
const data = await readStore();
|
|
@@ -54,3 +54,11 @@ export async function listOrgs() {
|
|
|
54
54
|
const data = await readStore();
|
|
55
55
|
return { orgs: Object.values(data.orgs), defaultOrg: data.defaultOrg };
|
|
56
56
|
}
|
|
57
|
+
export async function tryGetOrg(alias) {
|
|
58
|
+
try {
|
|
59
|
+
return await getOrg(alias);
|
|
60
|
+
}
|
|
61
|
+
catch {
|
|
62
|
+
return undefined;
|
|
63
|
+
}
|
|
64
|
+
}
|
package/dist/org.js
CHANGED
|
@@ -9,13 +9,14 @@ export function openInBrowser(url) {
|
|
|
9
9
|
: `xdg-open "${url}"`;
|
|
10
10
|
exec(command);
|
|
11
11
|
}
|
|
12
|
-
async function finishLogin(response, alias, setDefault) {
|
|
12
|
+
async function finishLogin(response, alias, setDefault, tokenOverride) {
|
|
13
13
|
const record = {
|
|
14
14
|
alias: alias ?? response.username,
|
|
15
15
|
orgId: response.orgId,
|
|
16
16
|
username: response.username,
|
|
17
17
|
instanceUrl: response.instanceUrl,
|
|
18
18
|
connectedAt: new Date().toISOString(),
|
|
19
|
+
token: tokenOverride ?? response.token,
|
|
19
20
|
};
|
|
20
21
|
await saveOrg(record, setDefault);
|
|
21
22
|
return record;
|
|
@@ -56,7 +57,7 @@ export async function loginAccessToken(client, options) {
|
|
|
56
57
|
instanceUrl: options.instanceUrl,
|
|
57
58
|
accessToken: options.accessToken,
|
|
58
59
|
});
|
|
59
|
-
return finishLogin(response, options.alias, options.setDefault);
|
|
60
|
+
return finishLogin(response, options.alias, options.setDefault, options.accessToken);
|
|
60
61
|
}
|
|
61
62
|
export async function loginCredentials(client, options) {
|
|
62
63
|
const { token } = await client.post("/auth/login", {
|
package/dist/project.js
CHANGED
|
@@ -1,15 +1,38 @@
|
|
|
1
1
|
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
const projectFile = ".xon/project.json";
|
|
4
|
-
|
|
4
|
+
const DEFAULT_SOURCE_DIR = "src";
|
|
5
|
+
const FULL_SOURCE_DIR = "force-app/main/default";
|
|
6
|
+
const FULL_METADATA_DIRS = [
|
|
7
|
+
"applications",
|
|
8
|
+
"aura",
|
|
9
|
+
"classes",
|
|
10
|
+
"contentassets",
|
|
11
|
+
"flexipages",
|
|
12
|
+
"layouts",
|
|
13
|
+
"lwc",
|
|
14
|
+
"objects",
|
|
15
|
+
"permissionsets",
|
|
16
|
+
"staticresources",
|
|
17
|
+
"tabs",
|
|
18
|
+
"triggers",
|
|
19
|
+
];
|
|
20
|
+
export async function createProject(directory, name, options = {}) {
|
|
5
21
|
const root = path.resolve(directory, name);
|
|
22
|
+
const sourceDir = options.full ? FULL_SOURCE_DIR : DEFAULT_SOURCE_DIR;
|
|
6
23
|
const config = {
|
|
7
24
|
name,
|
|
8
25
|
apiVersion: "v1",
|
|
9
26
|
createdAt: new Date().toISOString(),
|
|
27
|
+
sourceDir,
|
|
10
28
|
};
|
|
11
29
|
await mkdir(path.join(root, ".xon"), { recursive: true });
|
|
12
|
-
|
|
30
|
+
if (options.full) {
|
|
31
|
+
await Promise.all(FULL_METADATA_DIRS.map((dir) => mkdir(path.join(root, sourceDir, dir), { recursive: true })));
|
|
32
|
+
}
|
|
33
|
+
else {
|
|
34
|
+
await mkdir(path.join(root, sourceDir), { recursive: true });
|
|
35
|
+
}
|
|
13
36
|
await writeFile(path.join(root, projectFile), `${JSON.stringify(config, null, 2)}\n`, { flag: "wx" });
|
|
14
37
|
return root;
|
|
15
38
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cyberxon/xon",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"description": "A CLI for Xon projects and deployments",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "UNLICENSED",
|
|
@@ -16,7 +16,8 @@
|
|
|
16
16
|
"node": ">=18"
|
|
17
17
|
},
|
|
18
18
|
"files": [
|
|
19
|
-
"dist"
|
|
19
|
+
"dist",
|
|
20
|
+
"registry"
|
|
20
21
|
],
|
|
21
22
|
"bin": {
|
|
22
23
|
"xon": "dist/cli.js"
|