handoff-app 1.0.3-canary.6 → 1.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/Changelog.md CHANGED
@@ -6,7 +6,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6
6
  and this project adheres to
7
7
  [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
8
8
 
9
- ## \[1.0.3] - 2026-02-08
9
+ ## \[1.0.3] - 2026-02-17
10
10
 
11
11
  Bugfixes, UX improvements to the component page, improvements to react docgen,
12
12
  a new quickstart command, and security fixes.
@@ -26,6 +26,12 @@ a new quickstart command, and security fixes.
26
26
  * The API response documentation is now a tree view, rather a formated JSON
27
27
  object. This makes it much easier to see and reason about the JSON response.
28
28
 
29
+ ## Bug Fixes
30
+
31
+ * The API preview would through a 500 error under some conditions
32
+ * Using TSX components would cause an error in the code preview if the tsx component
33
+ used some kinds of valid react formatting.
34
+
29
35
  ## Type Gen
30
36
 
31
37
  * When using TSX instead of handlebars, the properties can be automatically
@@ -46,10 +52,10 @@ a new quickstart command, and security fixes.
46
52
  Since Handoff does not use server side rendered components, handoff sites are not
47
53
  vulnerable to this risks.
48
54
 
49
- ## Feature
55
+ ## QoL Improvements
50
56
 
51
- * Components now set a last mod time at build time so you can tell when a component
52
- has changed.
57
+ * Fixes a bug in webpack that made the include path lookups fail. @handoff and @/util now
58
+ works properly.
53
59
 
54
60
  ## \[1.0.2] - 2026-01-12
55
61
 
@@ -61,6 +67,10 @@ Bugfixes focused on developer quality of life.
61
67
  resulting in a 404 error. This patch resolves a race condition in the HMR, by
62
68
  preventing double write of the next config.
63
69
  * Patch an issue where the example component doesn't load in a clean project
70
+ * If you create pages without a declared weight, handoff would throw an error
71
+ * There was an old structure in the way we used handlebars that surfaces a warning
72
+ on build. We've updated that method.
73
+ * We removed an old page template that was no longer needed.
64
74
 
65
75
  ## \[1.0.1] - 2025-12-28
66
76
 
@@ -0,0 +1,6 @@
1
+ import { CommandModule } from 'yargs';
2
+ export interface PlatformInitArgs {
3
+ url?: string;
4
+ }
5
+ declare const command: CommandModule<{}, PlatformInitArgs>;
6
+ export default command;
@@ -0,0 +1,231 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || function (mod) {
19
+ if (mod && mod.__esModule) return mod;
20
+ var result = {};
21
+ if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
22
+ __setModuleDefault(result, mod);
23
+ return result;
24
+ };
25
+ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
26
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
27
+ return new (P || (P = Promise))(function (resolve, reject) {
28
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
29
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
30
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
31
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
32
+ });
33
+ };
34
+ var __importDefault = (this && this.__importDefault) || function (mod) {
35
+ return (mod && mod.__esModule) ? mod : { "default": mod };
36
+ };
37
+ Object.defineProperty(exports, "__esModule", { value: true });
38
+ const p = __importStar(require("@clack/prompts"));
39
+ const chalk_1 = __importDefault(require("chalk"));
40
+ const fs_extra_1 = __importDefault(require("fs-extra"));
41
+ const path_1 = __importDefault(require("path"));
42
+ const client_1 = require("../../platform/client");
43
+ const credentials_1 = require("../../platform/credentials");
44
+ const DEFAULT_PLATFORM_URL = 'http://localhost:3000';
45
+ const CONFIG_FILES = ['handoff.config.json', 'handoff.config.js', 'handoff.config.cjs'];
46
+ const command = {
47
+ command: 'platform:init',
48
+ describe: 'Link this directory to a Handoff platform project',
49
+ builder: (yargs) => {
50
+ return yargs.option('url', {
51
+ type: 'string',
52
+ description: 'Platform URL',
53
+ default: process.env.HANDOFF_PLATFORM_URL || DEFAULT_PLATFORM_URL,
54
+ });
55
+ },
56
+ handler: (args) => __awaiter(void 0, void 0, void 0, function* () {
57
+ const baseUrl = (args.url || DEFAULT_PLATFORM_URL).replace(/\/+$/, '');
58
+ const workingDir = process.cwd();
59
+ p.intro(chalk_1.default.bold('Handoff Platform Init'));
60
+ // 1. Check login
61
+ const token = (0, credentials_1.getToken)(baseUrl);
62
+ if (!token) {
63
+ p.cancel(`Not logged in to ${baseUrl}. Run ${chalk_1.default.cyan('handoff-app platform:login')} first.`);
64
+ process.exit(1);
65
+ }
66
+ const spinner = p.spinner();
67
+ try {
68
+ // 2. Fetch project list
69
+ spinner.start('Fetching projects...');
70
+ const projects = yield (0, client_1.listProjects)(baseUrl, token);
71
+ spinner.stop('Projects loaded.');
72
+ if (projects.length === 0) {
73
+ p.cancel('No projects found. Create a project in the Handoff platform first.');
74
+ process.exit(1);
75
+ }
76
+ // 3. Interactive project selection
77
+ const projectId = yield p.select({
78
+ message: 'Select a project to link:',
79
+ options: projects.map((proj) => ({
80
+ value: proj.id,
81
+ label: `${proj.name} (${proj.orgName})`,
82
+ hint: `${proj.slug} — ${proj.role}`,
83
+ })),
84
+ });
85
+ if (p.isCancel(projectId)) {
86
+ p.cancel('Init cancelled.');
87
+ process.exit(0);
88
+ }
89
+ const selectedProject = projects.find((proj) => proj.id === projectId);
90
+ // 4. Check if a local config exists
91
+ const existingConfigFile = CONFIG_FILES.find((file) => fs_extra_1.default.existsSync(path_1.default.resolve(workingDir, file)));
92
+ if (!existingConfigFile) {
93
+ // No local config — pull it from the remote project
94
+ spinner.start('No local config found. Fetching config from platform...');
95
+ try {
96
+ const manifest = yield (0, client_1.getManifest)(baseUrl, token, selectedProject.id);
97
+ // Look for handoff.config.js in the manifest
98
+ const remoteConfigKey = Object.keys(manifest.files).find((key) => key === 'handoff.config.js' || key === 'handoff.config.json' || key === 'handoff.config.cjs');
99
+ if (remoteConfigKey) {
100
+ const content = yield (0, client_1.downloadFile)(baseUrl, token, selectedProject.id, remoteConfigKey);
101
+ const localConfigPath = path_1.default.resolve(workingDir, remoteConfigKey);
102
+ fs_extra_1.default.writeFileSync(localConfigPath, content);
103
+ spinner.stop(`Downloaded ${remoteConfigKey} from platform.`);
104
+ // Now inject the platform fields into it
105
+ yield injectPlatformFields(localConfigPath, selectedProject.id, baseUrl);
106
+ }
107
+ else {
108
+ spinner.stop('No remote config found either. Creating a new config.');
109
+ // Create a minimal config with just the platform fields
110
+ const configPath = path_1.default.resolve(workingDir, 'handoff.config.js');
111
+ const configContent = generateMinimalConfig(selectedProject.id, baseUrl);
112
+ fs_extra_1.default.writeFileSync(configPath, configContent);
113
+ }
114
+ }
115
+ catch (err) {
116
+ spinner.stop('Could not fetch remote config.');
117
+ // Fall back to creating a minimal config
118
+ const configPath = path_1.default.resolve(workingDir, 'handoff.config.js');
119
+ const configContent = generateMinimalConfig(selectedProject.id, baseUrl);
120
+ fs_extra_1.default.writeFileSync(configPath, configContent);
121
+ }
122
+ }
123
+ else {
124
+ // Local config exists — add/update platform fields
125
+ const configPath = path_1.default.resolve(workingDir, existingConfigFile);
126
+ yield injectPlatformFields(configPath, selectedProject.id, baseUrl);
127
+ }
128
+ p.note([
129
+ `Project: ${chalk_1.default.bold(selectedProject.name)}`,
130
+ `Organization: ${chalk_1.default.dim(selectedProject.orgName)}`,
131
+ `Project ID: ${chalk_1.default.dim(selectedProject.id)}`,
132
+ `Platform: ${chalk_1.default.dim(baseUrl)}`,
133
+ ].join('\n'), 'Linked successfully');
134
+ p.outro(`Run ${chalk_1.default.cyan('handoff-app platform:pull')} to sync files from the platform.`);
135
+ }
136
+ catch (err) {
137
+ spinner.stop('Init failed.');
138
+ p.cancel(err.message || 'Init failed');
139
+ process.exit(1);
140
+ }
141
+ }),
142
+ };
143
+ /**
144
+ * Inject platformProjectId and platformUrl into an existing config file.
145
+ *
146
+ * For JSON files, parse and re-serialize.
147
+ * For JS/CJS files, use a regex approach to add/update the fields.
148
+ */
149
+ function injectPlatformFields(configPath, projectId, platformUrl) {
150
+ return __awaiter(this, void 0, void 0, function* () {
151
+ const ext = path_1.default.extname(configPath);
152
+ if (ext === '.json') {
153
+ const config = fs_extra_1.default.readJsonSync(configPath);
154
+ config.platformProjectId = projectId;
155
+ config.platformUrl = platformUrl;
156
+ fs_extra_1.default.writeJsonSync(configPath, config, { spaces: 2 });
157
+ }
158
+ else {
159
+ // JS or CJS file — read as text, inject/replace fields
160
+ let content = fs_extra_1.default.readFileSync(configPath, 'utf8');
161
+ content = upsertConfigField(content, 'platformProjectId', JSON.stringify(projectId));
162
+ content = upsertConfigField(content, 'platformUrl', JSON.stringify(platformUrl));
163
+ fs_extra_1.default.writeFileSync(configPath, content, 'utf8');
164
+ }
165
+ });
166
+ }
167
+ /**
168
+ * Insert or replace a top-level field in a JS config export.
169
+ * Looks for `fieldName: ...` and replaces the value, or appends
170
+ * the field before the closing `};` of module.exports.
171
+ */
172
+ function upsertConfigField(source, fieldName, value) {
173
+ // Pattern to match an existing field assignment (with optional trailing comma)
174
+ const existingPattern = new RegExp(`(${fieldName}\\s*:\\s*)([^,}\\n]+)(,?)`);
175
+ if (existingPattern.test(source)) {
176
+ return source.replace(existingPattern, `$1${value}$3`);
177
+ }
178
+ // Insert before the last `};` (end of module.exports)
179
+ // Handle both `module.exports = {` and `export default {` styles
180
+ // Also ensure the previous line ends with a comma so the JS stays valid
181
+ const closingBrace = /(\n)(};?\s*$)/;
182
+ if (closingBrace.test(source)) {
183
+ // Ensure the last property line before the closing brace has a trailing comma
184
+ source = ensureTrailingComma(source);
185
+ return source.replace(closingBrace, `$1 ${fieldName}: ${value},\n$2`);
186
+ }
187
+ // Fallback: just append as a comment warning
188
+ return source + `\n// Added by platform:init — could not auto-inject:\n// ${fieldName}: ${value}\n`;
189
+ }
190
+ /**
191
+ * Ensure the last property line before the closing `};` has a trailing comma.
192
+ * This prevents invalid JS when appending new properties to an object literal.
193
+ */
194
+ function ensureTrailingComma(source) {
195
+ const lines = source.split('\n');
196
+ // Find the closing `};` or `}` at the start of a line (module.exports / export default)
197
+ let closingIdx = -1;
198
+ for (let i = lines.length - 1; i >= 0; i--) {
199
+ if (/^};?\s*$/.test(lines[i])) {
200
+ closingIdx = i;
201
+ break;
202
+ }
203
+ }
204
+ if (closingIdx <= 0)
205
+ return source;
206
+ // Walk backwards past blank lines and comment-only lines to find the last property line
207
+ let lastPropIdx = closingIdx - 1;
208
+ while (lastPropIdx >= 0 && (/^\s*$/.test(lines[lastPropIdx]) || /^\s*\/\//.test(lines[lastPropIdx]))) {
209
+ lastPropIdx--;
210
+ }
211
+ if (lastPropIdx < 0)
212
+ return source;
213
+ // Strip inline comments for the check, but preserve them in the output
214
+ const stripped = lines[lastPropIdx].replace(/\s*\/\/.*$/, '').trimEnd();
215
+ if (stripped.length > 0 && !stripped.endsWith(',') && !stripped.endsWith('{')) {
216
+ // Insert a comma after the last significant character, preserving any inline comment
217
+ lines[lastPropIdx] = lines[lastPropIdx].replace(/(\S)(\s*(?:\/\/.*)?)$/, '$1,$2');
218
+ }
219
+ return lines.join('\n');
220
+ }
221
+ /**
222
+ * Generate a minimal handoff.config.js with platform fields.
223
+ */
224
+ function generateMinimalConfig(projectId, platformUrl) {
225
+ return `module.exports = {
226
+ platformProjectId: ${JSON.stringify(projectId)},
227
+ platformUrl: ${JSON.stringify(platformUrl)},
228
+ };
229
+ `;
230
+ }
231
+ exports.default = command;
@@ -0,0 +1,6 @@
1
+ import { CommandModule } from 'yargs';
2
+ export interface PlatformLoginArgs {
3
+ url?: string;
4
+ }
5
+ declare const command: CommandModule<{}, PlatformLoginArgs>;
6
+ export default command;
@@ -0,0 +1,108 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || function (mod) {
19
+ if (mod && mod.__esModule) return mod;
20
+ var result = {};
21
+ if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
22
+ __setModuleDefault(result, mod);
23
+ return result;
24
+ };
25
+ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
26
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
27
+ return new (P || (P = Promise))(function (resolve, reject) {
28
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
29
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
30
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
31
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
32
+ });
33
+ };
34
+ var __importDefault = (this && this.__importDefault) || function (mod) {
35
+ return (mod && mod.__esModule) ? mod : { "default": mod };
36
+ };
37
+ Object.defineProperty(exports, "__esModule", { value: true });
38
+ const p = __importStar(require("@clack/prompts"));
39
+ const chalk_1 = __importDefault(require("chalk"));
40
+ const child_process_1 = require("child_process");
41
+ const client_1 = require("../../platform/client");
42
+ const credentials_1 = require("../../platform/credentials");
43
+ const DEFAULT_PLATFORM_URL = 'http://localhost:3000';
44
+ const command = {
45
+ command: 'platform:login',
46
+ describe: 'Log in to the Handoff platform',
47
+ builder: (yargs) => {
48
+ return yargs.option('url', {
49
+ type: 'string',
50
+ description: 'Platform URL',
51
+ default: process.env.HANDOFF_PLATFORM_URL || DEFAULT_PLATFORM_URL,
52
+ });
53
+ },
54
+ handler: (args) => __awaiter(void 0, void 0, void 0, function* () {
55
+ const baseUrl = (args.url || DEFAULT_PLATFORM_URL).replace(/\/+$/, '');
56
+ p.intro(chalk_1.default.bold('Handoff Platform Login'));
57
+ const spinner = p.spinner();
58
+ try {
59
+ spinner.start('Initiating authentication...');
60
+ const deviceCode = yield (0, client_1.createDeviceCode)(baseUrl);
61
+ spinner.stop('Authentication initiated.');
62
+ // Display the user code prominently
63
+ p.note([
64
+ `Your one-time code: ${chalk_1.default.bold.cyan(deviceCode.userCode)}`,
65
+ '',
66
+ `Open this URL to sign in:`,
67
+ chalk_1.default.underline(`${deviceCode.verificationUrl}`),
68
+ ].join('\n'), 'Authentication');
69
+ // Try to open the browser automatically
70
+ openBrowser(deviceCode.verificationUrl);
71
+ spinner.start('Waiting for approval (this will open in your browser)...');
72
+ const result = yield (0, client_1.pollForToken)(baseUrl, deviceCode.deviceCode, deviceCode.interval, deviceCode.expiresIn);
73
+ spinner.stop('Authentication approved!');
74
+ // Store the token
75
+ (0, credentials_1.setToken)(baseUrl, result.token, {
76
+ name: result.user.name,
77
+ email: result.user.email,
78
+ });
79
+ p.note([
80
+ `Logged in as ${chalk_1.default.bold(result.user.name || result.user.email)}`,
81
+ `Platform: ${chalk_1.default.dim(baseUrl)}`,
82
+ ].join('\n'), 'Success');
83
+ p.outro('You can now use platform:init to link a project.');
84
+ }
85
+ catch (err) {
86
+ spinner.stop('Authentication failed.');
87
+ p.cancel(err.message || 'Login failed');
88
+ process.exit(1);
89
+ }
90
+ }),
91
+ };
92
+ /**
93
+ * Attempt to open a URL in the default browser.
94
+ * Fails silently — the user can always copy/paste the URL.
95
+ */
96
+ function openBrowser(url) {
97
+ const command = process.platform === 'darwin'
98
+ ? `open "${url}"`
99
+ : process.platform === 'win32'
100
+ ? `start "" "${url}"`
101
+ : `xdg-open "${url}"`;
102
+ (0, child_process_1.exec)(command, (err) => {
103
+ if (err) {
104
+ // Silently ignore — user will use the printed URL
105
+ }
106
+ });
107
+ }
108
+ exports.default = command;
@@ -0,0 +1,6 @@
1
+ import { CommandModule } from 'yargs';
2
+ export interface PlatformLogoutArgs {
3
+ url?: string;
4
+ }
5
+ declare const command: CommandModule<{}, PlatformLogoutArgs>;
6
+ export default command;
@@ -0,0 +1,88 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || function (mod) {
19
+ if (mod && mod.__esModule) return mod;
20
+ var result = {};
21
+ if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
22
+ __setModuleDefault(result, mod);
23
+ return result;
24
+ };
25
+ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
26
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
27
+ return new (P || (P = Promise))(function (resolve, reject) {
28
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
29
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
30
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
31
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
32
+ });
33
+ };
34
+ var __importDefault = (this && this.__importDefault) || function (mod) {
35
+ return (mod && mod.__esModule) ? mod : { "default": mod };
36
+ };
37
+ Object.defineProperty(exports, "__esModule", { value: true });
38
+ const p = __importStar(require("@clack/prompts"));
39
+ const chalk_1 = __importDefault(require("chalk"));
40
+ const client_1 = require("../../platform/client");
41
+ const credentials_1 = require("../../platform/credentials");
42
+ const DEFAULT_PLATFORM_URL = 'http://localhost:3000';
43
+ const command = {
44
+ command: 'platform:logout',
45
+ describe: 'Log out from the Handoff platform',
46
+ builder: (yargs) => {
47
+ return yargs.option('url', {
48
+ type: 'string',
49
+ description: 'Platform URL',
50
+ default: process.env.HANDOFF_PLATFORM_URL || DEFAULT_PLATFORM_URL,
51
+ });
52
+ },
53
+ handler: (args) => __awaiter(void 0, void 0, void 0, function* () {
54
+ const baseUrl = (args.url || DEFAULT_PLATFORM_URL).replace(/\/+$/, '');
55
+ p.intro(chalk_1.default.bold('Handoff Platform Logout'));
56
+ const token = (0, credentials_1.getToken)(baseUrl);
57
+ if (!token) {
58
+ p.note(`Not currently logged in to ${chalk_1.default.dim(baseUrl)}.`, 'Info');
59
+ p.outro('Nothing to do.');
60
+ return;
61
+ }
62
+ const spinner = p.spinner();
63
+ try {
64
+ // Revoke the token on the server (best-effort)
65
+ spinner.start('Revoking token...');
66
+ try {
67
+ yield (0, client_1.revokeToken)(baseUrl, token);
68
+ }
69
+ catch (_a) {
70
+ // If the server is unreachable or the token is already invalid,
71
+ // still proceed to remove the local credential.
72
+ }
73
+ spinner.stop('Token revoked.');
74
+ // Remove from local credentials store
75
+ (0, credentials_1.removeToken)(baseUrl);
76
+ p.note(`Logged out from ${chalk_1.default.dim(baseUrl)}.`, 'Success');
77
+ p.outro('Local credentials removed.');
78
+ }
79
+ catch (err) {
80
+ spinner.stop('Logout encountered an issue.');
81
+ // Still remove the local token even if revocation fails
82
+ (0, credentials_1.removeToken)(baseUrl);
83
+ p.note(`Local credentials removed, but server revocation may have failed.`, 'Warning');
84
+ p.outro('You are logged out locally.');
85
+ }
86
+ }),
87
+ };
88
+ exports.default = command;
@@ -0,0 +1,8 @@
1
+ import { CommandModule } from 'yargs';
2
+ export interface PlatformPullArgs {
3
+ url?: string;
4
+ force?: boolean;
5
+ debug?: boolean;
6
+ }
7
+ declare const command: CommandModule<{}, PlatformPullArgs>;
8
+ export default command;
@@ -0,0 +1,112 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || function (mod) {
19
+ if (mod && mod.__esModule) return mod;
20
+ var result = {};
21
+ if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
22
+ __setModuleDefault(result, mod);
23
+ return result;
24
+ };
25
+ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
26
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
27
+ return new (P || (P = Promise))(function (resolve, reject) {
28
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
29
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
30
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
31
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
32
+ });
33
+ };
34
+ var __importDefault = (this && this.__importDefault) || function (mod) {
35
+ return (mod && mod.__esModule) ? mod : { "default": mod };
36
+ };
37
+ Object.defineProperty(exports, "__esModule", { value: true });
38
+ const p = __importStar(require("@clack/prompts"));
39
+ const chalk_1 = __importDefault(require("chalk"));
40
+ const __1 = __importDefault(require("../../"));
41
+ const credentials_1 = require("../../platform/credentials");
42
+ const sync_1 = require("../../platform/sync");
43
+ const DEFAULT_PLATFORM_URL = 'http://localhost:3000';
44
+ const command = {
45
+ command: 'platform:pull',
46
+ describe: 'Pull project files from the Handoff platform',
47
+ builder: (yargs) => {
48
+ return yargs
49
+ .option('url', {
50
+ type: 'string',
51
+ description: 'Platform URL (overrides config)',
52
+ })
53
+ .option('force', {
54
+ alias: 'f',
55
+ type: 'boolean',
56
+ description: 'Overwrite all local files and delete orphaned files',
57
+ default: false,
58
+ })
59
+ .option('debug', {
60
+ alias: 'd',
61
+ type: 'boolean',
62
+ description: 'Enable debug mode',
63
+ });
64
+ },
65
+ handler: (args) => __awaiter(void 0, void 0, void 0, function* () {
66
+ const handoff = new __1.default(args.debug, args.force);
67
+ const config = handoff.config;
68
+ const baseUrl = (args.url || (config === null || config === void 0 ? void 0 : config.platformUrl) || process.env.HANDOFF_PLATFORM_URL || DEFAULT_PLATFORM_URL).replace(/\/+$/, '');
69
+ const projectId = config === null || config === void 0 ? void 0 : config.platformProjectId;
70
+ p.intro(chalk_1.default.bold('Handoff Platform Pull'));
71
+ if (!projectId) {
72
+ p.cancel(`No project linked. Run ${chalk_1.default.cyan('handoff-app platform:init')} first.`);
73
+ process.exit(1);
74
+ }
75
+ const token = (0, credentials_1.getToken)(baseUrl);
76
+ if (!token) {
77
+ p.cancel(`Not logged in to ${baseUrl}. Run ${chalk_1.default.cyan('handoff-app platform:login')} first.`);
78
+ process.exit(1);
79
+ }
80
+ const spinner = p.spinner();
81
+ try {
82
+ spinner.start('Pulling from platform...');
83
+ const result = yield (0, sync_1.pullProject)(baseUrl, token, projectId, handoff.workingPath, {
84
+ force: args.force,
85
+ onProgress: (msg) => spinner.message(msg),
86
+ });
87
+ spinner.stop('Pull complete.');
88
+ const summary = [
89
+ `Downloaded: ${chalk_1.default.green(String(result.downloaded.length))} file(s)`,
90
+ `Unchanged: ${chalk_1.default.dim(String(result.unchanged.length))} file(s)`,
91
+ ];
92
+ if (result.deleted.length > 0) {
93
+ summary.push(`Deleted: ${chalk_1.default.red(String(result.deleted.length))} file(s)`);
94
+ }
95
+ summary.push(`Sync version: ${chalk_1.default.cyan(String(result.version))}`);
96
+ p.note(summary.join('\n'), 'Pull Summary');
97
+ if (result.downloaded.length > 0) {
98
+ p.log.info(chalk_1.default.dim('Downloaded files:'));
99
+ for (const file of result.downloaded) {
100
+ p.log.info(chalk_1.default.dim(` + ${file}`));
101
+ }
102
+ }
103
+ p.outro('Local project is up to date.');
104
+ }
105
+ catch (err) {
106
+ spinner.stop('Pull failed.');
107
+ p.cancel(err.message || 'Pull failed');
108
+ process.exit(1);
109
+ }
110
+ }),
111
+ };
112
+ exports.default = command;
@@ -0,0 +1,8 @@
1
+ import { CommandModule } from 'yargs';
2
+ export interface PlatformPushArgs {
3
+ url?: string;
4
+ force?: boolean;
5
+ debug?: boolean;
6
+ }
7
+ declare const command: CommandModule<{}, PlatformPushArgs>;
8
+ export default command;