wayfront 0.1.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/LICENSE +21 -0
- package/README.md +63 -0
- package/package.json +35 -0
- package/src/commands/init.js +96 -0
- package/src/commands/templates-pull.js +48 -0
- package/src/commands/templates-push.js +97 -0
- package/src/commands/templates-reset.js +59 -0
- package/src/commands/use.js +21 -0
- package/src/index.js +42 -0
- package/src/lib/api.js +102 -0
- package/src/lib/config.js +41 -0
- package/src/lib/files.js +49 -0
- package/src/lib/prompt.js +21 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Wayfront
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
# Wayfront CLI
|
|
2
|
+
|
|
3
|
+
Pull and push Twig templates from your [Wayfront](https://wayfront.com) workspace.
|
|
4
|
+
|
|
5
|
+
## Quick start
|
|
6
|
+
|
|
7
|
+
```
|
|
8
|
+
npx wayfront
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
This will walk you through connecting your workspace. Enter your username and paste your API token.
|
|
12
|
+
|
|
13
|
+
## Workflow
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
# 1. Pull templates from your workspace
|
|
17
|
+
wayfront templates:pull
|
|
18
|
+
|
|
19
|
+
# 2. Edit templates locally in ./templates
|
|
20
|
+
# Use your editor, commit to git, review in PRs — whatever your flow is
|
|
21
|
+
|
|
22
|
+
# 3. Push your changes back
|
|
23
|
+
wayfront templates:push # only pushes templates that changed
|
|
24
|
+
|
|
25
|
+
# 4. Commit your work
|
|
26
|
+
git add templates/
|
|
27
|
+
git commit -m "Update invoice template"
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
The `templates/` folder is yours to version control. Pull once, make changes over time, push when ready.
|
|
31
|
+
|
|
32
|
+
## Commands
|
|
33
|
+
|
|
34
|
+
```
|
|
35
|
+
wayfront Show connection status
|
|
36
|
+
wayfront init [workspace] Connect a workspace
|
|
37
|
+
wayfront use <workspace> Switch active workspace
|
|
38
|
+
wayfront templates:pull [name] Pull templates (or one by name)
|
|
39
|
+
wayfront templates:push [name] Push changed templates (or one by name)
|
|
40
|
+
wayfront templates:reset [name] Reset modified templates to default
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
## Template naming
|
|
44
|
+
|
|
45
|
+
Templates use dot-notation that maps to folders:
|
|
46
|
+
|
|
47
|
+
```
|
|
48
|
+
portal.invoices.show → templates/portal/invoices/show.twig
|
|
49
|
+
email.invoice_paid → templates/email/invoice_paid.twig
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
## Multiple workspaces
|
|
53
|
+
|
|
54
|
+
Each workspace is identified by its Wayfront username. You can connect more than one and switch between them:
|
|
55
|
+
|
|
56
|
+
```bash
|
|
57
|
+
wayfront init clientco # connect a second workspace
|
|
58
|
+
wayfront use acme # switch back
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
## Requirements
|
|
62
|
+
|
|
63
|
+
Node.js 18+
|
package/package.json
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "wayfront",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "CLI for managing Wayfront templates",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"wayfront": "src/index.js"
|
|
8
|
+
},
|
|
9
|
+
"files": [
|
|
10
|
+
"src"
|
|
11
|
+
],
|
|
12
|
+
"engines": {
|
|
13
|
+
"node": ">=18"
|
|
14
|
+
},
|
|
15
|
+
"keywords": [
|
|
16
|
+
"wayfront",
|
|
17
|
+
"templates",
|
|
18
|
+
"twig",
|
|
19
|
+
"cli"
|
|
20
|
+
],
|
|
21
|
+
"scripts": {
|
|
22
|
+
"test": "node --test test/*.test.js"
|
|
23
|
+
},
|
|
24
|
+
"repository": {
|
|
25
|
+
"type": "git",
|
|
26
|
+
"url": "git+https://github.com/wayfronthq/wayfront-cli.git"
|
|
27
|
+
},
|
|
28
|
+
"homepage": "https://github.com/wayfronthq/wayfront-cli",
|
|
29
|
+
"license": "MIT",
|
|
30
|
+
"dependencies": {
|
|
31
|
+
"chalk": "^5.6.2",
|
|
32
|
+
"commander": "^14.0.3",
|
|
33
|
+
"ora": "^9.3.0"
|
|
34
|
+
}
|
|
35
|
+
}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { exec } from 'node:child_process';
|
|
2
|
+
import chalk from 'chalk';
|
|
3
|
+
import { loadConfig, saveConfig } from '../lib/config.js';
|
|
4
|
+
import { prompt, confirm } from '../lib/prompt.js';
|
|
5
|
+
|
|
6
|
+
function openUrl(url) {
|
|
7
|
+
const cmd = process.platform === 'darwin' ? 'open'
|
|
8
|
+
: process.platform === 'win32' ? 'start'
|
|
9
|
+
: 'xdg-open';
|
|
10
|
+
exec(`${cmd} '${url}'`);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function parseInput(input) {
|
|
14
|
+
if (/^https?:\/\//.test(input)) {
|
|
15
|
+
const parsed = new URL(input);
|
|
16
|
+
const wayfrontMatch = parsed.hostname.match(/^([^.]+)\.wayfront\.com$/);
|
|
17
|
+
if (wayfrontMatch) {
|
|
18
|
+
return { workspace: wayfrontMatch[1], url: null };
|
|
19
|
+
}
|
|
20
|
+
return { workspace: parsed.hostname.split('.')[0], url: `${parsed.protocol}//${parsed.host}` };
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const wayfrontMatch = input.match(/^([^.]+)\.wayfront\.com$/);
|
|
24
|
+
if (wayfrontMatch) {
|
|
25
|
+
return { workspace: wayfrontMatch[1], url: null };
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
if (input.includes('.')) {
|
|
29
|
+
return { workspace: input.split('.')[0], url: `https://${input}` };
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
return { workspace: input, url: null };
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export async function runInit(input) {
|
|
36
|
+
const { workspace, url } = input ? parseInput(input) : { workspace: null, url: null };
|
|
37
|
+
const config = loadConfig();
|
|
38
|
+
config.workspaces ??= {};
|
|
39
|
+
|
|
40
|
+
let finalWorkspace = workspace;
|
|
41
|
+
let finalUrl = url;
|
|
42
|
+
if (!finalWorkspace) {
|
|
43
|
+
console.log(chalk.dim(' Your Wayfront username, e.g. acme'));
|
|
44
|
+
const answer = await prompt('Workspace: ');
|
|
45
|
+
if (!answer) {
|
|
46
|
+
console.error('No workspace provided.');
|
|
47
|
+
process.exit(1);
|
|
48
|
+
}
|
|
49
|
+
const parsed = parseInput(answer);
|
|
50
|
+
finalWorkspace = parsed.workspace;
|
|
51
|
+
finalUrl = parsed.url;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const baseUrl = finalUrl || `https://${finalWorkspace}.wayfront.com`;
|
|
55
|
+
const existing = config.workspaces[finalWorkspace];
|
|
56
|
+
|
|
57
|
+
if (existing) {
|
|
58
|
+
config.default = finalWorkspace;
|
|
59
|
+
saveConfig(config);
|
|
60
|
+
console.log(chalk.green('✓') + ` Switched to "${finalWorkspace}" (${baseUrl})`);
|
|
61
|
+
|
|
62
|
+
const update = await confirm('Update API token?');
|
|
63
|
+
if (!update) return;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const tokenUrl = `${baseUrl}/settings/templates/api-token`;
|
|
67
|
+
|
|
68
|
+
openUrl(tokenUrl);
|
|
69
|
+
console.log(`\nOpening ${chalk.cyan(tokenUrl)}\n`);
|
|
70
|
+
|
|
71
|
+
const token = await prompt('Paste your token: ');
|
|
72
|
+
|
|
73
|
+
if (!token) {
|
|
74
|
+
console.error('No token provided.');
|
|
75
|
+
process.exit(1);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
config.workspaces[finalWorkspace] = {
|
|
79
|
+
token,
|
|
80
|
+
...(finalUrl ? { url: finalUrl } : {}),
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
config.default = finalWorkspace;
|
|
84
|
+
saveConfig(config);
|
|
85
|
+
|
|
86
|
+
console.log(chalk.green('✓') + ` Workspace "${finalWorkspace}" configured (${baseUrl})`);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export function registerInit(program) {
|
|
90
|
+
program
|
|
91
|
+
.command('init [workspace]')
|
|
92
|
+
.description('Connect a workspace')
|
|
93
|
+
.action(async (input) => {
|
|
94
|
+
await runInit(input);
|
|
95
|
+
});
|
|
96
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { mkdirSync, writeFileSync, existsSync } from 'node:fs';
|
|
2
|
+
import { dirname } from 'node:path';
|
|
3
|
+
import chalk from 'chalk';
|
|
4
|
+
import ora from 'ora';
|
|
5
|
+
import { apiGet } from '../lib/api.js';
|
|
6
|
+
import { nameToPath, elapsed } from '../lib/files.js';
|
|
7
|
+
|
|
8
|
+
function writeTemplate(template, dir) {
|
|
9
|
+
const filePath = nameToPath(template.name, dir);
|
|
10
|
+
mkdirSync(dirname(filePath), { recursive: true });
|
|
11
|
+
writeFileSync(filePath, template.data || '');
|
|
12
|
+
return filePath;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function registerTemplatesPull(program) {
|
|
16
|
+
program
|
|
17
|
+
.command('templates:pull [name]')
|
|
18
|
+
.description('Pull templates from the API (or one by name)')
|
|
19
|
+
.action(async (name) => {
|
|
20
|
+
const dir = './templates';
|
|
21
|
+
const isFirstPull = !existsSync(dir) || !existsSync(`${dir}/.gitignore`);
|
|
22
|
+
const start = Date.now();
|
|
23
|
+
const spinner = ora(name ? `Pulling ${name}…` : 'Pulling templates…').start();
|
|
24
|
+
|
|
25
|
+
try {
|
|
26
|
+
if (name) {
|
|
27
|
+
const template = await apiGet(`/api/templates/${name}`);
|
|
28
|
+
const filePath = writeTemplate(template, dir);
|
|
29
|
+
spinner.succeed(`${template.name} → ${filePath} ${elapsed(start)}`);
|
|
30
|
+
} else {
|
|
31
|
+
const templates = await apiGet('/api/templates');
|
|
32
|
+
|
|
33
|
+
for (const t of templates) {
|
|
34
|
+
writeTemplate(t, dir);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
spinner.succeed(`Pulled ${templates.length} template(s) to ${dir} ${elapsed(start)}`);
|
|
38
|
+
|
|
39
|
+
if (isFirstPull) {
|
|
40
|
+
console.log(chalk.dim('\n Edit templates locally, then push changes with: wayfront templates:push'));
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
} catch (err) {
|
|
44
|
+
spinner.fail(err.message);
|
|
45
|
+
process.exit(1);
|
|
46
|
+
}
|
|
47
|
+
});
|
|
48
|
+
}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import { readFileSync } from 'node:fs';
|
|
2
|
+
import chalk from 'chalk';
|
|
3
|
+
import ora from 'ora';
|
|
4
|
+
import { apiGet, apiPut } from '../lib/api.js';
|
|
5
|
+
import { nameToPath, pathToName, findTemplateFiles, formatName, elapsed } from '../lib/files.js';
|
|
6
|
+
|
|
7
|
+
export function registerTemplatesPush(program) {
|
|
8
|
+
program
|
|
9
|
+
.command('templates:push [name]')
|
|
10
|
+
.description('Push templates to the API (or one by name)')
|
|
11
|
+
.action(async (name) => {
|
|
12
|
+
const dir = './templates';
|
|
13
|
+
|
|
14
|
+
try {
|
|
15
|
+
if (name) {
|
|
16
|
+
const filePath = nameToPath(name, dir);
|
|
17
|
+
const start = Date.now();
|
|
18
|
+
const spinner = ora(`Pushing ${name}…`).start();
|
|
19
|
+
|
|
20
|
+
let content;
|
|
21
|
+
try {
|
|
22
|
+
content = readFileSync(filePath, 'utf8');
|
|
23
|
+
} catch {
|
|
24
|
+
spinner.fail(`File not found: ${filePath}`);
|
|
25
|
+
process.exit(1);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
await apiPut(`/api/templates/${name}`, { data: content });
|
|
29
|
+
spinner.succeed(`Pushed ${name} ${elapsed(start)}`);
|
|
30
|
+
} else {
|
|
31
|
+
const files = findTemplateFiles(dir);
|
|
32
|
+
|
|
33
|
+
if (files.length === 0) {
|
|
34
|
+
console.log(`No .twig files found in ${dir}`);
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const start = Date.now();
|
|
39
|
+
const spinner = ora('Comparing templates…').start();
|
|
40
|
+
|
|
41
|
+
// Fetch remote state to diff against
|
|
42
|
+
const remote = await apiGet('/api/templates');
|
|
43
|
+
const remoteByName = {};
|
|
44
|
+
for (const t of remote) {
|
|
45
|
+
remoteByName[t.name] = t.data || '';
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// Find changed templates
|
|
49
|
+
const changed = [];
|
|
50
|
+
for (const filePath of files) {
|
|
51
|
+
const templateName = pathToName(filePath, dir);
|
|
52
|
+
const local = readFileSync(filePath, 'utf8');
|
|
53
|
+
if (local !== remoteByName[templateName]) {
|
|
54
|
+
changed.push({ name: templateName, content: local });
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const unchanged = files.length - changed.length;
|
|
59
|
+
|
|
60
|
+
if (changed.length === 0) {
|
|
61
|
+
spinner.succeed(`Everything's in sync — ${chalk.dim(`${unchanged} template(s) unchanged`)} ${elapsed(start)}`);
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
spinner.text = `Pushing ${changed.length} changed template(s)…`;
|
|
66
|
+
let count = 0;
|
|
67
|
+
const errors = [];
|
|
68
|
+
|
|
69
|
+
for (const t of changed) {
|
|
70
|
+
try {
|
|
71
|
+
await apiPut(`/api/templates/${t.name}`, { data: t.content });
|
|
72
|
+
count++;
|
|
73
|
+
} catch (err) {
|
|
74
|
+
errors.push({ name: t.name, message: err.message });
|
|
75
|
+
}
|
|
76
|
+
spinner.text = `Pushing templates… (${count + errors.length}/${changed.length})`;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
if (errors.length === 0) {
|
|
80
|
+
spinner.succeed(`Pushed ${count}, ${unchanged} unchanged ${elapsed(start)}`);
|
|
81
|
+
for (const t of changed) {
|
|
82
|
+
console.log(` ${formatName(t.name)}`);
|
|
83
|
+
}
|
|
84
|
+
} else {
|
|
85
|
+
spinner.warn(`Pushed ${count} template(s), ${errors.length} failed`);
|
|
86
|
+
for (const e of errors) {
|
|
87
|
+
console.error(` ✗ ${e.name}: ${e.message}`);
|
|
88
|
+
}
|
|
89
|
+
process.exit(1);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
} catch (err) {
|
|
93
|
+
console.error(err.message);
|
|
94
|
+
process.exit(1);
|
|
95
|
+
}
|
|
96
|
+
});
|
|
97
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
import ora from 'ora';
|
|
3
|
+
import { apiGet, apiDelete } from '../lib/api.js';
|
|
4
|
+
import { confirm } from '../lib/prompt.js';
|
|
5
|
+
import { formatName, elapsed } from '../lib/files.js';
|
|
6
|
+
|
|
7
|
+
export function registerTemplatesReset(program) {
|
|
8
|
+
program
|
|
9
|
+
.command('templates:reset [name]')
|
|
10
|
+
.description('Reset templates to default (or one by name)')
|
|
11
|
+
.action(async (name) => {
|
|
12
|
+
try {
|
|
13
|
+
if (name) {
|
|
14
|
+
const ok = await confirm(`Reset "${name}" to default?`);
|
|
15
|
+
if (!ok) return;
|
|
16
|
+
|
|
17
|
+
const start = Date.now();
|
|
18
|
+
const spinner = ora(`Resetting ${name}…`).start();
|
|
19
|
+
await apiDelete(`/api/templates/${name}`);
|
|
20
|
+
spinner.succeed(`Reset ${name} to default ${elapsed(start)}`);
|
|
21
|
+
console.log(chalk.dim(`\n Run wayfront templates:pull ${name} to update your local copy`));
|
|
22
|
+
} else {
|
|
23
|
+
const spinner = ora('Fetching template list…').start();
|
|
24
|
+
const templates = await apiGet('/api/templates');
|
|
25
|
+
const custom = templates.filter((t) => t.is_modified);
|
|
26
|
+
|
|
27
|
+
if (custom.length === 0) {
|
|
28
|
+
spinner.succeed('No modified templates to reset');
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
spinner.stop();
|
|
33
|
+
console.log(`Found ${custom.length} modified template(s):\n`);
|
|
34
|
+
for (const t of custom) {
|
|
35
|
+
console.log(` ${formatName(t.name)}`);
|
|
36
|
+
}
|
|
37
|
+
console.log();
|
|
38
|
+
|
|
39
|
+
const ok = await confirm('Reset all to default?');
|
|
40
|
+
if (!ok) return;
|
|
41
|
+
|
|
42
|
+
const start = Date.now();
|
|
43
|
+
const resetSpinner = ora(`Resetting ${custom.length} template(s)…`).start();
|
|
44
|
+
let count = 0;
|
|
45
|
+
for (const t of custom) {
|
|
46
|
+
await apiDelete(`/api/templates/${t.name}`);
|
|
47
|
+
count++;
|
|
48
|
+
resetSpinner.text = `Resetting templates… (${count}/${custom.length})`;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
resetSpinner.succeed(`Reset ${count} template(s) to default ${elapsed(start)}`);
|
|
52
|
+
console.log(chalk.dim('\n Run wayfront templates:pull to update your local copies'));
|
|
53
|
+
}
|
|
54
|
+
} catch (err) {
|
|
55
|
+
console.error(err.message);
|
|
56
|
+
process.exit(1);
|
|
57
|
+
}
|
|
58
|
+
});
|
|
59
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
import { loadConfig, saveConfig } from '../lib/config.js';
|
|
3
|
+
|
|
4
|
+
export function registerUse(program) {
|
|
5
|
+
program
|
|
6
|
+
.command('use <workspace>')
|
|
7
|
+
.description('Switch active workspace')
|
|
8
|
+
.action((workspace) => {
|
|
9
|
+
const config = loadConfig();
|
|
10
|
+
|
|
11
|
+
if (!config.workspaces?.[workspace]) {
|
|
12
|
+
console.error(`Workspace "${workspace}" not found. Run: wayfront init <workspace>`);
|
|
13
|
+
process.exit(1);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
config.default = workspace;
|
|
17
|
+
saveConfig(config);
|
|
18
|
+
|
|
19
|
+
console.log(chalk.green('✓') + ` Active workspace set to "${workspace}"`);
|
|
20
|
+
});
|
|
21
|
+
}
|
package/src/index.js
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { Command } from 'commander';
|
|
4
|
+
import chalk from 'chalk';
|
|
5
|
+
import { loadConfig } from './lib/config.js';
|
|
6
|
+
import { registerInit, runInit } from './commands/init.js';
|
|
7
|
+
import { registerUse } from './commands/use.js';
|
|
8
|
+
import { registerTemplatesPull } from './commands/templates-pull.js';
|
|
9
|
+
import { registerTemplatesPush } from './commands/templates-push.js';
|
|
10
|
+
import { registerTemplatesReset } from './commands/templates-reset.js';
|
|
11
|
+
|
|
12
|
+
const program = new Command();
|
|
13
|
+
|
|
14
|
+
program
|
|
15
|
+
.name('wayfront')
|
|
16
|
+
.description('CLI for managing Wayfront templates')
|
|
17
|
+
.version('0.1.0')
|
|
18
|
+
.action(async () => {
|
|
19
|
+
const config = loadConfig();
|
|
20
|
+
const ws = config.default && config.workspaces?.[config.default];
|
|
21
|
+
|
|
22
|
+
if (!ws) {
|
|
23
|
+
console.log(`Welcome to Wayfront! Let's connect your workspace.\n`);
|
|
24
|
+
await runInit();
|
|
25
|
+
console.log();
|
|
26
|
+
} else {
|
|
27
|
+
const url = ws.url || `https://${config.default}.wayfront.com`;
|
|
28
|
+
const greetings = ['Ready', 'All set', 'Connected', 'Good to go'];
|
|
29
|
+
const greeting = greetings[Math.floor(Math.random() * greetings.length)];
|
|
30
|
+
console.log(`${chalk.green('✓')} ${greeting} — ${chalk.bold(config.default)} ${chalk.dim(`(${url})`)}\n`);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
program.outputHelp();
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
registerInit(program);
|
|
37
|
+
registerUse(program);
|
|
38
|
+
registerTemplatesPull(program);
|
|
39
|
+
registerTemplatesPush(program);
|
|
40
|
+
registerTemplatesReset(program);
|
|
41
|
+
|
|
42
|
+
program.parse();
|
package/src/lib/api.js
ADDED
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import { hostname, userInfo } from 'node:os';
|
|
2
|
+
import { getCredentials } from './config.js';
|
|
3
|
+
|
|
4
|
+
function getDeviceName() {
|
|
5
|
+
try {
|
|
6
|
+
return `${userInfo().username}@${hostname()}`;
|
|
7
|
+
} catch {
|
|
8
|
+
return hostname();
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
// Suppress the TLS warning when we intentionally disable cert checks for local dev
|
|
13
|
+
const originalEmitWarning = process.emitWarning;
|
|
14
|
+
process.emitWarning = function (warning, ...args) {
|
|
15
|
+
if (typeof warning === 'string' && warning.includes('NODE_TLS_REJECT_UNAUTHORIZED')) return;
|
|
16
|
+
return originalEmitWarning.call(this, warning, ...args);
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
export function isLocal(url) {
|
|
20
|
+
const { hostname } = new URL(url);
|
|
21
|
+
return hostname === 'localhost'
|
|
22
|
+
|| hostname.endsWith('.test')
|
|
23
|
+
|| hostname.endsWith('.local');
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
async function request(method, path, body) {
|
|
27
|
+
const { url: baseUrl, token } = getCredentials();
|
|
28
|
+
const url = `${baseUrl}${path}`;
|
|
29
|
+
|
|
30
|
+
// Trust self-signed certs for local dev domains
|
|
31
|
+
const prevTls = process.env.NODE_TLS_REJECT_UNAUTHORIZED;
|
|
32
|
+
if (isLocal(baseUrl)) {
|
|
33
|
+
process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0';
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const options = {
|
|
37
|
+
method,
|
|
38
|
+
headers: {
|
|
39
|
+
'Authorization': `Bearer ${token}`,
|
|
40
|
+
'Accept': 'application/json',
|
|
41
|
+
'X-Device-Name': getDeviceName(),
|
|
42
|
+
},
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
if (body !== undefined) {
|
|
46
|
+
options.headers['Content-Type'] = 'application/json';
|
|
47
|
+
options.body = JSON.stringify(body);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
let res;
|
|
51
|
+
try {
|
|
52
|
+
res = await fetch(url, options);
|
|
53
|
+
} catch (err) {
|
|
54
|
+
throw new Error(`Could not connect to ${url}`);
|
|
55
|
+
} finally {
|
|
56
|
+
if (prevTls === undefined) {
|
|
57
|
+
delete process.env.NODE_TLS_REJECT_UNAUTHORIZED;
|
|
58
|
+
} else {
|
|
59
|
+
process.env.NODE_TLS_REJECT_UNAUTHORIZED = prevTls;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
if (!res.ok) {
|
|
64
|
+
let data;
|
|
65
|
+
try {
|
|
66
|
+
data = await res.json();
|
|
67
|
+
} catch {
|
|
68
|
+
throw new Error(`API error ${res.status}: ${res.statusText}`);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
if (res.status === 401) {
|
|
72
|
+
throw new Error('Authentication failed. Check your API token.');
|
|
73
|
+
}
|
|
74
|
+
if (res.status === 403) {
|
|
75
|
+
throw new Error(data.message || 'Forbidden. Your license may not include API access.');
|
|
76
|
+
}
|
|
77
|
+
if (res.status === 422 && data.errors) {
|
|
78
|
+
const msgs = Object.entries(data.errors)
|
|
79
|
+
.map(([field, errs]) => ` ${field}: ${errs.join(', ')}`)
|
|
80
|
+
.join('\n');
|
|
81
|
+
throw new Error(`Validation failed:\n${msgs}`);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
throw new Error(data.message || `API error ${res.status}`);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
if (res.status === 204) return null;
|
|
88
|
+
|
|
89
|
+
return res.json();
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export function apiGet(path) {
|
|
93
|
+
return request('GET', path);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export function apiPut(path, body) {
|
|
97
|
+
return request('PUT', path, body);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export function apiDelete(path) {
|
|
101
|
+
return request('DELETE', path);
|
|
102
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { readFileSync, writeFileSync, mkdirSync } from 'node:fs';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import { homedir } from 'node:os';
|
|
4
|
+
|
|
5
|
+
const configDir = join(homedir(), '.config', 'wayfront');
|
|
6
|
+
const configPath = join(configDir, 'config.json');
|
|
7
|
+
|
|
8
|
+
export function loadConfig() {
|
|
9
|
+
try {
|
|
10
|
+
return JSON.parse(readFileSync(configPath, 'utf8'));
|
|
11
|
+
} catch {
|
|
12
|
+
return { default: null, workspaces: {} };
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function saveConfig(config) {
|
|
17
|
+
mkdirSync(configDir, { recursive: true });
|
|
18
|
+
writeFileSync(configPath, JSON.stringify(config, null, 2) + '\n');
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function getCredentials() {
|
|
22
|
+
const config = loadConfig();
|
|
23
|
+
|
|
24
|
+
if (!config.default) {
|
|
25
|
+
throw new Error('No active workspace. Run: wayfront init <workspace>');
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const ws = config.workspaces[config.default];
|
|
29
|
+
if (!ws) {
|
|
30
|
+
throw new Error(`Workspace "${config.default}" not found. Run: wayfront init <workspace>`);
|
|
31
|
+
}
|
|
32
|
+
if (!ws.token) {
|
|
33
|
+
throw new Error(`No token set for workspace "${config.default}". Run: wayfront init ${config.default}`);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
return {
|
|
37
|
+
workspace: config.default,
|
|
38
|
+
url: ws.url || `https://${config.default}.wayfront.com`,
|
|
39
|
+
token: ws.token,
|
|
40
|
+
};
|
|
41
|
+
}
|
package/src/lib/files.js
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { join, relative, sep } from 'node:path';
|
|
2
|
+
import { readdirSync } from 'node:fs';
|
|
3
|
+
import chalk from 'chalk';
|
|
4
|
+
|
|
5
|
+
export function nameToPath(name, dir = './templates') {
|
|
6
|
+
const parts = name.split('.');
|
|
7
|
+
return join(dir, ...parts) + '.twig';
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function pathToName(filePath, dir = './templates') {
|
|
11
|
+
let rel = relative(dir, filePath);
|
|
12
|
+
if (rel.endsWith('.twig')) rel = rel.slice(0, -5);
|
|
13
|
+
return rel.split(sep).join('.');
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function formatName(name) {
|
|
17
|
+
const lastDot = name.lastIndexOf('.');
|
|
18
|
+
if (lastDot === -1) return chalk.bold(name);
|
|
19
|
+
return chalk.dim(name.slice(0, lastDot + 1)) + chalk.bold(name.slice(lastDot + 1));
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function elapsed(start) {
|
|
23
|
+
const ms = Date.now() - start;
|
|
24
|
+
return chalk.dim(ms < 1000 ? `(${ms}ms)` : `(${(ms / 1000).toFixed(1)}s)`);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function findTemplateFiles(dir = './templates') {
|
|
28
|
+
const results = [];
|
|
29
|
+
|
|
30
|
+
function walk(current) {
|
|
31
|
+
let entries;
|
|
32
|
+
try {
|
|
33
|
+
entries = readdirSync(current, { withFileTypes: true });
|
|
34
|
+
} catch {
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
for (const entry of entries) {
|
|
38
|
+
const fullPath = join(current, entry.name);
|
|
39
|
+
if (entry.isDirectory()) {
|
|
40
|
+
walk(fullPath);
|
|
41
|
+
} else if (entry.name.endsWith('.twig')) {
|
|
42
|
+
results.push(fullPath);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
walk(dir);
|
|
48
|
+
return results;
|
|
49
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { createInterface } from 'node:readline';
|
|
2
|
+
|
|
3
|
+
export function prompt(question) {
|
|
4
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
5
|
+
return new Promise((resolve) => {
|
|
6
|
+
rl.question(question, (answer) => {
|
|
7
|
+
rl.close();
|
|
8
|
+
resolve(answer.trim());
|
|
9
|
+
});
|
|
10
|
+
});
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function confirm(question) {
|
|
14
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
15
|
+
return new Promise((resolve) => {
|
|
16
|
+
rl.question(`${question} (y/N) `, (answer) => {
|
|
17
|
+
rl.close();
|
|
18
|
+
resolve(answer.trim().toLowerCase() === 'y');
|
|
19
|
+
});
|
|
20
|
+
});
|
|
21
|
+
}
|