handoff-app 2.0.0-1 → 2.0.0-3
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 +170 -91
- package/config/docs/foundations/colors/guidelines.md +9 -0
- package/config/docs/foundations/effects/guidelines.md +9 -0
- package/config/docs/foundations/effects/resources.md +9 -0
- package/config/docs/foundations/grid.md +10 -0
- package/config/docs/foundations/icons/guidelines.md +9 -0
- package/config/docs/foundations/images/guidelines.md +9 -0
- package/config/docs/foundations/images/resources.md +9 -0
- package/config/docs/foundations/images.md +10 -0
- package/config/docs/foundations/logo/resources.md +9 -0
- package/config/docs/foundations/typography/guidelines.md +9 -0
- package/config/docs/foundations/typography/resources.md +9 -0
- package/config/docs/foundations.md +5 -3
- package/dist/app-builder/build.js +109 -49
- package/dist/app-builder/client-config.js +13 -12
- package/dist/app-builder/paths.js +7 -9
- package/dist/app-builder/static-export.js +6 -6
- package/dist/app-builder/vercel-output.js +2 -2
- package/dist/artifacts/url.d.ts +7 -7
- package/dist/artifacts/url.js +7 -7
- package/dist/cli/auth/browser.d.ts +2 -0
- package/dist/cli/auth/browser.js +33 -0
- package/dist/cli/auth/device.d.ts +11 -0
- package/dist/cli/auth/device.js +155 -0
- package/dist/cli/auth/store.d.ts +29 -0
- package/dist/cli/auth/store.js +146 -0
- package/dist/cli/create.js +48 -3
- package/dist/cli/scaffold.js +20 -154
- package/dist/commands/checkout/index.d.ts +6 -6
- package/dist/commands/checkout/index.js +8 -12
- package/dist/commands/index.js +4 -0
- package/dist/commands/login/index.d.ts +8 -0
- package/dist/commands/login/index.js +53 -0
- package/dist/commands/logout/index.d.ts +7 -0
- package/dist/commands/logout/index.js +54 -0
- package/dist/commands/publish/index.d.ts +6 -5
- package/dist/commands/publish/index.js +8 -11
- package/dist/commands/utils.d.ts +6 -0
- package/dist/commands/utils.js +9 -1
- package/dist/config/entries.d.ts +47 -0
- package/dist/config/entries.js +162 -0
- package/dist/config/index.d.ts +2 -0
- package/dist/config/index.js +5 -1
- package/dist/config/runtime.d.ts +9 -9
- package/dist/config/runtime.js +15 -15
- package/dist/index.d.ts +12 -8
- package/dist/index.js +26 -10
- package/dist/registry/asset-storage/resolve.d.ts +1 -1
- package/dist/registry/asset-storage/resolve.js +1 -1
- package/dist/registry/auth/access-tokens.d.ts +46 -0
- package/dist/registry/auth/access-tokens.js +165 -0
- package/dist/registry/auth/action-tokens.d.ts +66 -0
- package/dist/registry/auth/action-tokens.js +178 -0
- package/dist/registry/auth/crypto.d.ts +4 -0
- package/dist/registry/auth/crypto.js +16 -0
- package/dist/registry/auth/database.d.ts +7 -0
- package/dist/registry/auth/database.js +22 -0
- package/dist/registry/auth/device-authorizations.d.ts +51 -0
- package/dist/registry/auth/device-authorizations.js +170 -0
- package/dist/registry/auth/gravatar.d.ts +1 -0
- package/dist/registry/auth/gravatar.js +8 -0
- package/dist/registry/auth/index.d.ts +11 -0
- package/dist/registry/auth/index.js +72 -0
- package/dist/registry/auth/installation.d.ts +42 -0
- package/dist/registry/auth/installation.js +105 -0
- package/dist/registry/auth/password.d.ts +12 -0
- package/dist/registry/auth/password.js +58 -0
- package/dist/registry/auth/rate-limits.d.ts +22 -0
- package/dist/registry/auth/rate-limits.js +75 -0
- package/dist/registry/auth/types.d.ts +26 -0
- package/dist/registry/auth/types.js +9 -0
- package/dist/registry/auth/user-record.d.ts +13 -0
- package/dist/registry/auth/user-record.js +16 -0
- package/dist/registry/auth/users.d.ts +31 -0
- package/dist/registry/auth/users.js +135 -0
- package/dist/registry/auth/validation.d.ts +3 -0
- package/dist/registry/auth/validation.js +28 -0
- package/dist/registry/checkout/assets.js +4 -2
- package/dist/registry/checkout/index.d.ts +11 -6
- package/dist/registry/checkout/index.js +159 -30
- package/dist/registry/checkout/tokens.js +4 -2
- package/dist/registry/client.d.ts +2 -1
- package/dist/registry/client.js +8 -0
- package/dist/registry/connection.d.ts +9 -0
- package/dist/registry/connection.js +29 -1
- package/dist/registry/db/client.d.ts +9 -9
- package/dist/registry/db/client.js +7 -7
- package/dist/registry/db/driver.d.ts +46 -0
- package/dist/registry/db/{adapter.js → driver.js} +22 -19
- package/dist/registry/db/index.d.ts +2 -2
- package/dist/registry/db/index.js +15 -9
- package/dist/registry/db/migrate.d.ts +1 -1
- package/dist/registry/db/migrate.js +5 -5
- package/dist/registry/db/schema.d.ts +2521 -750
- package/dist/registry/db/schema.js +126 -1
- package/dist/registry/publish/assets.js +1 -1
- package/dist/registry/publish/errors.js +3 -3
- package/dist/registry/publish/index.d.ts +9 -2
- package/dist/registry/publish/index.js +96 -10
- package/dist/registry/publish/tokens.js +1 -1
- package/dist/registry/transfer.d.ts +13 -0
- package/dist/types/config.d.ts +11 -4
- package/docs/cli.md +4 -0
- package/drizzle/0005_add_registry_auth.sql +95 -0
- package/drizzle/meta/0005_snapshot.json +1926 -0
- package/drizzle/meta/_journal.json +8 -1
- package/drizzle.config.ts +1 -1
- package/package.json +5 -4
- package/src/app/components/Auth/AccountLayout.tsx +87 -0
- package/src/app/components/Auth/AuthControls.tsx +99 -0
- package/src/app/components/Auth/AuthShell.tsx +73 -0
- package/src/app/components/Auth/RegistryInstallGate.tsx +23 -0
- package/src/app/components/Auth/RegistrySessionProvider.tsx +15 -0
- package/src/app/components/Auth/api.ts +20 -0
- package/src/app/components/Foundations/DisplayLogo.tsx +3 -3
- package/src/app/components/Layout/Header.tsx +2 -0
- package/src/app/components/Navigation/MobileNav.tsx +2 -0
- package/src/app/lib/auth/api.ts +113 -0
- package/src/app/lib/auth/config.ts +175 -0
- package/src/app/lib/auth/email.ts +34 -0
- package/src/app/lib/docs-api/runtime-config.ts +10 -24
- package/src/app/lib/registry-api/auth.ts +29 -46
- package/src/app/lib/registry-api/errors.ts +2 -2
- package/src/app/lib/registry-api/handler.ts +19 -14
- package/src/app/lib/registry-api/token-transfer.ts +1 -1
- package/src/app/lib/registry-api/transfer.ts +31 -2
- package/src/app/lib/registry-connection.ts +2 -2
- package/src/app/next.config.mjs +13 -4
- package/src/app/pages/_app.tsx +10 -4
- package/src/app/pages/account/index.tsx +153 -0
- package/src/app/pages/account/tokens.tsx +230 -0
- package/src/app/pages/account/users.tsx +310 -0
- package/src/app/pages/api/account/index.ts +26 -0
- package/src/app/pages/api/account/tokens/[id].ts +17 -0
- package/src/app/pages/api/account/tokens/index.ts +44 -0
- package/src/app/pages/api/admin/users/[id]/[action].ts +89 -0
- package/src/app/pages/api/admin/users/index.ts +53 -0
- package/src/app/pages/api/auth/[...nextauth].ts +4 -0
- package/src/app/pages/api/auth/request-reset.ts +53 -0
- package/src/app/pages/api/auth/reset-password.ts +29 -0
- package/src/app/pages/api/install.ts +102 -0
- package/src/app/pages/api/oauth/device/approve.ts +23 -0
- package/src/app/pages/api/oauth/device/index.ts +41 -0
- package/src/app/pages/api/oauth/revoke.ts +24 -0
- package/src/app/pages/api/oauth/token.ts +31 -0
- package/src/app/pages/api/registry/health.ts +4 -3
- package/src/app/pages/api/registry/transfer/[kind]/[id].ts +3 -3
- package/src/app/pages/api/registry/transfer/[kind]/index.ts +21 -0
- package/src/app/pages/api/registry/transfer/assets/[collection].ts +2 -1
- package/src/app/pages/api/registry/transfer/assets/blobs/[hash].ts +2 -1
- package/src/app/pages/api/registry/transfer/tokens/[...setId].ts +2 -2
- package/src/app/pages/cli/device.tsx +143 -0
- package/src/app/pages/foundations/icons/index.tsx +2 -2
- package/src/app/pages/foundations/icons/{[name] → library/[name]}/index.tsx +6 -6
- package/src/app/pages/install.tsx +336 -0
- package/src/app/pages/login.tsx +117 -0
- package/src/app/pages/reset-password.tsx +159 -0
- package/src/app/types/next-auth.d.ts +27 -0
- package/src/app-builder/build.ts +96 -59
- package/src/app-builder/client-config.ts +12 -11
- package/src/app-builder/paths.ts +7 -9
- package/src/app-builder/static-export.ts +6 -6
- package/src/app-builder/vercel-output.ts +2 -2
- package/src/artifacts/url.ts +7 -7
- package/src/cli/auth/browser.ts +29 -0
- package/src/cli/auth/device.ts +181 -0
- package/src/cli/auth/store.ts +137 -0
- package/src/cli/create.ts +58 -3
- package/src/cli/scaffold.ts +20 -197
- package/src/commands/checkout/index.ts +10 -15
- package/src/commands/index.ts +4 -0
- package/src/commands/login/index.ts +48 -0
- package/src/commands/logout/index.ts +51 -0
- package/src/commands/publish/index.ts +10 -14
- package/src/commands/utils.ts +12 -0
- package/src/config/entries.ts +175 -0
- package/src/config/index.ts +4 -0
- package/src/config/runtime.ts +15 -14
- package/src/index.ts +26 -12
- package/src/registry/asset-storage/resolve.ts +1 -1
- package/src/registry/auth/access-tokens.ts +198 -0
- package/src/registry/auth/action-tokens.ts +225 -0
- package/src/registry/auth/crypto.ts +13 -0
- package/src/registry/auth/database.ts +15 -0
- package/src/registry/auth/device-authorizations.ts +223 -0
- package/src/registry/auth/gravatar.ts +6 -0
- package/src/registry/auth/index.ts +89 -0
- package/src/registry/auth/installation.ts +117 -0
- package/src/registry/auth/password.ts +46 -0
- package/src/registry/auth/rate-limits.ts +84 -0
- package/src/registry/auth/types.ts +34 -0
- package/src/registry/auth/user-record.ts +25 -0
- package/src/registry/auth/users.ts +145 -0
- package/src/registry/auth/validation.ts +24 -0
- package/src/registry/checkout/assets.ts +4 -2
- package/src/registry/checkout/index.ts +182 -30
- package/src/registry/checkout/tokens.ts +4 -2
- package/src/registry/client.ts +7 -1
- package/src/registry/connection.ts +23 -0
- package/src/registry/db/client.ts +12 -12
- package/src/registry/db/{adapter.ts → driver.ts} +24 -21
- package/src/registry/db/index.ts +16 -4
- package/src/registry/db/migrate.ts +5 -5
- package/src/registry/db/schema.ts +174 -1
- package/src/registry/publish/assets.ts +1 -1
- package/src/registry/publish/errors.ts +3 -3
- package/src/registry/publish/index.ts +108 -10
- package/src/registry/publish/tokens.ts +1 -1
- package/src/registry/transfer.ts +14 -0
- package/src/types/config.ts +11 -4
- package/templates/default/README.md.tpl +58 -1
- package/templates/default/gitignore.tpl +19 -3
- package/templates/default/handoff.config.js.tpl +13 -0
- package/templates/default/handoff.config.ts.tpl +13 -0
- package/templates/default/handoff.config.with-examples.js.tpl +13 -0
- package/templates/default/handoff.config.with-examples.ts.tpl +13 -0
- package/templates/default/package.json.tpl +2 -1
- package/templates/default/vercel.json.tpl +4 -0
- package/dist/registry/db/adapter.d.ts +0 -43
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
3
|
+
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
4
|
+
return new (P || (P = Promise))(function (resolve, reject) {
|
|
5
|
+
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
6
|
+
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
7
|
+
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
8
|
+
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
9
|
+
});
|
|
10
|
+
};
|
|
11
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
12
|
+
exports.revokeAccessToken = exports.loginWithDevice = void 0;
|
|
13
|
+
const logger_1 = require("../../utils/logger");
|
|
14
|
+
const browser_1 = require("./browser");
|
|
15
|
+
const store_1 = require("./store");
|
|
16
|
+
const DEVICE_GRANT = 'urn:ietf:params:oauth:grant-type:device_code';
|
|
17
|
+
const DEFAULT_DEVICE_EXPIRES_IN_SECONDS = 15 * 60;
|
|
18
|
+
const DEFAULT_TOKEN_EXPIRES_IN_SECONDS = 365 * 24 * 60 * 60;
|
|
19
|
+
const DEFAULT_POLL_INTERVAL_SECONDS = 5;
|
|
20
|
+
const sleep = (milliseconds) => new Promise((resolve) => setTimeout(resolve, milliseconds));
|
|
21
|
+
const responsePayload = (value) => {
|
|
22
|
+
const envelope = value;
|
|
23
|
+
return envelope && typeof envelope === 'object' && envelope.data ? envelope.data : value;
|
|
24
|
+
};
|
|
25
|
+
const responseError = (value) => {
|
|
26
|
+
if (!value || typeof value !== 'object')
|
|
27
|
+
return {};
|
|
28
|
+
const body = value;
|
|
29
|
+
if (typeof body.error === 'string') {
|
|
30
|
+
return { code: body.error, message: body.error_description || body.error };
|
|
31
|
+
}
|
|
32
|
+
if (body.error && typeof body.error === 'object') {
|
|
33
|
+
return { code: body.error.code, message: body.error.message };
|
|
34
|
+
}
|
|
35
|
+
return {};
|
|
36
|
+
};
|
|
37
|
+
const readJson = (response) => __awaiter(void 0, void 0, void 0, function* () {
|
|
38
|
+
const text = yield response.text();
|
|
39
|
+
if (!text)
|
|
40
|
+
return {};
|
|
41
|
+
try {
|
|
42
|
+
return JSON.parse(text);
|
|
43
|
+
}
|
|
44
|
+
catch (_a) {
|
|
45
|
+
return {};
|
|
46
|
+
}
|
|
47
|
+
});
|
|
48
|
+
const shouldOpenBrowser = (options) => {
|
|
49
|
+
if (options.openBrowser === false)
|
|
50
|
+
return false;
|
|
51
|
+
if (process.env.CI === '1' || process.env.CI === 'true')
|
|
52
|
+
return false;
|
|
53
|
+
return process.env.HANDOFF_LOGIN_NO_BROWSER !== '1' && process.env.HANDOFF_LOGIN_NO_BROWSER !== 'true';
|
|
54
|
+
};
|
|
55
|
+
const positiveNumberOr = (value, fallback) => typeof value === 'number' && Number.isFinite(value) && value > 0 ? value : fallback;
|
|
56
|
+
/** Complete the device grant and persist the one-time plaintext access token for this workspace. */
|
|
57
|
+
const loginWithDevice = (workingPath_1, remoteUrl_1, ...args_1) => __awaiter(void 0, [workingPath_1, remoteUrl_1, ...args_1], void 0, function* (workingPath, remoteUrl, options = {}) {
|
|
58
|
+
const baseUrl = (0, store_1.normalizeRegistryUrl)(remoteUrl);
|
|
59
|
+
let deviceResponse;
|
|
60
|
+
try {
|
|
61
|
+
deviceResponse = yield fetch(`${baseUrl}/api/oauth/device`, {
|
|
62
|
+
method: 'POST',
|
|
63
|
+
headers: { Accept: 'application/json', 'Content-Type': 'application/json' },
|
|
64
|
+
body: '{}',
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
catch (error) {
|
|
68
|
+
throw new Error(`Could not reach the registry at ${baseUrl}: ${error instanceof Error ? error.message : String(error)}`);
|
|
69
|
+
}
|
|
70
|
+
const rawDevice = yield readJson(deviceResponse);
|
|
71
|
+
const device = responsePayload(rawDevice);
|
|
72
|
+
if (!deviceResponse.ok) {
|
|
73
|
+
const failure = responseError(rawDevice);
|
|
74
|
+
throw new Error(`Device authorization failed (${deviceResponse.status}): ${failure.message || deviceResponse.statusText}`);
|
|
75
|
+
}
|
|
76
|
+
if (!device.device_code || !device.user_code || !device.verification_uri) {
|
|
77
|
+
throw new Error('The registry returned an invalid device authorization response.');
|
|
78
|
+
}
|
|
79
|
+
const verificationUrl = (0, store_1.assertRegistryOriginUrl)(baseUrl, device.verification_uri_complete ||
|
|
80
|
+
`${device.verification_uri}${device.verification_uri.includes('?') ? '&' : '?'}user_code=${encodeURIComponent(device.user_code)}`);
|
|
81
|
+
logger_1.Logger.log('');
|
|
82
|
+
logger_1.Logger.log('Approve this device in your browser:');
|
|
83
|
+
logger_1.Logger.log(verificationUrl);
|
|
84
|
+
logger_1.Logger.log('');
|
|
85
|
+
logger_1.Logger.log(`Device code: ${device.user_code}`);
|
|
86
|
+
logger_1.Logger.log('');
|
|
87
|
+
if (shouldOpenBrowser(options)) {
|
|
88
|
+
(0, browser_1.tryOpenBrowser)(verificationUrl);
|
|
89
|
+
logger_1.Logger.info('Attempted to open your default browser.');
|
|
90
|
+
}
|
|
91
|
+
const deadline = Date.now() + positiveNumberOr(device.expires_in, DEFAULT_DEVICE_EXPIRES_IN_SECONDS) * 1000;
|
|
92
|
+
let intervalMs = positiveNumberOr(device.interval, DEFAULT_POLL_INTERVAL_SECONDS) * 1000;
|
|
93
|
+
while (Date.now() < deadline) {
|
|
94
|
+
yield sleep(Math.min(intervalMs, Math.max(0, deadline - Date.now())));
|
|
95
|
+
let tokenResponse;
|
|
96
|
+
try {
|
|
97
|
+
tokenResponse = yield fetch(`${baseUrl}/api/oauth/token`, {
|
|
98
|
+
method: 'POST',
|
|
99
|
+
headers: { Accept: 'application/json', 'Content-Type': 'application/json' },
|
|
100
|
+
body: JSON.stringify({
|
|
101
|
+
grant_type: DEVICE_GRANT,
|
|
102
|
+
device_code: device.device_code,
|
|
103
|
+
}),
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
catch (error) {
|
|
107
|
+
throw new Error(`Could not reach the registry at ${baseUrl}: ${error instanceof Error ? error.message : String(error)}`);
|
|
108
|
+
}
|
|
109
|
+
const rawToken = yield readJson(tokenResponse);
|
|
110
|
+
const token = responsePayload(rawToken);
|
|
111
|
+
if (tokenResponse.ok && token.access_token) {
|
|
112
|
+
const expiresIn = positiveNumberOr(token.expires_in, DEFAULT_TOKEN_EXPIRES_IN_SECONDS);
|
|
113
|
+
const auth = {
|
|
114
|
+
remoteUrl: baseUrl,
|
|
115
|
+
accessToken: token.access_token,
|
|
116
|
+
expiresAtMs: Date.now() + expiresIn * 1000,
|
|
117
|
+
};
|
|
118
|
+
yield (0, store_1.writeCliAuth)(workingPath, auth);
|
|
119
|
+
return auth;
|
|
120
|
+
}
|
|
121
|
+
const failure = responseError(token);
|
|
122
|
+
if (failure.code === 'authorization_pending')
|
|
123
|
+
continue;
|
|
124
|
+
if (failure.code === 'slow_down') {
|
|
125
|
+
intervalMs = Math.min(intervalMs + 5000, 60000);
|
|
126
|
+
continue;
|
|
127
|
+
}
|
|
128
|
+
if (failure.code === 'expired_token')
|
|
129
|
+
break;
|
|
130
|
+
throw new Error(`Token request failed: ${failure.message || tokenResponse.statusText || 'unknown error'}`);
|
|
131
|
+
}
|
|
132
|
+
throw new Error('Device authorization expired. Run `handoff-app login` again.');
|
|
133
|
+
});
|
|
134
|
+
exports.loginWithDevice = loginWithDevice;
|
|
135
|
+
/**
|
|
136
|
+
* Best-effort remote revocation. The caller must clear the local credential regardless of failure.
|
|
137
|
+
* The bearer header identifies the current token while the body supports RFC 7009-style handlers.
|
|
138
|
+
*/
|
|
139
|
+
const revokeAccessToken = (auth) => __awaiter(void 0, void 0, void 0, function* () {
|
|
140
|
+
const response = yield fetch(`${(0, store_1.normalizeRegistryUrl)(auth.remoteUrl)}/api/oauth/revoke`, {
|
|
141
|
+
method: 'POST',
|
|
142
|
+
headers: {
|
|
143
|
+
Accept: 'application/json',
|
|
144
|
+
Authorization: `Bearer ${auth.accessToken}`,
|
|
145
|
+
'Content-Type': 'application/json',
|
|
146
|
+
},
|
|
147
|
+
body: JSON.stringify({ token: auth.accessToken }),
|
|
148
|
+
});
|
|
149
|
+
if (!response.ok) {
|
|
150
|
+
const body = yield readJson(response);
|
|
151
|
+
const failure = responseError(body);
|
|
152
|
+
throw new Error(failure.message || `${response.status} ${response.statusText}`);
|
|
153
|
+
}
|
|
154
|
+
});
|
|
155
|
+
exports.revokeAccessToken = revokeAccessToken;
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/** Credentials issued to this workspace by one registry. */
|
|
2
|
+
export interface CliAuth {
|
|
3
|
+
remoteUrl: string;
|
|
4
|
+
accessToken: string;
|
|
5
|
+
/** Epoch milliseconds after which the token must not be used. */
|
|
6
|
+
expiresAtMs: number;
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* Canonicalize a registry URL for both HTTP requests and exact credential matching.
|
|
10
|
+
* Base paths are retained; query strings, fragments, credentials, and trailing slashes are not.
|
|
11
|
+
*/
|
|
12
|
+
export declare const normalizeRegistryUrl: (value: string) => string;
|
|
13
|
+
/**
|
|
14
|
+
* A verification URL is only trustworthy if it's a web URL on the same origin as the
|
|
15
|
+
* registry the CLI is authenticating against. This stops a tampered device-authorization
|
|
16
|
+
* response from sending the browser to (or injecting a command via) another origin.
|
|
17
|
+
*/
|
|
18
|
+
export declare const assertRegistryOriginUrl: (baseUrl: string, candidate: string) => string;
|
|
19
|
+
export declare const cliAuthFilePath: (workingPath: string) => string;
|
|
20
|
+
/** Invalid or partial credential files are treated as unauthenticated. */
|
|
21
|
+
export declare const readCliAuth: (workingPath: string) => Promise<CliAuth | null>;
|
|
22
|
+
/**
|
|
23
|
+
* Atomically replace the credential file. Restrictive permissions are best-effort on platforms
|
|
24
|
+
* whose filesystems do not support POSIX modes.
|
|
25
|
+
*/
|
|
26
|
+
export declare const writeCliAuth: (workingPath: string, auth: CliAuth) => Promise<void>;
|
|
27
|
+
export declare const clearCliAuth: (workingPath: string) => Promise<void>;
|
|
28
|
+
export declare const cliAuthMatchesRegistry: (auth: CliAuth | null, remoteUrl: string) => boolean;
|
|
29
|
+
export declare const cliAuthTokenIsValid: (auth: CliAuth | null, skewMs?: number) => boolean;
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
3
|
+
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
4
|
+
return new (P || (P = Promise))(function (resolve, reject) {
|
|
5
|
+
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
6
|
+
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
7
|
+
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
8
|
+
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
9
|
+
});
|
|
10
|
+
};
|
|
11
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
12
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
13
|
+
};
|
|
14
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
15
|
+
exports.cliAuthTokenIsValid = exports.cliAuthMatchesRegistry = exports.clearCliAuth = exports.writeCliAuth = exports.readCliAuth = exports.cliAuthFilePath = exports.assertRegistryOriginUrl = exports.normalizeRegistryUrl = void 0;
|
|
16
|
+
const fs_extra_1 = __importDefault(require("fs-extra"));
|
|
17
|
+
const path_1 = __importDefault(require("path"));
|
|
18
|
+
const AUTH_DIRECTORY = '.handoff';
|
|
19
|
+
const AUTH_FILE = 'cli-auth.json';
|
|
20
|
+
/**
|
|
21
|
+
* Canonicalize a registry URL for both HTTP requests and exact credential matching.
|
|
22
|
+
* Base paths are retained; query strings, fragments, credentials, and trailing slashes are not.
|
|
23
|
+
*/
|
|
24
|
+
const normalizeRegistryUrl = (value) => {
|
|
25
|
+
let parsed;
|
|
26
|
+
try {
|
|
27
|
+
parsed = new URL(value.trim());
|
|
28
|
+
}
|
|
29
|
+
catch (_a) {
|
|
30
|
+
throw new Error(`Invalid registry URL "${value}". Pass an absolute http:// or https:// URL.`);
|
|
31
|
+
}
|
|
32
|
+
if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:') {
|
|
33
|
+
throw new Error(`Invalid registry URL protocol "${parsed.protocol}". Use http:// or https://.`);
|
|
34
|
+
}
|
|
35
|
+
if (parsed.username || parsed.password) {
|
|
36
|
+
throw new Error('Registry URLs must not contain credentials.');
|
|
37
|
+
}
|
|
38
|
+
const pathname = parsed.pathname.replace(/\/+$/, '');
|
|
39
|
+
return `${parsed.origin}${pathname === '/' ? '' : pathname}`;
|
|
40
|
+
};
|
|
41
|
+
exports.normalizeRegistryUrl = normalizeRegistryUrl;
|
|
42
|
+
/**
|
|
43
|
+
* A verification URL is only trustworthy if it's a web URL on the same origin as the
|
|
44
|
+
* registry the CLI is authenticating against. This stops a tampered device-authorization
|
|
45
|
+
* response from sending the browser to (or injecting a command via) another origin.
|
|
46
|
+
*/
|
|
47
|
+
const assertRegistryOriginUrl = (baseUrl, candidate) => {
|
|
48
|
+
let url;
|
|
49
|
+
try {
|
|
50
|
+
url = new URL(candidate);
|
|
51
|
+
}
|
|
52
|
+
catch (_a) {
|
|
53
|
+
throw new Error('The registry returned an invalid verification URL.');
|
|
54
|
+
}
|
|
55
|
+
if (url.protocol !== 'http:' && url.protocol !== 'https:') {
|
|
56
|
+
throw new Error(`The registry returned a verification URL with an unsupported protocol "${url.protocol}".`);
|
|
57
|
+
}
|
|
58
|
+
if (url.origin !== new URL(baseUrl).origin) {
|
|
59
|
+
throw new Error('The registry returned a verification URL for a different origin than the registry.');
|
|
60
|
+
}
|
|
61
|
+
return url.toString();
|
|
62
|
+
};
|
|
63
|
+
exports.assertRegistryOriginUrl = assertRegistryOriginUrl;
|
|
64
|
+
const cliAuthFilePath = (workingPath) => path_1.default.resolve(workingPath, AUTH_DIRECTORY, AUTH_FILE);
|
|
65
|
+
exports.cliAuthFilePath = cliAuthFilePath;
|
|
66
|
+
const isCliAuth = (value) => {
|
|
67
|
+
if (!value || typeof value !== 'object')
|
|
68
|
+
return false;
|
|
69
|
+
const auth = value;
|
|
70
|
+
return (typeof auth.remoteUrl === 'string' &&
|
|
71
|
+
typeof auth.accessToken === 'string' &&
|
|
72
|
+
auth.accessToken.length > 0 &&
|
|
73
|
+
typeof auth.expiresAtMs === 'number' &&
|
|
74
|
+
Number.isFinite(auth.expiresAtMs));
|
|
75
|
+
};
|
|
76
|
+
/** Invalid or partial credential files are treated as unauthenticated. */
|
|
77
|
+
const readCliAuth = (workingPath) => __awaiter(void 0, void 0, void 0, function* () {
|
|
78
|
+
try {
|
|
79
|
+
const value = (yield fs_extra_1.default.readJson((0, exports.cliAuthFilePath)(workingPath)));
|
|
80
|
+
if (!isCliAuth(value))
|
|
81
|
+
return null;
|
|
82
|
+
return Object.assign(Object.assign({}, value), { remoteUrl: (0, exports.normalizeRegistryUrl)(value.remoteUrl) });
|
|
83
|
+
}
|
|
84
|
+
catch (_a) {
|
|
85
|
+
return null;
|
|
86
|
+
}
|
|
87
|
+
});
|
|
88
|
+
exports.readCliAuth = readCliAuth;
|
|
89
|
+
/**
|
|
90
|
+
* Atomically replace the credential file. Restrictive permissions are best-effort on platforms
|
|
91
|
+
* whose filesystems do not support POSIX modes.
|
|
92
|
+
*/
|
|
93
|
+
const writeCliAuth = (workingPath, auth) => __awaiter(void 0, void 0, void 0, function* () {
|
|
94
|
+
const filePath = (0, exports.cliAuthFilePath)(workingPath);
|
|
95
|
+
const directory = path_1.default.dirname(filePath);
|
|
96
|
+
const temporaryPath = `${filePath}.${process.pid}.${Date.now()}.tmp`;
|
|
97
|
+
const value = Object.assign(Object.assign({}, auth), { remoteUrl: (0, exports.normalizeRegistryUrl)(auth.remoteUrl) });
|
|
98
|
+
yield fs_extra_1.default.ensureDir(directory, 0o700);
|
|
99
|
+
try {
|
|
100
|
+
yield fs_extra_1.default.chmod(directory, 0o700);
|
|
101
|
+
}
|
|
102
|
+
catch (_a) {
|
|
103
|
+
// Windows and some mounted filesystems do not implement POSIX modes.
|
|
104
|
+
}
|
|
105
|
+
try {
|
|
106
|
+
yield fs_extra_1.default.writeFile(temporaryPath, `${JSON.stringify(value, null, 2)}\n`, { encoding: 'utf8', mode: 0o600 });
|
|
107
|
+
try {
|
|
108
|
+
yield fs_extra_1.default.chmod(temporaryPath, 0o600);
|
|
109
|
+
}
|
|
110
|
+
catch (_b) {
|
|
111
|
+
// Best effort; see the directory chmod above.
|
|
112
|
+
}
|
|
113
|
+
yield fs_extra_1.default.rename(temporaryPath, filePath);
|
|
114
|
+
try {
|
|
115
|
+
yield fs_extra_1.default.chmod(filePath, 0o600);
|
|
116
|
+
}
|
|
117
|
+
catch (_c) {
|
|
118
|
+
// Best effort.
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
catch (error) {
|
|
122
|
+
yield fs_extra_1.default.remove(temporaryPath).catch(() => undefined);
|
|
123
|
+
throw error;
|
|
124
|
+
}
|
|
125
|
+
});
|
|
126
|
+
exports.writeCliAuth = writeCliAuth;
|
|
127
|
+
const clearCliAuth = (workingPath) => __awaiter(void 0, void 0, void 0, function* () {
|
|
128
|
+
const filePath = (0, exports.cliAuthFilePath)(workingPath);
|
|
129
|
+
if (yield fs_extra_1.default.pathExists(filePath)) {
|
|
130
|
+
yield fs_extra_1.default.remove(filePath);
|
|
131
|
+
}
|
|
132
|
+
});
|
|
133
|
+
exports.clearCliAuth = clearCliAuth;
|
|
134
|
+
const cliAuthMatchesRegistry = (auth, remoteUrl) => {
|
|
135
|
+
if (!auth)
|
|
136
|
+
return false;
|
|
137
|
+
try {
|
|
138
|
+
return auth.remoteUrl === (0, exports.normalizeRegistryUrl)(remoteUrl);
|
|
139
|
+
}
|
|
140
|
+
catch (_a) {
|
|
141
|
+
return false;
|
|
142
|
+
}
|
|
143
|
+
};
|
|
144
|
+
exports.cliAuthMatchesRegistry = cliAuthMatchesRegistry;
|
|
145
|
+
const cliAuthTokenIsValid = (auth, skewMs = 30000) => Boolean((auth === null || auth === void 0 ? void 0 : auth.accessToken) && auth.expiresAtMs > Date.now() + skewMs);
|
|
146
|
+
exports.cliAuthTokenIsValid = cliAuthTokenIsValid;
|
package/dist/cli/create.js
CHANGED
|
@@ -85,7 +85,7 @@ const createDirectory = (dirPath) => {
|
|
|
85
85
|
* Get mandatory templates based on whether examples are included
|
|
86
86
|
* Uses different handoff config templates for blank vs. with-examples projects
|
|
87
87
|
*/
|
|
88
|
-
const getMandatoryTemplates = (includeExamples, languagePreference) => {
|
|
88
|
+
const getMandatoryTemplates = (includeExamples, languagePreference, includeVercel) => {
|
|
89
89
|
const configFormat = languagePreference;
|
|
90
90
|
const configTemplateBaseName = includeExamples ? 'handoff.config.with-examples' : 'handoff.config';
|
|
91
91
|
const configTemplateSource = `${configTemplateBaseName}.${configFormat === 'typescript' ? 'ts' : 'js'}.tpl`;
|
|
@@ -103,6 +103,9 @@ const getMandatoryTemplates = (includeExamples, languagePreference) => {
|
|
|
103
103
|
if (languagePreference === 'typescript') {
|
|
104
104
|
templates.push({ source: 'tsconfig.json.tpl', destination: 'tsconfig.json' });
|
|
105
105
|
}
|
|
106
|
+
if (includeVercel) {
|
|
107
|
+
templates.push({ source: 'vercel.json.tpl', destination: 'vercel.json' });
|
|
108
|
+
}
|
|
106
109
|
return templates;
|
|
107
110
|
};
|
|
108
111
|
const getExampleTemplates = (languagePreference) => {
|
|
@@ -127,7 +130,8 @@ const renderTemplate = (content, data) => {
|
|
|
127
130
|
});
|
|
128
131
|
};
|
|
129
132
|
const getDependenciesToInstall = (options) => {
|
|
130
|
-
|
|
133
|
+
// TODO: switch back to `handoff-app@latest` before the 2.0 stable release.
|
|
134
|
+
const dependencies = ['handoff-app@next'];
|
|
131
135
|
const devDependencies = [];
|
|
132
136
|
if (options.includeExamples) {
|
|
133
137
|
dependencies.push('react', 'react-dom');
|
|
@@ -250,6 +254,45 @@ const create = () => __awaiter(void 0, void 0, void 0, function* () {
|
|
|
250
254
|
process.exit(0);
|
|
251
255
|
}
|
|
252
256
|
const languagePreference = languagePreferenceSelection;
|
|
257
|
+
const includeVercelSelection = yield p.confirm({
|
|
258
|
+
message: 'Add a Vercel deployment config (vercel.json)?',
|
|
259
|
+
initialValue: true,
|
|
260
|
+
});
|
|
261
|
+
if (p.isCancel(includeVercelSelection)) {
|
|
262
|
+
p.cancel('Project creation cancelled.');
|
|
263
|
+
process.exit(0);
|
|
264
|
+
}
|
|
265
|
+
const includeVercel = includeVercelSelection;
|
|
266
|
+
// When a Vercel config is scaffolded, choose which deployment its build command produces. Both
|
|
267
|
+
// package to `.vercel/output`; they differ in what is served. Registry is the recommended,
|
|
268
|
+
// pre-selected option; static stays available as the simplest deploy.
|
|
269
|
+
let vercelBuildCommand = 'npm run build -- --target registry --package vercel';
|
|
270
|
+
if (includeVercel) {
|
|
271
|
+
const vercelDeploymentSelection = yield p.select({
|
|
272
|
+
message: 'Which deployment should Vercel build?',
|
|
273
|
+
options: [
|
|
274
|
+
{
|
|
275
|
+
value: 'registry',
|
|
276
|
+
label: 'Registry (dynamic)',
|
|
277
|
+
hint: 'Live, database-backed app (needs a Postgres DATABASE_URL). The more capable, actively evolving option — recommended for a shared design system.',
|
|
278
|
+
},
|
|
279
|
+
{
|
|
280
|
+
value: 'static',
|
|
281
|
+
label: 'Static site',
|
|
282
|
+
hint: 'Prebuilt snapshot served as plain files. The simplest to deploy and run.',
|
|
283
|
+
},
|
|
284
|
+
],
|
|
285
|
+
initialValue: 'registry',
|
|
286
|
+
});
|
|
287
|
+
if (p.isCancel(vercelDeploymentSelection)) {
|
|
288
|
+
p.cancel('Project creation cancelled.');
|
|
289
|
+
process.exit(0);
|
|
290
|
+
}
|
|
291
|
+
vercelBuildCommand =
|
|
292
|
+
vercelDeploymentSelection === 'registry'
|
|
293
|
+
? 'npm run build -- --target registry --package vercel'
|
|
294
|
+
: 'npm run build -- --target static --package vercel';
|
|
295
|
+
}
|
|
253
296
|
// Get Figma project ID
|
|
254
297
|
p.log.step(chalk_1.default.blue('Figma Configuration'));
|
|
255
298
|
const figmaProjectId = yield p.text({
|
|
@@ -277,6 +320,8 @@ const create = () => __awaiter(void 0, void 0, void 0, function* () {
|
|
|
277
320
|
figmaAccessToken: (figmaAccessToken || '').trim(),
|
|
278
321
|
languagePreference,
|
|
279
322
|
includeExamples,
|
|
323
|
+
includeVercel,
|
|
324
|
+
vercelBuildCommand,
|
|
280
325
|
};
|
|
281
326
|
// Create files
|
|
282
327
|
const spinner = p.spinner();
|
|
@@ -287,7 +332,7 @@ const create = () => __awaiter(void 0, void 0, void 0, function* () {
|
|
|
287
332
|
}
|
|
288
333
|
// Combine mandatory templates with example templates based on user choice
|
|
289
334
|
const templates = [
|
|
290
|
-
...getMandatoryTemplates(includeExamples, languagePreference),
|
|
335
|
+
...getMandatoryTemplates(includeExamples, languagePreference, includeVercel),
|
|
291
336
|
...(includeExamples ? getExampleTemplates(languagePreference) : []),
|
|
292
337
|
];
|
|
293
338
|
try {
|
package/dist/cli/scaffold.js
CHANGED
|
@@ -40,8 +40,8 @@ const p = __importStar(require("@clack/prompts"));
|
|
|
40
40
|
const chalk_1 = __importDefault(require("chalk"));
|
|
41
41
|
const fs_extra_1 = __importDefault(require("fs-extra"));
|
|
42
42
|
const path_1 = __importDefault(require("path"));
|
|
43
|
+
const entries_1 = require("../config/entries");
|
|
43
44
|
// Constants
|
|
44
|
-
const CONFIG_FILES = ['handoff.config.ts', 'handoff.config.js', 'handoff.config.cjs', 'handoff.config.json'];
|
|
45
45
|
const COMPONENTS_DIR = 'components';
|
|
46
46
|
const DEFAULT_GROUPS = [
|
|
47
47
|
{ value: 'Atomic Elements', label: 'Atomic Elements' },
|
|
@@ -197,142 +197,6 @@ const countMatchingRegisteredComponents = (figmaComponents, registeredIds) => {
|
|
|
197
197
|
const figmaSet = new Set(figmaComponents.map((c) => c.name.toLowerCase()));
|
|
198
198
|
return registeredIds.filter((id) => figmaSet.has(id.toLowerCase())).length;
|
|
199
199
|
};
|
|
200
|
-
/**
|
|
201
|
-
* Check if components will be auto-loaded by an existing entry
|
|
202
|
-
* e.g., if 'components' directory is listed, all subdirectories are auto-loaded
|
|
203
|
-
*/
|
|
204
|
-
const willComponentsAutoLoad = (existingPaths) => {
|
|
205
|
-
// Normalize paths for comparison
|
|
206
|
-
const normalizedPaths = existingPaths.map((p) => p.replace(/\/$/, '').toLowerCase());
|
|
207
|
-
// If 'components' directory itself is in the list, all components auto-load
|
|
208
|
-
return normalizedPaths.includes(COMPONENTS_DIR);
|
|
209
|
-
};
|
|
210
|
-
/**
|
|
211
|
-
* Format component paths as a properly indented array string for JS files
|
|
212
|
-
*/
|
|
213
|
-
const formatComponentsArray = (paths, indent = ' ') => {
|
|
214
|
-
if (paths.length === 0)
|
|
215
|
-
return '[]';
|
|
216
|
-
if (paths.length === 1)
|
|
217
|
-
return `['${paths[0]}']`;
|
|
218
|
-
return `[\n${paths.map((p) => `${indent}'${p}',`).join('\n')}\n${indent.slice(2)}]`;
|
|
219
|
-
};
|
|
220
|
-
/**
|
|
221
|
-
* Update handoff config file to add component paths
|
|
222
|
-
*/
|
|
223
|
-
const updateConfigFile = (handoff, componentNames) => __awaiter(void 0, void 0, void 0, function* () {
|
|
224
|
-
const configFile = CONFIG_FILES.find((file) => fs_extra_1.default.existsSync(path_1.default.resolve(handoff.workingPath, file)));
|
|
225
|
-
const newComponentPaths = componentNames.map((name) => `${COMPONENTS_DIR}/${name}`);
|
|
226
|
-
if (!configFile) {
|
|
227
|
-
// Create a new handoff.config.json
|
|
228
|
-
const newConfig = {
|
|
229
|
-
entries: {
|
|
230
|
-
components: newComponentPaths,
|
|
231
|
-
},
|
|
232
|
-
};
|
|
233
|
-
const configPath = path_1.default.resolve(handoff.workingPath, 'handoff.config.json');
|
|
234
|
-
yield fs_extra_1.default.writeJSON(configPath, newConfig, { spaces: 2 });
|
|
235
|
-
return { success: true, isJsConfig: false, configPath, skipped: false };
|
|
236
|
-
}
|
|
237
|
-
const configPath = path_1.default.resolve(handoff.workingPath, configFile);
|
|
238
|
-
if (configFile.endsWith('.json')) {
|
|
239
|
-
const config = yield fs_extra_1.default.readJSON(configPath);
|
|
240
|
-
// Initialize entries if not present
|
|
241
|
-
if (!config.entries) {
|
|
242
|
-
config.entries = {};
|
|
243
|
-
}
|
|
244
|
-
if (!config.entries.components) {
|
|
245
|
-
config.entries.components = [];
|
|
246
|
-
}
|
|
247
|
-
// Check if components will auto-load
|
|
248
|
-
if (willComponentsAutoLoad(config.entries.components)) {
|
|
249
|
-
return { success: true, isJsConfig: false, configPath, skipped: true };
|
|
250
|
-
}
|
|
251
|
-
// Add new component paths
|
|
252
|
-
const existingPaths = new Set(config.entries.components);
|
|
253
|
-
for (const componentPath of newComponentPaths) {
|
|
254
|
-
if (!existingPaths.has(componentPath)) {
|
|
255
|
-
config.entries.components.push(componentPath);
|
|
256
|
-
}
|
|
257
|
-
}
|
|
258
|
-
yield fs_extra_1.default.writeJSON(configPath, config, { spaces: 2 });
|
|
259
|
-
return { success: true, isJsConfig: false, configPath, skipped: false };
|
|
260
|
-
}
|
|
261
|
-
// For code config files, try to update them programmatically
|
|
262
|
-
if (configFile.endsWith('.ts') || configFile.endsWith('.js') || configFile.endsWith('.cjs')) {
|
|
263
|
-
try {
|
|
264
|
-
let content = yield fs_extra_1.default.readFile(configPath, 'utf8');
|
|
265
|
-
// Check if entries.components already exists
|
|
266
|
-
const hasEntriesComponents = /entries\s*:\s*\{[\s\S]*?components\s*:/.test(content);
|
|
267
|
-
if (hasEntriesComponents) {
|
|
268
|
-
// Find and update the components array
|
|
269
|
-
// Match: components: [...] or components: ["..."]
|
|
270
|
-
const componentsArrayRegex = /(components\s*:\s*\[)([^\]]*)(\])/;
|
|
271
|
-
const match = content.match(componentsArrayRegex);
|
|
272
|
-
if (match) {
|
|
273
|
-
const existingContent = match[2].trim();
|
|
274
|
-
const existingPaths = existingContent
|
|
275
|
-
.split(',')
|
|
276
|
-
.map((s) => s.trim().replace(/['"]/g, ''))
|
|
277
|
-
.filter((s) => s.length > 0);
|
|
278
|
-
// Check if components will auto-load
|
|
279
|
-
if (willComponentsAutoLoad(existingPaths)) {
|
|
280
|
-
return { success: true, isJsConfig: true, configPath, skipped: true };
|
|
281
|
-
}
|
|
282
|
-
const existingSet = new Set(existingPaths);
|
|
283
|
-
const pathsToAdd = newComponentPaths.filter((p) => !existingSet.has(p));
|
|
284
|
-
if (pathsToAdd.length > 0) {
|
|
285
|
-
const allPaths = [...existingPaths, ...pathsToAdd];
|
|
286
|
-
// Format with proper indentation (detect existing indentation)
|
|
287
|
-
const indentMatch = content.match(/components\s*:\s*\[\s*\n(\s*)/);
|
|
288
|
-
const indent = indentMatch ? indentMatch[1] : ' ';
|
|
289
|
-
const formattedArray = formatComponentsArray(allPaths, indent);
|
|
290
|
-
content = content.replace(componentsArrayRegex, `components: ${formattedArray}`);
|
|
291
|
-
yield fs_extra_1.default.writeFile(configPath, content, 'utf8');
|
|
292
|
-
}
|
|
293
|
-
return { success: true, isJsConfig: true, configPath, skipped: false };
|
|
294
|
-
}
|
|
295
|
-
}
|
|
296
|
-
// If entries exists but no components, or no entries at all
|
|
297
|
-
// Try to add the entries.components section
|
|
298
|
-
const hasEntries = /entries\s*:\s*\{/.test(content);
|
|
299
|
-
if (hasEntries) {
|
|
300
|
-
// Add components to existing entries object with proper formatting
|
|
301
|
-
const formattedArray = formatComponentsArray(newComponentPaths);
|
|
302
|
-
content = content.replace(/(entries\s*:\s*\{)/, `$1\n components: ${formattedArray},`);
|
|
303
|
-
yield fs_extra_1.default.writeFile(configPath, content, 'utf8');
|
|
304
|
-
return { success: true, isJsConfig: true, configPath, skipped: false };
|
|
305
|
-
}
|
|
306
|
-
// No entries object, try to add it before the closing of exported object
|
|
307
|
-
const formattedArray = formatComponentsArray(newComponentPaths);
|
|
308
|
-
const entriesBlock = ` entries: {\n components: ${formattedArray},\n },\n`;
|
|
309
|
-
// Try to insert after module.exports = {
|
|
310
|
-
if (content.includes('module.exports = {')) {
|
|
311
|
-
content = content.replace(/(module\.exports\s*=\s*\{)/, `$1\n${entriesBlock}`);
|
|
312
|
-
yield fs_extra_1.default.writeFile(configPath, content, 'utf8');
|
|
313
|
-
return { success: true, isJsConfig: true, configPath, skipped: false };
|
|
314
|
-
}
|
|
315
|
-
// Try to insert after export default {
|
|
316
|
-
if (/export\s+default\s+\{/.test(content)) {
|
|
317
|
-
content = content.replace(/(export\s+default\s+\{)/, `$1\n${entriesBlock}`);
|
|
318
|
-
yield fs_extra_1.default.writeFile(configPath, content, 'utf8');
|
|
319
|
-
return { success: true, isJsConfig: true, configPath, skipped: false };
|
|
320
|
-
}
|
|
321
|
-
// Try to insert into defineConfig({ ... })
|
|
322
|
-
if (/defineConfig\s*\(\s*\{/.test(content)) {
|
|
323
|
-
content = content.replace(/(defineConfig\s*\(\s*\{)/, `$1\n${entriesBlock}`);
|
|
324
|
-
yield fs_extra_1.default.writeFile(configPath, content, 'utf8');
|
|
325
|
-
return { success: true, isJsConfig: true, configPath, skipped: false };
|
|
326
|
-
}
|
|
327
|
-
// Fallback: couldn't parse the JS config
|
|
328
|
-
return { success: false, isJsConfig: true, configPath, skipped: false };
|
|
329
|
-
}
|
|
330
|
-
catch (_a) {
|
|
331
|
-
return { success: false, isJsConfig: true, configPath, skipped: false };
|
|
332
|
-
}
|
|
333
|
-
}
|
|
334
|
-
return { success: false, isJsConfig: false, configPath, skipped: false };
|
|
335
|
-
});
|
|
336
200
|
/**
|
|
337
201
|
* Create component files
|
|
338
202
|
*/
|
|
@@ -513,18 +377,6 @@ const runScaffold = (handoff) => __awaiter(void 0, void 0, void 0, function* ()
|
|
|
513
377
|
allCreatedFiles.push(...createdFiles);
|
|
514
378
|
componentNames.push(config.name);
|
|
515
379
|
}
|
|
516
|
-
// Update config if requested
|
|
517
|
-
let configUpdateResult = null;
|
|
518
|
-
if (updateConfig) {
|
|
519
|
-
configUpdateResult = yield updateConfigFile(handoff, componentNames);
|
|
520
|
-
if (!configUpdateResult.success) {
|
|
521
|
-
const relativePath = path_1.default.relative(handoff.workingPath, configUpdateResult.configPath);
|
|
522
|
-
p.log.warn(`Could not automatically update ${relativePath}. Please manually add these paths to entries.components:`);
|
|
523
|
-
for (const name of componentNames) {
|
|
524
|
-
console.log(chalk_1.default.yellow(` 'components/${name}'`));
|
|
525
|
-
}
|
|
526
|
-
}
|
|
527
|
-
}
|
|
528
380
|
generationSpinner.stop('Files created successfully');
|
|
529
381
|
// Display summary
|
|
530
382
|
p.log.success('Created files:');
|
|
@@ -532,13 +384,27 @@ const runScaffold = (handoff) => __awaiter(void 0, void 0, void 0, function* ()
|
|
|
532
384
|
const relativePath = path_1.default.relative(handoff.workingPath, file);
|
|
533
385
|
console.log(chalk_1.default.dim(` ${relativePath}`));
|
|
534
386
|
}
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
387
|
+
// Update config if requested. Components already covered by a collection directory auto-load,
|
|
388
|
+
// so we only write the uncovered ones into entries.components.
|
|
389
|
+
if (updateConfig) {
|
|
390
|
+
const componentDirs = componentNames.map((name) => path_1.default.resolve(handoff.workingPath, COMPONENTS_DIR, name));
|
|
391
|
+
const uncovered = componentDirs.filter((dir) => !(0, entries_1.isEntryCovered)(handoff, 'components', dir));
|
|
392
|
+
if (uncovered.length === 0) {
|
|
393
|
+
p.log.info(`Config already covers these components - they'll auto-load`);
|
|
539
394
|
}
|
|
540
395
|
else {
|
|
541
|
-
|
|
396
|
+
const result = yield (0, entries_1.writeEntries)(handoff, 'components', uncovered);
|
|
397
|
+
if (result.status === 'added') {
|
|
398
|
+
const configFileName = result.configPath ? path_1.default.basename(result.configPath) : 'handoff.config.json';
|
|
399
|
+
p.log.success(`Updated ${configFileName} with component paths`);
|
|
400
|
+
}
|
|
401
|
+
else {
|
|
402
|
+
const where = result.configPath ? path_1.default.relative(handoff.workingPath, result.configPath) : 'handoff.config';
|
|
403
|
+
p.log.warn(`Could not automatically update ${where}. Please manually add these paths to entries.components:`);
|
|
404
|
+
for (const rel of result.pending) {
|
|
405
|
+
console.log(chalk_1.default.yellow(` '${rel}'`));
|
|
406
|
+
}
|
|
407
|
+
}
|
|
542
408
|
}
|
|
543
409
|
}
|
|
544
410
|
p.outro(chalk_1.default.green(`Successfully created ${componentConfigs.length} component stub(s)!`));
|
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
import { CommandModule } from 'yargs';
|
|
2
2
|
import { SharedArgs } from '../types';
|
|
3
|
-
|
|
4
|
-
declare const ENTITY_KINDS: readonly ["component", "pattern", "page", "tokens", "assets"];
|
|
3
|
+
import { RegistryEntityKind } from '../utils';
|
|
5
4
|
export interface CheckoutArgs extends SharedArgs {
|
|
6
|
-
type:
|
|
5
|
+
type: RegistryEntityKind;
|
|
7
6
|
id?: string;
|
|
8
7
|
}
|
|
9
8
|
/**
|
|
10
|
-
* `handoff-app checkout <
|
|
11
|
-
* into this workspace
|
|
12
|
-
*
|
|
9
|
+
* `handoff-app checkout <components|patterns|pages> [id]` pulls entities from the connected registry
|
|
10
|
+
* into this workspace, writing their source files in standard authoring form and synthesizing local
|
|
11
|
+
* declarations. Pass an `id` to pull that one entity, or omit it to pull every published entity of
|
|
12
|
+
* that kind.
|
|
13
13
|
*
|
|
14
14
|
* `handoff-app checkout tokens [setId]` — pull every published token set (or only the named set) into
|
|
15
15
|
* this workspace: reconstruct `tokens.json` and restore the generated token files.
|