@webmate-studio/cli 0.3.61 → 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/README.md +222 -0
- package/bin/wm.mjs +208 -0
- package/package.json +5 -1
- package/src/commands/build.js +335 -0
- package/src/commands/clone.js +414 -0
- package/src/commands/components.js +101 -0
- package/src/commands/core.js +1039 -0
- package/src/commands/dev.js +0 -12
- package/src/commands/doctor.js +192 -0
- package/src/commands/install.js +312 -0
- package/src/commands/login.js +158 -0
- package/src/commands/logout.js +22 -0
- package/src/commands/projects.js +91 -0
- package/src/commands/pull.js +192 -0
- package/src/commands/push.js +231 -0
- package/src/commands/reset.js +118 -0
- package/src/commands/status.js +118 -0
- package/src/commands/versions.js +130 -0
- package/src/commands/whoami.js +64 -0
- package/src/index.js +1 -15
- package/src/utils/api-client.js +131 -0
- package/src/utils/auth-resolver.js +145 -0
- package/src/utils/auth-storage.js +104 -0
- package/src/utils/component-files.js +195 -0
- package/src/utils/device-flow.js +111 -0
- package/src/utils/git-snapshot.js +63 -0
- package/src/utils/tenant-api.js +103 -0
- package/src/utils/webmate-meta.js +75 -0
- package/src/utils/auth.js +0 -125
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import { existsSync } from 'fs';
|
|
2
|
+
import { resolve } from 'path';
|
|
3
|
+
import pc from 'picocolors';
|
|
4
|
+
import { logger } from '@webmate-studio/core';
|
|
5
|
+
import { apiFetch, ApiError } from '../utils/api-client.js';
|
|
6
|
+
import { readMeta } from '../utils/webmate-meta.js';
|
|
7
|
+
import { readComponentFiles, diffHashes, getComponentId } from '../utils/component-files.js';
|
|
8
|
+
import { tenantApiFetch, resolveTenantSubdomain } from '../utils/tenant-api.js';
|
|
9
|
+
|
|
10
|
+
function resolveComponentDir(arg) {
|
|
11
|
+
const cwd = process.cwd();
|
|
12
|
+
if (!arg) return cwd;
|
|
13
|
+
return resolve(cwd, arg);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function fmtState(state) {
|
|
17
|
+
switch (state) {
|
|
18
|
+
case 'in-sync': return pc.green('in sync');
|
|
19
|
+
case 'changes-local': return pc.yellow('local changes');
|
|
20
|
+
case 'behind-remote': return pc.cyan('behind remote');
|
|
21
|
+
case 'diverged': return pc.red('diverged');
|
|
22
|
+
case 'unlinked': return pc.dim('not linked');
|
|
23
|
+
default: return state;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export async function statusCommand(componentArg, options = {}) {
|
|
28
|
+
const rootDir = resolveComponentDir(componentArg);
|
|
29
|
+
|
|
30
|
+
if (!existsSync(rootDir)) {
|
|
31
|
+
logger.error(`Directory not found: ${rootDir}`);
|
|
32
|
+
process.exit(1);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const meta = readMeta(rootDir);
|
|
36
|
+
let componentInfo;
|
|
37
|
+
try {
|
|
38
|
+
componentInfo = getComponentId(rootDir);
|
|
39
|
+
} catch (err) {
|
|
40
|
+
logger.error(err.message);
|
|
41
|
+
process.exit(1);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const componentId = componentInfo.id;
|
|
45
|
+
const { fileHashes: localHashes } = readComponentFiles(rootDir);
|
|
46
|
+
|
|
47
|
+
const localDiff = meta?.fileHashes
|
|
48
|
+
? diffHashes(localHashes, meta.fileHashes)
|
|
49
|
+
: { added: Object.keys(localHashes), modified: [], removed: [] };
|
|
50
|
+
|
|
51
|
+
const hasLocal =
|
|
52
|
+
localDiff.added.length > 0 ||
|
|
53
|
+
localDiff.modified.length > 0 ||
|
|
54
|
+
localDiff.removed.length > 0;
|
|
55
|
+
|
|
56
|
+
let remoteLatest = null;
|
|
57
|
+
let remoteError = null;
|
|
58
|
+
if (!options.offline) {
|
|
59
|
+
try {
|
|
60
|
+
const sub = resolveTenantSubdomain(meta);
|
|
61
|
+
const path = sub
|
|
62
|
+
? `/api/tenant-components/${componentId}/versions`
|
|
63
|
+
: `/api/organization/components/${componentId}/versions`;
|
|
64
|
+
const list = await tenantApiFetch(path, {}, meta);
|
|
65
|
+
remoteLatest = list?.versions?.[0] ?? null;
|
|
66
|
+
} catch (err) {
|
|
67
|
+
remoteError = err instanceof ApiError
|
|
68
|
+
? `HTTP ${err.status}: ${err.message}`
|
|
69
|
+
: err.message;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const behindRemote =
|
|
74
|
+
meta?.baseVersion && remoteLatest && remoteLatest.id !== meta.baseVersion;
|
|
75
|
+
|
|
76
|
+
let state;
|
|
77
|
+
if (!meta) state = 'unlinked';
|
|
78
|
+
else if (hasLocal && behindRemote) state = 'diverged';
|
|
79
|
+
else if (hasLocal) state = 'changes-local';
|
|
80
|
+
else if (behindRemote) state = 'behind-remote';
|
|
81
|
+
else state = 'in-sync';
|
|
82
|
+
|
|
83
|
+
console.log();
|
|
84
|
+
console.log(`${pc.bold(componentInfo.displayName ?? componentId)}`);
|
|
85
|
+
console.log(` ${pc.dim('Path:')} ${pc.dim(rootDir)}`);
|
|
86
|
+
console.log(` ${pc.dim('ID:')} ${pc.dim(componentId)}`);
|
|
87
|
+
console.log(` ${pc.dim('State:')} ${fmtState(state)}`);
|
|
88
|
+
|
|
89
|
+
if (meta) {
|
|
90
|
+
console.log(
|
|
91
|
+
` ${pc.dim('Local:')} ${meta.version ? pc.cyan(meta.version) : pc.dim('(no version)')}` +
|
|
92
|
+
(meta.baseVersion ? ` ${pc.dim('(' + meta.baseVersion.slice(0, 8) + ')')}` : '')
|
|
93
|
+
);
|
|
94
|
+
}
|
|
95
|
+
if (remoteLatest) {
|
|
96
|
+
console.log(
|
|
97
|
+
` ${pc.dim('Remote:')} ${pc.cyan(remoteLatest.version)} ${pc.dim('(' + remoteLatest.id.slice(0, 8) + ')')}`
|
|
98
|
+
);
|
|
99
|
+
} else if (remoteError) {
|
|
100
|
+
console.log(` ${pc.dim('Remote:')} ${pc.red(remoteError)}`);
|
|
101
|
+
} else if (options.offline) {
|
|
102
|
+
console.log(` ${pc.dim('Remote:')} ${pc.dim('(skipped, --offline)')}`);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
if (hasLocal) {
|
|
106
|
+
console.log();
|
|
107
|
+
console.log(` ${pc.bold('Local changes since last pull:')}`);
|
|
108
|
+
for (const p of localDiff.modified.slice(0, 20)) console.log(` ${pc.yellow('M')} ${p}`);
|
|
109
|
+
for (const p of localDiff.added.slice(0, 20)) console.log(` ${pc.green('A')} ${p}`);
|
|
110
|
+
for (const p of localDiff.removed.slice(0, 20)) console.log(` ${pc.red('D')} ${p}`);
|
|
111
|
+
const shown = localDiff.modified.length + localDiff.added.length + localDiff.removed.length;
|
|
112
|
+
if (shown > 60) console.log(` ${pc.dim('… more')}`);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
if (state === 'in-sync') process.exit(0);
|
|
116
|
+
if (state === 'unlinked') process.exit(1);
|
|
117
|
+
process.exit(0);
|
|
118
|
+
}
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import { existsSync } from 'fs';
|
|
2
|
+
import { resolve } from 'path';
|
|
3
|
+
import ora from 'ora';
|
|
4
|
+
import pc from 'picocolors';
|
|
5
|
+
import { logger } from '@webmate-studio/core';
|
|
6
|
+
import { apiFetch, ApiError } from '../utils/api-client.js';
|
|
7
|
+
import { readMeta } from '../utils/webmate-meta.js';
|
|
8
|
+
import { getComponentId } from '../utils/component-files.js';
|
|
9
|
+
import { tenantApiFetch, resolveTenantSubdomain } from '../utils/tenant-api.js';
|
|
10
|
+
|
|
11
|
+
function isUuid(value) {
|
|
12
|
+
return /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(value);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function resolveComponentId(arg) {
|
|
16
|
+
if (!arg) {
|
|
17
|
+
const meta = readMeta(process.cwd());
|
|
18
|
+
if (meta?.componentId) return meta.componentId;
|
|
19
|
+
if (existsSync(`${process.cwd()}/component.json`)) {
|
|
20
|
+
try {
|
|
21
|
+
return getComponentId(process.cwd()).id;
|
|
22
|
+
} catch {
|
|
23
|
+
// ignore
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
return null;
|
|
27
|
+
}
|
|
28
|
+
if (isUuid(arg)) return arg;
|
|
29
|
+
const dir = resolve(process.cwd(), arg);
|
|
30
|
+
const meta = readMeta(dir);
|
|
31
|
+
if (meta?.componentId) return meta.componentId;
|
|
32
|
+
if (existsSync(`${dir}/component.json`)) {
|
|
33
|
+
try {
|
|
34
|
+
return getComponentId(dir).id;
|
|
35
|
+
} catch {
|
|
36
|
+
// ignore
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
return null;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function fmt(date) {
|
|
43
|
+
if (!date) return '';
|
|
44
|
+
try {
|
|
45
|
+
return new Date(date).toISOString().slice(0, 16).replace('T', ' ');
|
|
46
|
+
} catch {
|
|
47
|
+
return String(date);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function truncate(s, n) {
|
|
52
|
+
if (!s) return '';
|
|
53
|
+
if (s.length <= n) return s;
|
|
54
|
+
return s.slice(0, n - 1) + '…';
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export async function versionsCommand(componentArg, options = {}) {
|
|
58
|
+
const componentId = resolveComponentId(componentArg);
|
|
59
|
+
if (!componentId) {
|
|
60
|
+
logger.error(
|
|
61
|
+
'Could not resolve component id. Pass a UUID directly, ' +
|
|
62
|
+
'or run from inside a component directory.'
|
|
63
|
+
);
|
|
64
|
+
process.exit(1);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const spinner = ora(`Fetching versions for ${pc.dim(componentId)}…`).start();
|
|
68
|
+
let response;
|
|
69
|
+
try {
|
|
70
|
+
// Tenant-Routing wenn der Component-Ordner einen tenantSubdomain
|
|
71
|
+
// im .webmate.json trägt (oder env-Variable gesetzt ist).
|
|
72
|
+
const meta = readMeta(process.cwd()) ?? null;
|
|
73
|
+
const sub = resolveTenantSubdomain(meta);
|
|
74
|
+
const path = sub
|
|
75
|
+
? `/api/tenant-components/${componentId}/versions`
|
|
76
|
+
: `/api/organization/components/${componentId}/versions`;
|
|
77
|
+
response = await tenantApiFetch(path, {}, meta);
|
|
78
|
+
spinner.stop();
|
|
79
|
+
} catch (err) {
|
|
80
|
+
if (err instanceof ApiError) {
|
|
81
|
+
spinner.fail(`Fetch failed (HTTP ${err.status}): ${err.message}`);
|
|
82
|
+
} else {
|
|
83
|
+
spinner.fail(err.message);
|
|
84
|
+
}
|
|
85
|
+
process.exit(1);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const versions = response?.versions ?? [];
|
|
89
|
+
if (!versions.length) {
|
|
90
|
+
logger.info('No versions yet for this component.');
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const limit = options.limit ? Number(options.limit) : versions.length;
|
|
95
|
+
const slice = versions.slice(0, limit);
|
|
96
|
+
|
|
97
|
+
console.log();
|
|
98
|
+
console.log(pc.bold(`${versions.length} version(s) for ${pc.dim(componentId)}`));
|
|
99
|
+
console.log();
|
|
100
|
+
|
|
101
|
+
const VER_W = 12;
|
|
102
|
+
const ID_W = 26;
|
|
103
|
+
const AUTH_W = 22;
|
|
104
|
+
const DATE_W = 18;
|
|
105
|
+
|
|
106
|
+
console.log(
|
|
107
|
+
pc.dim(
|
|
108
|
+
`${'Version'.padEnd(VER_W)} ${'Version ID'.padEnd(ID_W)} ${'Author'.padEnd(AUTH_W)} ${'Created'.padEnd(DATE_W)} Message`
|
|
109
|
+
)
|
|
110
|
+
);
|
|
111
|
+
console.log(
|
|
112
|
+
pc.dim(
|
|
113
|
+
'-'.repeat(VER_W) + ' ' + '-'.repeat(ID_W) + ' ' + '-'.repeat(AUTH_W) + ' ' + '-'.repeat(DATE_W) + ' -------'
|
|
114
|
+
)
|
|
115
|
+
);
|
|
116
|
+
|
|
117
|
+
for (const v of slice) {
|
|
118
|
+
console.log(
|
|
119
|
+
`${pc.cyan(String(v.version ?? '').padEnd(VER_W))} ` +
|
|
120
|
+
`${pc.dim(String(v.id ?? '').padEnd(ID_W))} ` +
|
|
121
|
+
`${truncate(v.gitAuthorName ?? '', AUTH_W).padEnd(AUTH_W)} ` +
|
|
122
|
+
`${pc.dim(fmt(v.createdAt).padEnd(DATE_W))} ` +
|
|
123
|
+
truncate(v.gitCommitMsg ?? '', 80)
|
|
124
|
+
);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
if (slice.length < versions.length) {
|
|
128
|
+
console.log(pc.dim(` … ${versions.length - slice.length} more (use --limit to extend)`));
|
|
129
|
+
}
|
|
130
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { logger } from '@webmate-studio/core';
|
|
2
|
+
import pc from 'picocolors';
|
|
3
|
+
import { resolveAuth } from '../utils/auth-resolver.js';
|
|
4
|
+
import { apiFetch, ApiError } from '../utils/api-client.js';
|
|
5
|
+
import { getAuthFilePath } from '../utils/auth-storage.js';
|
|
6
|
+
|
|
7
|
+
function formatSource(auth) {
|
|
8
|
+
switch (auth.source) {
|
|
9
|
+
case 'env':
|
|
10
|
+
return 'WEBMATE_TOKEN environment variable';
|
|
11
|
+
case 'file':
|
|
12
|
+
return getAuthFilePath();
|
|
13
|
+
case 'workspace':
|
|
14
|
+
return auth.workspacePath ?? '.webmate/config.json';
|
|
15
|
+
default:
|
|
16
|
+
return auth.source;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export async function whoamiCommand(options = {}) {
|
|
21
|
+
const auth = resolveAuth();
|
|
22
|
+
if (!auth) {
|
|
23
|
+
logger.error('Not logged in. Run `wm login` or set WEBMATE_TOKEN.');
|
|
24
|
+
process.exit(1);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
console.log(`${pc.bold('Token source:')} ${pc.dim(formatSource(auth))}`);
|
|
28
|
+
console.log(`${pc.bold('Base URL:')} ${pc.dim(auth.baseUrl)}`);
|
|
29
|
+
|
|
30
|
+
if (options.local) {
|
|
31
|
+
if (auth.email) console.log(`${pc.bold('Email:')} ${pc.cyan(auth.email)}`);
|
|
32
|
+
if (auth.userId) console.log(`${pc.bold('User ID:')} ${pc.dim(auth.userId)}`);
|
|
33
|
+
if (auth.organizationId) {
|
|
34
|
+
console.log(`${pc.bold('Organization:')} ${pc.dim(auth.organizationSlug ?? auth.organizationId)}`);
|
|
35
|
+
}
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
let me;
|
|
40
|
+
try {
|
|
41
|
+
me = await apiFetch('/api/auth/me');
|
|
42
|
+
} catch (err) {
|
|
43
|
+
if (err instanceof ApiError) {
|
|
44
|
+
logger.error(`Token rejected (HTTP ${err.status}): ${err.message}`);
|
|
45
|
+
} else {
|
|
46
|
+
logger.error(`Could not reach ${auth.baseUrl}: ${err.message}`);
|
|
47
|
+
}
|
|
48
|
+
process.exit(1);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const user = me?.data ?? me?.user ?? me;
|
|
52
|
+
const email = user?.email ?? auth.email ?? '(unknown)';
|
|
53
|
+
const userId = user?.id ?? auth.userId ?? '(unknown)';
|
|
54
|
+
const orgId = user?.organizationId ?? auth.organizationId ?? null;
|
|
55
|
+
const orgSlug = user?.organizationSlug ?? auth.organizationSlug ?? null;
|
|
56
|
+
const fullName = [user?.firstName, user?.lastName].filter(Boolean).join(' ');
|
|
57
|
+
|
|
58
|
+
console.log(`${pc.bold('Email:')} ${pc.cyan(email)}`);
|
|
59
|
+
if (fullName) console.log(`${pc.bold('Name:')} ${fullName}`);
|
|
60
|
+
console.log(`${pc.bold('User ID:')} ${pc.dim(userId)}`);
|
|
61
|
+
if (orgId) {
|
|
62
|
+
console.log(`${pc.bold('Organization:')} ${pc.dim(orgSlug ?? orgId)}`);
|
|
63
|
+
}
|
|
64
|
+
}
|
package/src/index.js
CHANGED
|
@@ -1,17 +1,3 @@
|
|
|
1
|
-
export { buildCommand } from './commands/build.js';
|
|
2
|
-
export { pushCommand } from './commands/push.js';
|
|
3
1
|
export { devCommand } from './commands/dev.js';
|
|
4
2
|
export { initCommand } from './commands/init.js';
|
|
5
|
-
|
|
6
|
-
// Export auth utilities for use by other packages (e.g. preview)
|
|
7
|
-
export {
|
|
8
|
-
loadAuth,
|
|
9
|
-
saveAuth,
|
|
10
|
-
clearAuth,
|
|
11
|
-
isLoggedIn,
|
|
12
|
-
getCurrentUser,
|
|
13
|
-
getApiToken,
|
|
14
|
-
getCurrentTenant,
|
|
15
|
-
getCmsBaseUrl,
|
|
16
|
-
getTenantCmsUrl
|
|
17
|
-
} from './utils/auth.js';
|
|
3
|
+
export { generate } from './commands/generate.js';
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import { requireAuth, resolveAuth } from './auth-resolver.js';
|
|
2
|
+
import { readAuth, writeAuth, jwtSecondsToExpiry } from './auth-storage.js';
|
|
3
|
+
|
|
4
|
+
const REFRESH_WINDOW_SECONDS = 7 * 24 * 60 * 60; // 7 days
|
|
5
|
+
|
|
6
|
+
function buildUrl(baseUrl, path) {
|
|
7
|
+
const base = baseUrl.replace(/\/+$/, '');
|
|
8
|
+
const rel = path.startsWith('/') ? path : `/${path}`;
|
|
9
|
+
return `${base}${rel}`;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export class ApiError extends Error {
|
|
13
|
+
constructor(message, { status, body, url } = {}) {
|
|
14
|
+
super(message);
|
|
15
|
+
this.name = 'ApiError';
|
|
16
|
+
this.status = status;
|
|
17
|
+
this.body = body;
|
|
18
|
+
this.url = url;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Exchange an existing (still-valid) JWT for a fresh one using
|
|
24
|
+
* POST /api/auth/refresh. Returns null on any failure so callers
|
|
25
|
+
* can fall back to the old token without crashing the command.
|
|
26
|
+
*/
|
|
27
|
+
export async function tryRefreshToken({ token, baseUrl }) {
|
|
28
|
+
try {
|
|
29
|
+
const response = await fetch(buildUrl(baseUrl, '/api/auth/refresh'), {
|
|
30
|
+
method: 'POST',
|
|
31
|
+
headers: {
|
|
32
|
+
Authorization: `Bearer ${token}`,
|
|
33
|
+
Accept: 'application/json'
|
|
34
|
+
}
|
|
35
|
+
});
|
|
36
|
+
if (!response.ok) return null;
|
|
37
|
+
const data = await response.json().catch(() => null);
|
|
38
|
+
if (!data?.access_token) return null;
|
|
39
|
+
return { token: data.access_token, expiresIn: data.expires_in ?? null };
|
|
40
|
+
} catch {
|
|
41
|
+
return null;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* If the current credential is a file-persisted JWT that is within
|
|
47
|
+
* REFRESH_WINDOW_SECONDS of expiring, try to swap it for a fresh
|
|
48
|
+
* one and rewrite ~/.webmate/auth.json. Best-effort: failures are
|
|
49
|
+
* silent so a transient network glitch does not break the request
|
|
50
|
+
* that triggered the check.
|
|
51
|
+
*
|
|
52
|
+
* Only the file-backed flow is refreshed:
|
|
53
|
+
* - env-supplied WEBMATE_TOKEN is opaque to us (CI/CD owns its rotation)
|
|
54
|
+
* - workspace .webmate/config.json apiTokens are wms_* (no expiry)
|
|
55
|
+
* - wms_* api tokens never expire either, so JWT detection guards us
|
|
56
|
+
*/
|
|
57
|
+
async function maybeAutoRefresh() {
|
|
58
|
+
const auth = resolveAuth();
|
|
59
|
+
if (auth?.source !== 'file') return null;
|
|
60
|
+
|
|
61
|
+
const secondsLeft = jwtSecondsToExpiry(auth.token);
|
|
62
|
+
if (secondsLeft === null) return null; // not a JWT (wms_ tokens persisted via env are handled above)
|
|
63
|
+
if (secondsLeft > REFRESH_WINDOW_SECONDS) return null; // still plenty of time
|
|
64
|
+
|
|
65
|
+
const refreshed = await tryRefreshToken({ token: auth.token, baseUrl: auth.baseUrl });
|
|
66
|
+
if (!refreshed?.token) return null;
|
|
67
|
+
|
|
68
|
+
const existing = readAuth();
|
|
69
|
+
if (existing) {
|
|
70
|
+
writeAuth({ ...existing, token: refreshed.token });
|
|
71
|
+
}
|
|
72
|
+
return refreshed.token;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export async function apiFetch(path, opts = {}) {
|
|
76
|
+
const {
|
|
77
|
+
method = 'GET',
|
|
78
|
+
body,
|
|
79
|
+
baseUrl: explicitBase,
|
|
80
|
+
token: explicitToken,
|
|
81
|
+
headers: extraHeaders = {},
|
|
82
|
+
json = true
|
|
83
|
+
} = opts;
|
|
84
|
+
|
|
85
|
+
let token = explicitToken;
|
|
86
|
+
let baseUrl = explicitBase;
|
|
87
|
+
if (!token || !baseUrl) {
|
|
88
|
+
const auth = requireAuth();
|
|
89
|
+
token = token ?? auth.token;
|
|
90
|
+
baseUrl = baseUrl ?? auth.baseUrl;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Refresh the file-backed JWT if it is within a week of expiring,
|
|
94
|
+
// but only when the caller did not pass an explicit token (in that
|
|
95
|
+
// case the caller owns its credential).
|
|
96
|
+
if (!explicitToken) {
|
|
97
|
+
const refreshed = await maybeAutoRefresh();
|
|
98
|
+
if (refreshed) token = refreshed;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const url = buildUrl(baseUrl, path);
|
|
102
|
+
const headers = {
|
|
103
|
+
Authorization: `Bearer ${token}`,
|
|
104
|
+
Accept: 'application/json',
|
|
105
|
+
...extraHeaders
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
let payload;
|
|
109
|
+
if (body !== undefined) {
|
|
110
|
+
if (body instanceof FormData || typeof body === 'string') {
|
|
111
|
+
payload = body;
|
|
112
|
+
} else {
|
|
113
|
+
payload = JSON.stringify(body);
|
|
114
|
+
headers['Content-Type'] = 'application/json';
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const response = await fetch(url, { method, headers, body: payload });
|
|
119
|
+
const contentType = response.headers.get('content-type') ?? '';
|
|
120
|
+
const isJson = contentType.includes('application/json');
|
|
121
|
+
const parsed = isJson ? await response.json().catch(() => null) : await response.text();
|
|
122
|
+
|
|
123
|
+
if (!response.ok) {
|
|
124
|
+
const message =
|
|
125
|
+
(parsed && typeof parsed === 'object' && (parsed.error || parsed.message)) ||
|
|
126
|
+
`HTTP ${response.status} ${response.statusText}`;
|
|
127
|
+
throw new ApiError(message, { status: response.status, body: parsed, url });
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
return json ? parsed : { status: response.status, body: parsed, headers: response.headers };
|
|
131
|
+
}
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from 'fs';
|
|
2
|
+
import { join } from 'path';
|
|
3
|
+
import { readAuth } from './auth-storage.js';
|
|
4
|
+
|
|
5
|
+
const WORKSPACE_CONFIG_PATHS = [
|
|
6
|
+
'.webmate/config.json',
|
|
7
|
+
'../.webmate/config.json',
|
|
8
|
+
'../../.webmate/config.json'
|
|
9
|
+
];
|
|
10
|
+
|
|
11
|
+
// Pre-release default points at staging. Switch to https://app.webmate-studio.com
|
|
12
|
+
// once the CLI sync flow is GA.
|
|
13
|
+
const DEFAULT_BASE_URL = 'https://app.webmate-studio.io';
|
|
14
|
+
|
|
15
|
+
function readWorkspaceConfigRaw(cwd = process.cwd()) {
|
|
16
|
+
for (const rel of WORKSPACE_CONFIG_PATHS) {
|
|
17
|
+
const full = join(cwd, rel);
|
|
18
|
+
if (!existsSync(full)) continue;
|
|
19
|
+
try {
|
|
20
|
+
const raw = readFileSync(full, 'utf-8');
|
|
21
|
+
const data = JSON.parse(raw);
|
|
22
|
+
if (data && typeof data === 'object') {
|
|
23
|
+
return { data, path: full };
|
|
24
|
+
}
|
|
25
|
+
} catch {
|
|
26
|
+
// ignore malformed workspace config
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
return null;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function readWorkspaceConfig(cwd = process.cwd()) {
|
|
33
|
+
const hit = readWorkspaceConfigRaw(cwd);
|
|
34
|
+
if (!hit?.data?.apiToken) return null;
|
|
35
|
+
return {
|
|
36
|
+
token: hit.data.apiToken,
|
|
37
|
+
baseUrl: hit.data.baseUrl ?? DEFAULT_BASE_URL,
|
|
38
|
+
organizationId: hit.data.organizationId ?? null,
|
|
39
|
+
repositoryId: hit.data.repositoryId ?? null,
|
|
40
|
+
path: hit.path
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Returns the project context (organizationId, repositoryId, …) from the
|
|
46
|
+
* nearest `.webmate/config.json`, regardless of whether an apiToken is set.
|
|
47
|
+
* Used by the preview server to tell the workbench frontend which project
|
|
48
|
+
* it is currently rendering, so the org-library filter and clone modal can
|
|
49
|
+
* default to that project.
|
|
50
|
+
*/
|
|
51
|
+
export function readWorkspaceContext(cwd = process.cwd()) {
|
|
52
|
+
const hit = readWorkspaceConfigRaw(cwd);
|
|
53
|
+
if (!hit?.data) return null;
|
|
54
|
+
return {
|
|
55
|
+
organizationId: hit.data.organizationId ?? null,
|
|
56
|
+
organizationSlug: hit.data.organizationSlug ?? null,
|
|
57
|
+
organizationName: hit.data.organizationName ?? null,
|
|
58
|
+
repositoryId: hit.data.repositoryId ?? null,
|
|
59
|
+
repositoryName: hit.data.repositoryName ?? null,
|
|
60
|
+
baseUrl: hit.data.baseUrl ?? null,
|
|
61
|
+
path: hit.path
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export function getDefaultBaseUrl({ cwd = process.cwd() } = {}) {
|
|
66
|
+
if (process.env.WEBMATE_BASE_URL) {
|
|
67
|
+
return { baseUrl: process.env.WEBMATE_BASE_URL, source: 'env' };
|
|
68
|
+
}
|
|
69
|
+
const ws = readWorkspaceConfigRaw(cwd);
|
|
70
|
+
if (ws?.data?.baseUrl) {
|
|
71
|
+
return { baseUrl: ws.data.baseUrl, source: 'workspace', path: ws.path };
|
|
72
|
+
}
|
|
73
|
+
return { baseUrl: DEFAULT_BASE_URL, source: 'default' };
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export function resolveAuth({ cwd = process.cwd() } = {}) {
|
|
77
|
+
// The repositoryId comes from the workspace config regardless of which
|
|
78
|
+
// token source wins — env/file tokens still want to know which project
|
|
79
|
+
// they're sitting in so `wm push` can route a first push to the right
|
|
80
|
+
// ComponentRepository.
|
|
81
|
+
const ws = readWorkspaceContext(cwd);
|
|
82
|
+
const wsExtra = {
|
|
83
|
+
repositoryId: ws?.repositoryId ?? null,
|
|
84
|
+
workspacePath: ws?.path ?? null
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
const envToken = process.env.WEBMATE_TOKEN;
|
|
88
|
+
if (envToken) {
|
|
89
|
+
return {
|
|
90
|
+
source: 'env',
|
|
91
|
+
token: envToken,
|
|
92
|
+
baseUrl: process.env.WEBMATE_BASE_URL ?? DEFAULT_BASE_URL,
|
|
93
|
+
userId: null,
|
|
94
|
+
email: null,
|
|
95
|
+
organizationId: process.env.WEBMATE_ORG_ID ?? null,
|
|
96
|
+
organizationSlug: null,
|
|
97
|
+
...wsExtra
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const fileAuth = readAuth();
|
|
102
|
+
if (fileAuth?.token) {
|
|
103
|
+
return {
|
|
104
|
+
source: 'file',
|
|
105
|
+
token: fileAuth.token,
|
|
106
|
+
baseUrl: fileAuth.baseUrl,
|
|
107
|
+
userId: fileAuth.userId,
|
|
108
|
+
email: fileAuth.email,
|
|
109
|
+
organizationId: fileAuth.organizationId,
|
|
110
|
+
organizationSlug: fileAuth.organizationSlug,
|
|
111
|
+
...wsExtra
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const workspace = readWorkspaceConfig(cwd);
|
|
116
|
+
if (workspace?.token) {
|
|
117
|
+
return {
|
|
118
|
+
source: 'workspace',
|
|
119
|
+
token: workspace.token,
|
|
120
|
+
baseUrl: workspace.baseUrl,
|
|
121
|
+
userId: null,
|
|
122
|
+
email: null,
|
|
123
|
+
organizationId: workspace.organizationId,
|
|
124
|
+
organizationSlug: null,
|
|
125
|
+
repositoryId: workspace.repositoryId,
|
|
126
|
+
workspacePath: workspace.path
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
return null;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
export function requireAuth(opts) {
|
|
134
|
+
const auth = resolveAuth(opts);
|
|
135
|
+
if (!auth) {
|
|
136
|
+
const err = new Error(
|
|
137
|
+
'No Webmate credentials found. Run `wm login` or set WEBMATE_TOKEN.'
|
|
138
|
+
);
|
|
139
|
+
err.code = 'WM_NO_AUTH';
|
|
140
|
+
throw err;
|
|
141
|
+
}
|
|
142
|
+
return auth;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
export { DEFAULT_BASE_URL };
|