@cyberxon/xon 0.1.0 → 0.2.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,12 +1,25 @@
1
1
  #!/usr/bin/env node
2
+ import path from "node:path";
2
3
  import { Command, CommanderError } from "commander";
3
4
  import { ApiClient, ApiError } from "./api-client.js";
4
- import { createProject } from "./project.js";
5
+ import { createProject, readProject } from "./project.js";
5
6
  import { updateCli } from "./update.js";
7
+ import { cancelDeploy, previewDeploy, quickDeploy, reportDeploy, resumeDeploy, startDeploy } from "./deploy.js";
8
+ import { previewRetrieve, resumeRetrieve, startRetrieve } from "./retrieve.js";
9
+ import { collectSourceFiles, writeSourceFiles } from "./source.js";
10
+ import { displayOrg, loginAccessToken, loginJwt, loginSfdxUrl, loginWeb, logoutOrg, openInBrowser, openOrg } from "./org.js";
11
+ import { clearOrgs, listOrgs } from "./org-store.js";
6
12
  const program = new Command();
7
13
  function output(value) {
8
14
  console.log(typeof value === "string" ? value : JSON.stringify(value, null, 2));
9
15
  }
16
+ async function readStdin() {
17
+ const chunks = [];
18
+ for await (const chunk of process.stdin) {
19
+ chunks.push(chunk);
20
+ }
21
+ return Buffer.concat(chunks).toString("utf8").trim();
22
+ }
10
23
  function apiClient(options) {
11
24
  const baseUrl = options.baseUrl ?? process.env.XON_BASE_URL;
12
25
  if (!baseUrl) {
@@ -14,6 +27,27 @@ function apiClient(options) {
14
27
  }
15
28
  return new ApiClient({ baseUrl, token: options.token ?? process.env.XON_TOKEN });
16
29
  }
30
+ async function requireProjectRoot() {
31
+ try {
32
+ await readProject(".");
33
+ }
34
+ catch {
35
+ throw new Error("Not in a xon project directory (missing .xon/project.json). Run this inside a project created with 'xon project create'.");
36
+ }
37
+ }
38
+ async function waitForDeploy(client, status, waitSeconds) {
39
+ if (waitSeconds > 0 && !status.done) {
40
+ return resumeDeploy(client, status.id, waitSeconds);
41
+ }
42
+ return status;
43
+ }
44
+ const parseWait = (value) => {
45
+ const parsed = Number.parseInt(value, 10);
46
+ if (Number.isNaN(parsed) || parsed < 0) {
47
+ throw new Error("--wait must be a non-negative number of seconds");
48
+ }
49
+ return parsed;
50
+ };
17
51
  program
18
52
  .name("xon")
19
53
  .description("Manage Xon projects and deployments")
@@ -28,6 +62,248 @@ project
28
62
  const root = await createProject(options.directory, name);
29
63
  console.log(`Created project ${name} in ${root}`);
30
64
  });
65
+ const projectDeploy = project.command("deploy").description("Deploy metadata between your project and an org");
66
+ projectDeploy
67
+ .command("start")
68
+ .description("Deploy metadata to an org from your local project")
69
+ .option("-d, --source-dir <dir>", "Directory of source to deploy", "src")
70
+ .option("-o, --target-org <org>", "Org to deploy to")
71
+ .option("-c, --dry-run", "Validate the deploy without applying it", false)
72
+ .option("-w, --wait <seconds>", "Seconds to wait for the deploy to finish (0 = don't wait)", parseWait, 0)
73
+ .option("-b, --base-url <url>", "API base URL")
74
+ .option("-t, --token <token>", "Bearer token")
75
+ .action(async (options) => {
76
+ await requireProjectRoot();
77
+ const client = apiClient(options);
78
+ const status = await startDeploy(client, path.resolve(options.sourceDir), {
79
+ targetOrg: options.targetOrg,
80
+ checkOnly: options.dryRun,
81
+ });
82
+ output(await waitForDeploy(client, status, options.wait));
83
+ });
84
+ projectDeploy
85
+ .command("validate")
86
+ .description("Validate a metadata deployment without actually executing it")
87
+ .option("-d, --source-dir <dir>", "Directory of source to validate", "src")
88
+ .option("-o, --target-org <org>", "Org to validate against")
89
+ .option("-w, --wait <seconds>", "Seconds to wait for validation to finish (0 = don't wait)", parseWait, 0)
90
+ .option("-b, --base-url <url>", "API base URL")
91
+ .option("-t, --token <token>", "Bearer token")
92
+ .action(async (options) => {
93
+ await requireProjectRoot();
94
+ const client = apiClient(options);
95
+ const status = await startDeploy(client, path.resolve(options.sourceDir), {
96
+ targetOrg: options.targetOrg,
97
+ checkOnly: true,
98
+ });
99
+ output(await waitForDeploy(client, status, options.wait));
100
+ });
101
+ projectDeploy
102
+ .command("quick")
103
+ .description("Quickly deploy a validated deployment to an org")
104
+ .requiredOption("-i, --job-id <id>", "Id of the validated deploy to promote")
105
+ .option("-w, --wait <seconds>", "Seconds to wait for the deploy to finish (0 = don't wait)", parseWait, 0)
106
+ .option("-b, --base-url <url>", "API base URL")
107
+ .option("-t, --token <token>", "Bearer token")
108
+ .action(async (options) => {
109
+ const client = apiClient(options);
110
+ const status = await quickDeploy(client, options.jobId);
111
+ output(await waitForDeploy(client, status, options.wait));
112
+ });
113
+ projectDeploy
114
+ .command("report")
115
+ .description("Check or poll for the status of a deploy operation")
116
+ .requiredOption("-i, --job-id <id>", "Id of the deploy to check")
117
+ .option("-b, --base-url <url>", "API base URL")
118
+ .option("-t, --token <token>", "Bearer token")
119
+ .action(async (options) => {
120
+ output(await reportDeploy(apiClient(options), options.jobId));
121
+ });
122
+ projectDeploy
123
+ .command("resume")
124
+ .description("Resume watching a deployment until it finishes")
125
+ .requiredOption("-i, --job-id <id>", "Id of the deploy to resume watching")
126
+ .option("-w, --wait <seconds>", "Seconds to wait for the deploy to finish", parseWait, 60)
127
+ .option("-b, --base-url <url>", "API base URL")
128
+ .option("-t, --token <token>", "Bearer token")
129
+ .action(async (options) => {
130
+ output(await resumeDeploy(apiClient(options), options.jobId, options.wait));
131
+ });
132
+ projectDeploy
133
+ .command("cancel")
134
+ .description("Cancel a deploy operation")
135
+ .requiredOption("-i, --job-id <id>", "Id of the deploy to cancel")
136
+ .option("-b, --base-url <url>", "API base URL")
137
+ .option("-t, --token <token>", "Bearer token")
138
+ .action(async (options) => {
139
+ output(await cancelDeploy(apiClient(options), options.jobId));
140
+ });
141
+ projectDeploy
142
+ .command("preview")
143
+ .description("Preview deployment details for the local project source")
144
+ .option("-d, --source-dir <dir>", "Directory of source to preview", "src")
145
+ .action(async (options) => {
146
+ await requireProjectRoot();
147
+ const files = await collectSourceFiles(path.resolve(options.sourceDir));
148
+ console.table(previewDeploy(files));
149
+ });
150
+ const projectRetrieve = project.command("retrieve").description("Retrieve metadata from an org into your project");
151
+ projectRetrieve
152
+ .command("start")
153
+ .description("Retrieve metadata from an org to your local project")
154
+ .option("-d, --source-dir <dir>", "Directory to write retrieved source into", "src")
155
+ .option("-o, --target-org <org>", "Org to retrieve from")
156
+ .option("-w, --wait <seconds>", "Seconds to wait for the retrieve to finish", parseWait, 60)
157
+ .option("-b, --base-url <url>", "API base URL")
158
+ .option("-t, --token <token>", "Bearer token")
159
+ .action(async (options) => {
160
+ await requireProjectRoot();
161
+ const client = apiClient(options);
162
+ let result = await startRetrieve(client, options.targetOrg);
163
+ if (options.wait > 0 && !result.done) {
164
+ result = await resumeRetrieve(client, result.id, options.wait);
165
+ }
166
+ if (result.success && result.files) {
167
+ await writeSourceFiles(path.resolve(options.sourceDir), result.files);
168
+ console.log(`Retrieved ${result.files.length} file(s) into ${options.sourceDir}`);
169
+ }
170
+ const { files, ...summary } = result;
171
+ output(summary);
172
+ });
173
+ projectRetrieve
174
+ .command("preview")
175
+ .description("Preview retrieval details including potential conflicts")
176
+ .option("-o, --target-org <org>", "Org to preview retrieval from")
177
+ .option("-b, --base-url <url>", "API base URL")
178
+ .option("-t, --token <token>", "Bearer token")
179
+ .action(async (options) => {
180
+ const rows = await previewRetrieve(apiClient(options), options.targetOrg);
181
+ console.table(rows);
182
+ });
183
+ const org = program.command("org").description("Manage your Salesforce org connections");
184
+ const orgLogin = org.command("login").description("Log in to a Salesforce org");
185
+ orgLogin
186
+ .command("web")
187
+ .description("Log in to an org using the web server flow")
188
+ .option("-r, --instance-url <url>", "Login/instance URL (use https://test.salesforce.com for sandboxes, or a My Domain URL)", "https://login.salesforce.com")
189
+ .option("-a, --alias <alias>", "Alias to save this org connection under")
190
+ .option("-d, --set-default", "Set this org as the default", false)
191
+ .option("-w, --wait <seconds>", "Seconds to wait for the browser login to complete", parseWait, 300)
192
+ .option("-b, --base-url <url>", "API base URL")
193
+ .option("-t, --token <token>", "Bearer token")
194
+ .action(async (options) => {
195
+ const record = await loginWeb(apiClient(options), {
196
+ alias: options.alias,
197
+ setDefault: options.setDefault,
198
+ waitSeconds: options.wait,
199
+ instanceUrl: options.instanceUrl,
200
+ });
201
+ console.log(`Logged in to ${record.instanceUrl} as ${record.username} (alias: ${record.alias})`);
202
+ });
203
+ orgLogin
204
+ .command("jwt")
205
+ .description("Log in to an org using a JSON web token (JWT)")
206
+ .requiredOption("-u, --username <username>", "Username of the org")
207
+ .requiredOption("-i, --client-id <id>", "Connected app consumer key")
208
+ .requiredOption("-f, --jwt-key-file <path>", "Path to the JWT private key file")
209
+ .option("-r, --instance-url <url>", "Login/instance URL", "https://login.salesforce.com")
210
+ .option("-a, --alias <alias>", "Alias to save this org connection under")
211
+ .option("-d, --set-default", "Set this org as the default", false)
212
+ .option("-b, --base-url <url>", "API base URL")
213
+ .option("-t, --token <token>", "Bearer token")
214
+ .action(async (options) => {
215
+ const record = await loginJwt(apiClient(options), options);
216
+ console.log(`Logged in to ${record.instanceUrl} as ${record.username} (alias: ${record.alias})`);
217
+ });
218
+ orgLogin
219
+ .command("sfdx-url")
220
+ .description("Authorize an org using a Salesforce DX authorization URL")
221
+ .option("-f, --sfdx-url-file <path>", "File containing the sfdx auth URL")
222
+ .option("--sfdx-url-stdin", "Read the sfdx auth URL from stdin", false)
223
+ .option("-a, --alias <alias>", "Alias to save this org connection under")
224
+ .option("-d, --set-default", "Set this org as the default", false)
225
+ .option("-b, --base-url <url>", "API base URL")
226
+ .option("-t, --token <token>", "Bearer token")
227
+ .action(async (options) => {
228
+ const sfdxAuthUrl = options.sfdxUrlStdin
229
+ ? await readStdin()
230
+ : options.sfdxUrlFile
231
+ ? (await (await import("node:fs/promises")).readFile(options.sfdxUrlFile, "utf8")).trim()
232
+ : (() => {
233
+ throw new Error("Pass --sfdx-url-file <path> or --sfdx-url-stdin.");
234
+ })();
235
+ const record = await loginSfdxUrl(apiClient(options), { sfdxAuthUrl, alias: options.alias, setDefault: options.setDefault });
236
+ console.log(`Logged in to ${record.instanceUrl} as ${record.username} (alias: ${record.alias})`);
237
+ });
238
+ orgLogin
239
+ .command("access-token")
240
+ .description("Authorize an org using an existing Salesforce access token")
241
+ .requiredOption("-r, --instance-url <url>", "Instance URL of the org")
242
+ .option("-a, --alias <alias>", "Alias to save this org connection under")
243
+ .option("-d, --set-default", "Set this org as the default", false)
244
+ .option("-b, --base-url <url>", "API base URL")
245
+ .option("-t, --token <token>", "Bearer token")
246
+ .action(async (options) => {
247
+ const accessToken = await readStdin();
248
+ if (!accessToken) {
249
+ throw new Error("Pipe the access token via stdin, e.g. echo $TOKEN | xon org login access-token -r <url>.");
250
+ }
251
+ const record = await loginAccessToken(apiClient(options), {
252
+ instanceUrl: options.instanceUrl,
253
+ accessToken,
254
+ alias: options.alias,
255
+ setDefault: options.setDefault,
256
+ });
257
+ console.log(`Logged in to ${record.instanceUrl} as ${record.username} (alias: ${record.alias})`);
258
+ });
259
+ org
260
+ .command("logout")
261
+ .description("Log out of a Salesforce org")
262
+ .option("-o, --target-org <org>", "Alias of the org to log out of")
263
+ .option("-a, --all", "Log out of all connected orgs", false)
264
+ .option("-b, --base-url <url>", "API base URL")
265
+ .option("-t, --token <token>", "Bearer token")
266
+ .action(async (options) => {
267
+ const client = apiClient(options);
268
+ if (options.all) {
269
+ const { orgs } = await listOrgs();
270
+ for (const record of orgs) {
271
+ await logoutOrg(client, record.alias);
272
+ console.log(`Logged out of ${record.alias}`);
273
+ }
274
+ await clearOrgs();
275
+ return;
276
+ }
277
+ const record = await logoutOrg(client, options.targetOrg);
278
+ console.log(`Logged out of ${record.alias}`);
279
+ });
280
+ org
281
+ .command("display")
282
+ .description("Display information about an org")
283
+ .option("-o, --target-org <org>", "Alias of the org to display")
284
+ .option("-b, --base-url <url>", "API base URL")
285
+ .option("-t, --token <token>", "Bearer token")
286
+ .action(async (options) => {
287
+ output(await displayOrg(apiClient(options), options.targetOrg));
288
+ });
289
+ org
290
+ .command("open")
291
+ .description("Open your default org, or another specified org, in a browser")
292
+ .option("-o, --target-org <org>", "Alias of the org to open")
293
+ .option("-p, --path <path>", "Path to navigate to after login")
294
+ .option("-r, --url-only", "Print the URL instead of opening a browser", false)
295
+ .option("-b, --base-url <url>", "API base URL")
296
+ .option("-t, --token <token>", "Bearer token")
297
+ .action(async (options) => {
298
+ const { url, org: record } = await openOrg(apiClient(options), { alias: options.targetOrg, path: options.path });
299
+ if (options.urlOnly) {
300
+ console.log(url);
301
+ }
302
+ else {
303
+ openInBrowser(url);
304
+ console.log(`Opening ${record.alias} (${record.instanceUrl})`);
305
+ }
306
+ });
31
307
  program
32
308
  .command("update")
33
309
  .description("Update xon to the latest version")
package/dist/deploy.js ADDED
@@ -0,0 +1,25 @@
1
+ import { pollJob } from "./polling.js";
2
+ import { collectSourceFiles } from "./source.js";
3
+ export async function startDeploy(client, sourceDir, options) {
4
+ const source = await collectSourceFiles(sourceDir);
5
+ return client.post("/deployRequests", {
6
+ source,
7
+ targetOrg: options.targetOrg,
8
+ checkOnly: options.checkOnly ?? false,
9
+ });
10
+ }
11
+ export async function reportDeploy(client, id) {
12
+ return client.get(`/deployRequests/${encodeURIComponent(id)}`);
13
+ }
14
+ export async function resumeDeploy(client, id, waitSeconds) {
15
+ return pollJob(client, `/deployRequests/${encodeURIComponent(id)}`, { waitSeconds });
16
+ }
17
+ export async function cancelDeploy(client, id) {
18
+ return client.post(`/deployRequests/${encodeURIComponent(id)}/cancel`, undefined);
19
+ }
20
+ export async function quickDeploy(client, id) {
21
+ return client.post(`/deployRequests/${encodeURIComponent(id)}/quickDeploy`, undefined);
22
+ }
23
+ export function previewDeploy(files) {
24
+ return files.map((file) => ({ path: file.path, action: "Add" }));
25
+ }
@@ -0,0 +1,56 @@
1
+ import { mkdir, readFile, writeFile } from "node:fs/promises";
2
+ import os from "node:os";
3
+ import path from "node:path";
4
+ const storeDir = path.join(os.homedir(), ".xon");
5
+ const storePath = path.join(storeDir, "orgs.json");
6
+ async function readStore() {
7
+ try {
8
+ const contents = await readFile(storePath, "utf8");
9
+ return JSON.parse(contents);
10
+ }
11
+ catch {
12
+ return { orgs: {} };
13
+ }
14
+ }
15
+ async function writeStore(data) {
16
+ await mkdir(storeDir, { recursive: true });
17
+ await writeFile(storePath, `${JSON.stringify(data, null, 2)}\n`);
18
+ }
19
+ export async function saveOrg(record, setDefault) {
20
+ const data = await readStore();
21
+ data.orgs[record.alias] = record;
22
+ if (setDefault || !data.defaultOrg) {
23
+ data.defaultOrg = record.alias;
24
+ }
25
+ await writeStore(data);
26
+ }
27
+ export async function removeOrg(alias) {
28
+ const data = await readStore();
29
+ delete data.orgs[alias];
30
+ if (data.defaultOrg === alias) {
31
+ delete data.defaultOrg;
32
+ }
33
+ await writeStore(data);
34
+ }
35
+ export async function clearOrgs() {
36
+ const data = await readStore();
37
+ const removed = Object.values(data.orgs);
38
+ await writeStore({ orgs: {} });
39
+ return removed;
40
+ }
41
+ export async function getOrg(alias) {
42
+ const data = await readStore();
43
+ const key = alias ?? data.defaultOrg;
44
+ if (!key) {
45
+ throw new Error("No target org specified and no default org is set. Pass -o/--target-org or run 'xon org login' with --set-default.");
46
+ }
47
+ const record = data.orgs[key];
48
+ if (!record) {
49
+ throw new Error(`No org connection found for '${key}'. Run 'xon org login' to connect it.`);
50
+ }
51
+ return record;
52
+ }
53
+ export async function listOrgs() {
54
+ const data = await readStore();
55
+ return { orgs: Object.values(data.orgs), defaultOrg: data.defaultOrg };
56
+ }
package/dist/org.js ADDED
@@ -0,0 +1,78 @@
1
+ import { exec } from "node:child_process";
2
+ import { readFile } from "node:fs/promises";
3
+ import { getOrg, removeOrg, saveOrg } from "./org-store.js";
4
+ export function openInBrowser(url) {
5
+ const command = process.platform === "darwin"
6
+ ? `open "${url}"`
7
+ : process.platform === "win32"
8
+ ? `start "" "${url}"`
9
+ : `xdg-open "${url}"`;
10
+ exec(command);
11
+ }
12
+ async function finishLogin(response, alias, setDefault) {
13
+ const record = {
14
+ alias: alias ?? response.username,
15
+ orgId: response.orgId,
16
+ username: response.username,
17
+ instanceUrl: response.instanceUrl,
18
+ connectedAt: new Date().toISOString(),
19
+ };
20
+ await saveOrg(record, setDefault);
21
+ return record;
22
+ }
23
+ export async function loginWeb(client, options) {
24
+ const { loginId, authUrl } = await client.post("/orgs/login/web", {
25
+ instanceUrl: options.instanceUrl,
26
+ });
27
+ openInBrowser(authUrl);
28
+ console.log(`Opening browser to complete login. If it didn't open, visit:\n${authUrl}`);
29
+ const deadline = Date.now() + options.waitSeconds * 1000;
30
+ let result = await client.get(`/orgs/login/${encodeURIComponent(loginId)}`);
31
+ while (!result.done && Date.now() < deadline) {
32
+ await new Promise((resolve) => setTimeout(resolve, 2000));
33
+ result = await client.get(`/orgs/login/${encodeURIComponent(loginId)}`);
34
+ }
35
+ if (!result.done || !result.orgId || !result.username || !result.instanceUrl) {
36
+ throw new Error("Timed out waiting for the browser login to complete.");
37
+ }
38
+ return finishLogin(result, options.alias, options.setDefault);
39
+ }
40
+ export async function loginJwt(client, options) {
41
+ const privateKey = await readFile(options.jwtKeyFile, "utf8");
42
+ const response = await client.post("/orgs/login/jwt", {
43
+ username: options.username,
44
+ clientId: options.clientId,
45
+ instanceUrl: options.instanceUrl,
46
+ privateKey,
47
+ });
48
+ return finishLogin(response, options.alias, options.setDefault);
49
+ }
50
+ export async function loginSfdxUrl(client, options) {
51
+ const response = await client.post("/orgs/login/sfdx-url", { sfdxAuthUrl: options.sfdxAuthUrl });
52
+ return finishLogin(response, options.alias, options.setDefault);
53
+ }
54
+ export async function loginAccessToken(client, options) {
55
+ const response = await client.post("/orgs/login/access-token", {
56
+ instanceUrl: options.instanceUrl,
57
+ accessToken: options.accessToken,
58
+ });
59
+ return finishLogin(response, options.alias, options.setDefault);
60
+ }
61
+ export async function logoutOrg(client, alias) {
62
+ const record = await getOrg(alias);
63
+ await client.post(`/orgs/${encodeURIComponent(record.alias)}/logout`, undefined);
64
+ await removeOrg(record.alias);
65
+ return record;
66
+ }
67
+ export async function displayOrg(client, alias) {
68
+ const record = await getOrg(alias);
69
+ const remote = await client.get(`/orgs/${encodeURIComponent(record.alias)}`);
70
+ return { ...record, ...remote };
71
+ }
72
+ export async function openOrg(client, options) {
73
+ const record = await getOrg(options.alias);
74
+ const { url } = await client.post(`/orgs/${encodeURIComponent(record.alias)}/frontDoorUrl`, {
75
+ path: options.path,
76
+ });
77
+ return { url, org: record };
78
+ }
@@ -0,0 +1,10 @@
1
+ export async function pollJob(client, path, options) {
2
+ const interval = options.intervalMs ?? 2000;
3
+ const deadline = Date.now() + options.waitSeconds * 1000;
4
+ let status = await client.get(path);
5
+ while (!status.done && Date.now() < deadline) {
6
+ await new Promise((resolve) => setTimeout(resolve, interval));
7
+ status = await client.get(path);
8
+ }
9
+ return status;
10
+ }
@@ -0,0 +1,11 @@
1
+ import { pollJob } from "./polling.js";
2
+ export async function startRetrieve(client, targetOrg) {
3
+ return client.post("/retrieveRequests", { targetOrg });
4
+ }
5
+ export async function resumeRetrieve(client, id, waitSeconds) {
6
+ return pollJob(client, `/retrieveRequests/${encodeURIComponent(id)}`, { waitSeconds });
7
+ }
8
+ export async function previewRetrieve(client, targetOrg) {
9
+ const query = targetOrg ? `?targetOrg=${encodeURIComponent(targetOrg)}` : "";
10
+ return client.get(`/retrieveRequests/preview${query}`);
11
+ }
package/dist/source.js ADDED
@@ -0,0 +1,28 @@
1
+ import { mkdir, readFile, readdir, writeFile } from "node:fs/promises";
2
+ import path from "node:path";
3
+ export async function collectSourceFiles(sourceDir) {
4
+ const files = [];
5
+ async function walk(dir) {
6
+ const entries = await readdir(dir, { withFileTypes: true });
7
+ for (const entry of entries) {
8
+ const fullPath = path.join(dir, entry.name);
9
+ if (entry.isDirectory()) {
10
+ await walk(fullPath);
11
+ }
12
+ else if (entry.isFile()) {
13
+ const relative = path.relative(sourceDir, fullPath).split(path.sep).join("/");
14
+ const content = await readFile(fullPath);
15
+ files.push({ path: relative, content: content.toString("base64") });
16
+ }
17
+ }
18
+ }
19
+ await walk(sourceDir);
20
+ return files;
21
+ }
22
+ export async function writeSourceFiles(sourceDir, files) {
23
+ for (const file of files) {
24
+ const target = path.join(sourceDir, ...file.path.split("/"));
25
+ await mkdir(path.dirname(target), { recursive: true });
26
+ await writeFile(target, Buffer.from(file.content, "base64"));
27
+ }
28
+ }
package/dist/update.js CHANGED
@@ -1,10 +1,16 @@
1
1
  import { spawn } from "node:child_process";
2
+ import { existsSync } from "node:fs";
3
+ import { readFile } from "node:fs/promises";
2
4
  import { fileURLToPath } from "node:url";
3
5
  import path from "node:path";
4
6
  function packageRoot() {
5
7
  const dist = path.dirname(fileURLToPath(import.meta.url));
6
8
  return path.dirname(dist);
7
9
  }
10
+ async function packageName(root) {
11
+ const contents = await readFile(path.join(root, "package.json"), "utf8");
12
+ return JSON.parse(contents).name;
13
+ }
8
14
  function run(command, args, cwd) {
9
15
  return new Promise((resolve, reject) => {
10
16
  const child = spawn(command, args, { cwd, stdio: "inherit" });
@@ -35,14 +41,14 @@ function captureOutput(command, args, cwd) {
35
41
  }
36
42
  export async function updateCli() {
37
43
  const root = packageRoot();
38
- let status;
39
- try {
40
- status = await captureOutput("git", ["status", "--porcelain"], root);
41
- }
42
- catch {
43
- throw new Error(`xon isn't installed from a git checkout at ${root}, so it can't self-update. ` +
44
- "Reinstall following the README instructions.");
44
+ if (!existsSync(path.join(root, ".git"))) {
45
+ const name = await packageName(root);
46
+ console.log(`Updating ${name} via npm...`);
47
+ await run("npm", ["install", "-g", `${name}@latest`], root);
48
+ console.log(`${name} is up to date.`);
49
+ return;
45
50
  }
51
+ const status = await captureOutput("git", ["status", "--porcelain"], root);
46
52
  if (status.trim().length > 0) {
47
53
  throw new Error(`xon has local changes in ${root}. Commit or stash them before updating.`);
48
54
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cyberxon/xon",
3
- "version": "0.1.0",
3
+ "version": "0.2.1",
4
4
  "description": "A CLI for Xon projects and deployments",
5
5
  "type": "module",
6
6
  "license": "UNLICENSED",