@hs-x/cli 0.1.3 → 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +97 -986
- package/dist/account-store.d.ts +45 -22
- package/dist/account-store.d.ts.map +1 -1
- package/dist/account-store.js +73 -16
- package/dist/account-store.js.map +1 -1
- package/dist/cli/index.d.ts.map +1 -1
- package/dist/cli/index.js +177 -16
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/kit.d.ts +1 -1
- package/dist/cli/kit.d.ts.map +1 -1
- package/dist/cli/kit.js.map +1 -1
- package/dist/commands/account.d.ts +6 -6
- package/dist/commands/account.d.ts.map +1 -1
- package/dist/commands/account.js +25 -23
- package/dist/commands/account.js.map +1 -1
- package/dist/commands/api.d.ts +1 -1
- package/dist/commands/completion.d.ts.map +1 -1
- package/dist/commands/completion.js +6 -4
- package/dist/commands/completion.js.map +1 -1
- package/dist/commands/connect.d.ts +16 -0
- package/dist/commands/connect.d.ts.map +1 -1
- package/dist/commands/connect.js +143 -17
- package/dist/commands/connect.js.map +1 -1
- package/dist/commands/control-plane-read.d.ts +3 -3
- package/dist/commands/control-plane-read.d.ts.map +1 -1
- package/dist/commands/control-plane-read.js +21 -33
- package/dist/commands/control-plane-read.js.map +1 -1
- package/dist/commands/deploy-promote.d.ts +1 -1
- package/dist/commands/deploy-promote.d.ts.map +1 -1
- package/dist/commands/deploy-promote.js +3 -3
- package/dist/commands/deploy-promote.js.map +1 -1
- package/dist/commands/deploy.d.ts.map +1 -1
- package/dist/commands/deploy.js +114 -7
- package/dist/commands/deploy.js.map +1 -1
- package/dist/commands/dev.d.ts +5 -0
- package/dist/commands/dev.d.ts.map +1 -1
- package/dist/commands/dev.js +233 -2
- package/dist/commands/dev.js.map +1 -1
- package/dist/commands/doctor.d.ts +1 -1
- package/dist/commands/doctor.js +2 -2
- package/dist/commands/doctor.js.map +1 -1
- package/dist/commands/flags.d.ts +1 -1
- package/dist/commands/flags.d.ts.map +1 -1
- package/dist/commands/flags.js +59 -35
- package/dist/commands/flags.js.map +1 -1
- package/dist/commands/help-command.d.ts +15 -1
- package/dist/commands/help-command.d.ts.map +1 -1
- package/dist/commands/help-command.js +316 -31
- package/dist/commands/help-command.js.map +1 -1
- package/dist/commands/init.d.ts +11 -8
- package/dist/commands/init.d.ts.map +1 -1
- package/dist/commands/init.js +435 -246
- package/dist/commands/init.js.map +1 -1
- package/dist/commands/link.d.ts +1 -26
- package/dist/commands/link.d.ts.map +1 -1
- package/dist/commands/link.js +11 -236
- package/dist/commands/link.js.map +1 -1
- package/dist/commands/login.d.ts +4 -12
- package/dist/commands/login.d.ts.map +1 -1
- package/dist/commands/login.js +196 -390
- package/dist/commands/login.js.map +1 -1
- package/dist/commands/migrate.d.ts +12 -7
- package/dist/commands/migrate.d.ts.map +1 -1
- package/dist/commands/migrate.js +311 -205
- package/dist/commands/migrate.js.map +1 -1
- package/dist/commands/rollback.d.ts +1 -20
- package/dist/commands/rollback.d.ts.map +1 -1
- package/dist/commands/rollback.js +6 -275
- package/dist/commands/rollback.js.map +1 -1
- package/dist/commands/status.d.ts +1 -1
- package/dist/commands/status.js +1 -1
- package/dist/commands/status.js.map +1 -1
- package/dist/commands/unlink.d.ts +1 -21
- package/dist/commands/unlink.d.ts.map +1 -1
- package/dist/commands/unlink.js +12 -94
- package/dist/commands/unlink.js.map +1 -1
- package/dist/commands/update.js +7 -7
- package/dist/commands/update.js.map +1 -1
- package/dist/commands/validate.d.ts +2 -2
- package/dist/commands/validate.d.ts.map +1 -1
- package/dist/commands/validate.js +1 -1
- package/dist/commands/validate.js.map +1 -1
- package/dist/config.d.ts +1 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +7 -0
- package/dist/config.js.map +1 -1
- package/dist/constants.d.ts +1 -1
- package/dist/constants.d.ts.map +1 -1
- package/dist/constants.js +5 -4
- package/dist/constants.js.map +1 -1
- package/dist/control-plane-fetch.d.ts.map +1 -1
- package/dist/control-plane-fetch.js +2 -2
- package/dist/control-plane-fetch.js.map +1 -1
- package/dist/control-plane-loader.d.ts +1 -1
- package/dist/control-plane-loader.js +1 -1
- package/dist/dev/invoke.d.ts +44 -0
- package/dist/dev/invoke.d.ts.map +1 -0
- package/dist/dev/invoke.js +155 -0
- package/dist/dev/invoke.js.map +1 -0
- package/dist/errors-registry.d.ts.map +1 -1
- package/dist/errors-registry.js +86 -1
- package/dist/errors-registry.js.map +1 -1
- package/dist/help.d.ts +10 -0
- package/dist/help.d.ts.map +1 -1
- package/dist/help.js +212 -5
- package/dist/help.js.map +1 -1
- package/dist/init/templates.d.ts +11 -3
- package/dist/init/templates.d.ts.map +1 -1
- package/dist/init/templates.js +115 -27
- package/dist/init/templates.js.map +1 -1
- package/dist/project-binding.d.ts +17 -0
- package/dist/project-binding.d.ts.map +1 -0
- package/dist/project-binding.js +30 -0
- package/dist/project-binding.js.map +1 -0
- package/dist/prompt.d.ts +21 -0
- package/dist/prompt.d.ts.map +1 -1
- package/dist/prompt.js +186 -50
- package/dist/prompt.js.map +1 -1
- package/dist/reporter/human.d.ts +5 -0
- package/dist/reporter/human.d.ts.map +1 -1
- package/dist/reporter/human.js +48 -18
- package/dist/reporter/human.js.map +1 -1
- package/dist/reporter/json.d.ts +6 -0
- package/dist/reporter/json.d.ts.map +1 -1
- package/dist/reporter/json.js +16 -0
- package/dist/reporter/json.js.map +1 -1
- package/dist/reporter/style.d.ts +2 -1
- package/dist/reporter/style.d.ts.map +1 -1
- package/dist/reporter/style.js +14 -3
- package/dist/reporter/style.js.map +1 -1
- package/dist/reporter/types.d.ts +9 -0
- package/dist/reporter/types.d.ts.map +1 -1
- package/dist/reporter/types.js.map +1 -1
- package/dist/services/account-store.d.ts +9 -4
- package/dist/services/account-store.d.ts.map +1 -1
- package/dist/services/account-store.js +46 -22
- package/dist/services/account-store.js.map +1 -1
- package/dist/services/control-plane.js +3 -2
- package/dist/services/control-plane.js.map +1 -1
- package/dist/services/live.d.ts +1 -1
- package/dist/services/reporter.d.ts +3 -0
- package/dist/services/reporter.d.ts.map +1 -1
- package/dist/services/reporter.js +2 -0
- package/dist/services/reporter.js.map +1 -1
- package/package.json +21 -8
package/dist/commands/migrate.js
CHANGED
|
@@ -1,174 +1,15 @@
|
|
|
1
|
-
import { mkdir, readFile, stat, writeFile } from 'node:fs/promises';
|
|
2
|
-
import { basename, dirname, join, resolve } from 'node:path';
|
|
3
1
|
import * as Args from '@effect/cli/Args';
|
|
4
2
|
import * as Command from '@effect/cli/Command';
|
|
5
3
|
import * as Options from '@effect/cli/Options';
|
|
6
4
|
import { FileSystem, Path } from '@effect/platform';
|
|
7
|
-
import { generateLegacyCardProject } from '@hs-x/codegen';
|
|
8
|
-
import { classifyMigrationInput, validateProject } from '@hs-x/validator';
|
|
5
|
+
import { generateLegacyCardProject, generateMigratedHsxProject, } from '@hs-x/codegen';
|
|
6
|
+
import { classifyMigrationInput, detectMigrationSource, planCutover, validateProject } from '@hs-x/validator';
|
|
9
7
|
import { Effect, Option } from 'effect';
|
|
10
8
|
import { cliError, exitWith } from '../cli-error.js';
|
|
11
9
|
import { accountIdOption, controlPlaneUrlOption, cwdOption, jsonOption, runHandler, userIdOption, } from '../cli/kit.js';
|
|
12
|
-
import { createDeployHubSpotDeveloperClient } from '../hubspot-developer-client.js';
|
|
13
|
-
import { emitHelp } from '../help.js';
|
|
14
|
-
import { createReporter } from '../reporter/index.js';
|
|
15
10
|
import { Cwd } from '../services/cwd.js';
|
|
16
11
|
import { HubSpotDevClient } from '../services/hubspot-dev-client.js';
|
|
17
12
|
import { Reporter } from '../services/reporter.js';
|
|
18
|
-
export async function migrateCommand({ argv, cwd, json, }) {
|
|
19
|
-
const subcommand = argv[1] ?? 'help';
|
|
20
|
-
if (subcommand !== 'inspect' &&
|
|
21
|
-
subcommand !== 'cards' &&
|
|
22
|
-
subcommand !== 'report' &&
|
|
23
|
-
subcommand !== 'swap-cards') {
|
|
24
|
-
emitHelp(migrateHelpText(), json);
|
|
25
|
-
return { exitCode: subcommand === 'help' ? 0 : 1 };
|
|
26
|
-
}
|
|
27
|
-
if (subcommand === 'swap-cards') {
|
|
28
|
-
return await migrateSwapCardsCommand({ argv, json });
|
|
29
|
-
}
|
|
30
|
-
const target = argv.find((arg, index) => index > 1 && !arg.startsWith('-'));
|
|
31
|
-
if (!target) {
|
|
32
|
-
throw new Error(`Usage: hs-x migrate ${subcommand} <path> [--json]`);
|
|
33
|
-
}
|
|
34
|
-
const input = await readMigrationInput(resolve(cwd, target));
|
|
35
|
-
const result = classifyMigrationInput(input);
|
|
36
|
-
if (subcommand === 'report') {
|
|
37
|
-
const response = {
|
|
38
|
-
ok: true,
|
|
39
|
-
command: 'migrate report',
|
|
40
|
-
summary: {
|
|
41
|
-
detected: result.detected,
|
|
42
|
-
automatable: result.automatable.length,
|
|
43
|
-
humanReview: result.manualWork.length,
|
|
44
|
-
cloudflareRequired: result.cloudflareRequired,
|
|
45
|
-
canGenerateCards: result.manualWork.length === 0,
|
|
46
|
-
},
|
|
47
|
-
inspect: result,
|
|
48
|
-
nextSteps: migrationNextSteps(result),
|
|
49
|
-
};
|
|
50
|
-
if (json) {
|
|
51
|
-
process.stdout.write(`${JSON.stringify(response, null, 2)}\n`);
|
|
52
|
-
}
|
|
53
|
-
else {
|
|
54
|
-
process.stdout.write(renderMigrationReport(response));
|
|
55
|
-
}
|
|
56
|
-
return { exitCode: 0 };
|
|
57
|
-
}
|
|
58
|
-
if (subcommand === 'inspect') {
|
|
59
|
-
const response = { ok: true, command: 'migrate inspect', ...result };
|
|
60
|
-
if (json) {
|
|
61
|
-
process.stdout.write(`${JSON.stringify(response, null, 2)}\n`);
|
|
62
|
-
}
|
|
63
|
-
else {
|
|
64
|
-
process.stdout.write(renderMigrationInspect(response));
|
|
65
|
-
}
|
|
66
|
-
return { exitCode: result.manualWork.length === 0 ? 0 : 1 };
|
|
67
|
-
}
|
|
68
|
-
if (result.manualWork.length > 0 || !isLegacyCardMigrationInput(input)) {
|
|
69
|
-
const response = { ok: false, command: 'migrate cards', ...result };
|
|
70
|
-
if (json) {
|
|
71
|
-
process.stdout.write(`${JSON.stringify(response, null, 2)}\n`);
|
|
72
|
-
}
|
|
73
|
-
else {
|
|
74
|
-
process.stdout.write(renderMigrationInspect(response));
|
|
75
|
-
}
|
|
76
|
-
return { exitCode: 1 };
|
|
77
|
-
}
|
|
78
|
-
const outputRoot = resolve(cwd, resolveFlag(argv, '--project') ?? 'migrated-app');
|
|
79
|
-
const generated = generateLegacyCardProject(input);
|
|
80
|
-
await writeProjectFiles(outputRoot, generated.files);
|
|
81
|
-
const validation = await validateProject({ root: outputRoot });
|
|
82
|
-
const response = {
|
|
83
|
-
ok: validation.ok,
|
|
84
|
-
command: 'migrate cards',
|
|
85
|
-
outputRoot,
|
|
86
|
-
createdFiles: Object.keys(generated.files).sort(),
|
|
87
|
-
inspect: result,
|
|
88
|
-
validation,
|
|
89
|
-
};
|
|
90
|
-
if (json) {
|
|
91
|
-
process.stdout.write(`${JSON.stringify(response, null, 2)}\n`);
|
|
92
|
-
}
|
|
93
|
-
else {
|
|
94
|
-
const reporter = createReporter({ command: 'migrate cards', argv });
|
|
95
|
-
reporter.header(basename(outputRoot));
|
|
96
|
-
reporter
|
|
97
|
-
.step('Generated migrated App Card project')
|
|
98
|
-
.ok(`${response.createdFiles.length} files ${outputRoot}`);
|
|
99
|
-
for (const file of response.createdFiles)
|
|
100
|
-
reporter.info(file);
|
|
101
|
-
if (validation.ok) {
|
|
102
|
-
reporter.step('Validation').ok();
|
|
103
|
-
}
|
|
104
|
-
else {
|
|
105
|
-
reporter.step('Validation').fail(`${validation.diagnostics.length} issue(s)`);
|
|
106
|
-
for (const d of validation.diagnostics) {
|
|
107
|
-
const code = d.code ?? 'diagnostic';
|
|
108
|
-
const msg = d.message ?? JSON.stringify(d);
|
|
109
|
-
reporter.error(code, msg);
|
|
110
|
-
}
|
|
111
|
-
}
|
|
112
|
-
reporter.done(validation.ok ? 'Migrated' : 'Failed', validation.ok ? 0 : 1);
|
|
113
|
-
}
|
|
114
|
-
return { exitCode: validation.ok ? 0 : 1 };
|
|
115
|
-
}
|
|
116
|
-
async function migrateSwapCardsCommand({ argv, json, }) {
|
|
117
|
-
const appId = readRequiredNumberFlag(argv, '--app-id');
|
|
118
|
-
const legacyCrmCardId = resolveFlag(argv, '--legacy-card-id');
|
|
119
|
-
const replacementAppCardId = resolveFlag(argv, '--replacement-card-id');
|
|
120
|
-
if (!legacyCrmCardId) {
|
|
121
|
-
throw new Error('--legacy-card-id is required.');
|
|
122
|
-
}
|
|
123
|
-
if (!replacementAppCardId) {
|
|
124
|
-
throw new Error('--replacement-card-id is required.');
|
|
125
|
-
}
|
|
126
|
-
const local = argv.includes('--local-hubspot');
|
|
127
|
-
const client = await createDeployHubSpotDeveloperClient({
|
|
128
|
-
argv,
|
|
129
|
-
local,
|
|
130
|
-
allowedOperations: ['hubspot-migration-read', 'hubspot-card-view-swap'],
|
|
131
|
-
});
|
|
132
|
-
const apps = await client.migrations.listApps();
|
|
133
|
-
const helpDeskAppCardId = resolveFlag(argv, '--helpdesk-card-id');
|
|
134
|
-
const swapRequest = helpDeskAppCardId === undefined
|
|
135
|
-
? { appId, legacyCrmCardId, replacementAppCardId }
|
|
136
|
-
: { appId, legacyCrmCardId, replacementAppCardId, helpDeskAppCardId };
|
|
137
|
-
const swap = await client.migrations.migrateCardViews(swapRequest);
|
|
138
|
-
const response = {
|
|
139
|
-
ok: true,
|
|
140
|
-
command: 'migrate swap-cards',
|
|
141
|
-
appId,
|
|
142
|
-
legacyCrmCardId,
|
|
143
|
-
replacementAppCardId,
|
|
144
|
-
...(helpDeskAppCardId === undefined ? {} : { helpDeskAppCardId }),
|
|
145
|
-
migrationApps: apps,
|
|
146
|
-
swap,
|
|
147
|
-
};
|
|
148
|
-
if (json) {
|
|
149
|
-
process.stdout.write(`${JSON.stringify(response, null, 2)}\n`);
|
|
150
|
-
}
|
|
151
|
-
else {
|
|
152
|
-
const reporter = createReporter({ command: 'migrate swap-cards', argv });
|
|
153
|
-
reporter.header(legacyCrmCardId);
|
|
154
|
-
reporter.step('Migrated legacy CRM card').ok(`-> App Card ${replacementAppCardId}`);
|
|
155
|
-
reporter.done('Swapped');
|
|
156
|
-
}
|
|
157
|
-
return { exitCode: 0 };
|
|
158
|
-
}
|
|
159
|
-
async function readMigrationInput(path) {
|
|
160
|
-
const stats = await stat(path);
|
|
161
|
-
const file = stats.isDirectory() ? join(path, 'migration.json') : path;
|
|
162
|
-
const contents = await readFile(file, 'utf8');
|
|
163
|
-
return JSON.parse(contents);
|
|
164
|
-
}
|
|
165
|
-
async function writeProjectFiles(root, files) {
|
|
166
|
-
for (const [file, contents] of Object.entries(files)) {
|
|
167
|
-
const path = join(root, file);
|
|
168
|
-
await mkdir(dirname(path), { recursive: true });
|
|
169
|
-
await writeFile(path, contents, { flag: 'wx' });
|
|
170
|
-
}
|
|
171
|
-
}
|
|
172
13
|
function isLegacyCardMigrationInput(value) {
|
|
173
14
|
if (!isRecord(value) || !isRecord(value.legacyApp) || !isRecord(value.legacyCard)) {
|
|
174
15
|
return false;
|
|
@@ -221,50 +62,140 @@ function migrationNextSteps(result) {
|
|
|
221
62
|
: 'Use `hs-x deploy --hubspot-only` once HubSpot upload is wired.',
|
|
222
63
|
];
|
|
223
64
|
}
|
|
224
|
-
function readRequiredNumberFlag(argv, flag) {
|
|
225
|
-
const value = resolveFlag(argv, flag);
|
|
226
|
-
if (!value) {
|
|
227
|
-
throw new Error(`${flag} is required.`);
|
|
228
|
-
}
|
|
229
|
-
return readPositiveInteger(value, flag);
|
|
230
|
-
}
|
|
231
|
-
function readPositiveInteger(value, label) {
|
|
232
|
-
const parsed = Number.parseInt(value, 10);
|
|
233
|
-
if (!Number.isInteger(parsed) || parsed <= 0) {
|
|
234
|
-
throw new Error(`${label} must be a positive integer.`);
|
|
235
|
-
}
|
|
236
|
-
return parsed;
|
|
237
|
-
}
|
|
238
|
-
function isFlagValue(value) {
|
|
239
|
-
return value !== undefined && value.length > 0 && !value.startsWith('-');
|
|
240
|
-
}
|
|
241
|
-
function resolveFlag(argv, flag) {
|
|
242
|
-
const index = argv.indexOf(flag);
|
|
243
|
-
if (index !== -1) {
|
|
244
|
-
const next = argv[index + 1];
|
|
245
|
-
if (isFlagValue(next))
|
|
246
|
-
return next;
|
|
247
|
-
}
|
|
248
|
-
const prefix = `${flag}=`;
|
|
249
|
-
const found = argv.find((arg) => arg.startsWith(prefix));
|
|
250
|
-
if (!found)
|
|
251
|
-
return undefined;
|
|
252
|
-
const value = found.slice(prefix.length);
|
|
253
|
-
return value.length > 0 ? value : undefined;
|
|
254
|
-
}
|
|
255
65
|
function isRecord(value) {
|
|
256
66
|
return typeof value === 'object' && value !== null;
|
|
257
67
|
}
|
|
258
68
|
export function migrateHelpText() {
|
|
259
69
|
return `hs-x migrate
|
|
260
70
|
|
|
71
|
+
Migrate a legacy HubSpot app (2023.2 / 2025.1 / 2025.2) to a 2026.03 HS-X
|
|
72
|
+
project. The generated project is a NET-NEW dupe for testing in \`hs-x dev\`;
|
|
73
|
+
your original app keeps running untouched until cutover.
|
|
74
|
+
|
|
261
75
|
Commands:
|
|
262
|
-
hs-x migrate
|
|
263
|
-
hs-x migrate
|
|
76
|
+
hs-x migrate run [path] [--out <dir>] [--json] Detect, classify, and generate the dupe (the one command)
|
|
77
|
+
hs-x migrate cutover [path] [--json] Dry-run plan: what cutover changes on the ORIGINAL app
|
|
78
|
+
hs-x migrate inspect <path> [--json] Classify a legacy migration input
|
|
79
|
+
hs-x migrate report <path> [--json] Summarize migration readiness
|
|
264
80
|
hs-x migrate cards <path> [--project <dir>] [--json]
|
|
265
81
|
hs-x migrate swap-cards --app-id <id> --legacy-card-id <id> --replacement-card-id <id> [--helpdesk-card-id <id>] [--local-hubspot] [--json]
|
|
82
|
+
|
|
83
|
+
Run hs-x migrate <subcommand> --help for details on each step.
|
|
266
84
|
`;
|
|
267
85
|
}
|
|
86
|
+
export function migrateSubcommandHelpText(sub) {
|
|
87
|
+
switch (sub) {
|
|
88
|
+
case 'run':
|
|
89
|
+
return `hs-x migrate run [path]
|
|
90
|
+
|
|
91
|
+
The one-command migration: detect the legacy source (a project tree with
|
|
92
|
+
hsproject.json, or a migration.json input), classify every component, and
|
|
93
|
+
generate a NET-NEW 2026.03 HS-X project (the dupe) from everything portable —
|
|
94
|
+
app cards, serverless functions, webhook subscriptions, workflow actions.
|
|
95
|
+
|
|
96
|
+
Pieces needing a human decision (external receivers, endpoint URL contracts)
|
|
97
|
+
are listed with doc links; they gate CUTOVER, not the dupe — test it in
|
|
98
|
+
\`hs-x dev\` first. Re-run safely after resolving findings.
|
|
99
|
+
|
|
100
|
+
Arguments:
|
|
101
|
+
path Legacy source (default: current directory)
|
|
102
|
+
|
|
103
|
+
Flags:
|
|
104
|
+
--out <dir> Output directory (default: migrated-app)
|
|
105
|
+
--cwd <dir> Resolve paths relative to <dir>
|
|
106
|
+
--json Emit JSON result
|
|
107
|
+
|
|
108
|
+
Exits 1 only when nothing portable could be generated or validation fails.
|
|
109
|
+
`;
|
|
110
|
+
case 'cutover':
|
|
111
|
+
return `hs-x migrate cutover [path]
|
|
112
|
+
|
|
113
|
+
Print the DRY-RUN cutover plan: the ordered steps that migrate the ORIGINAL
|
|
114
|
+
app in place to 2026.03 — keeping its app id, installs, OAuth grants, and
|
|
115
|
+
listing — once the dupe has proven the migration in \`hs-x dev\`.
|
|
116
|
+
|
|
117
|
+
The plan is honest about the one-way door (no downgrade from 2026.03) and
|
|
118
|
+
lists every open decision (external receivers, endpoint contracts,
|
|
119
|
+
marketplace listing) that gates execution. Execution itself never runs
|
|
120
|
+
without an explicit human go; --execute is refused in this build.
|
|
121
|
+
|
|
122
|
+
Arguments:
|
|
123
|
+
path The ORIGINAL legacy source (default: current directory)
|
|
124
|
+
|
|
125
|
+
Flags:
|
|
126
|
+
--json Emit the plan as JSON
|
|
127
|
+
--cwd <dir> Resolve paths relative to <dir>
|
|
128
|
+
`;
|
|
129
|
+
case 'inspect':
|
|
130
|
+
return `hs-x migrate inspect <path>
|
|
131
|
+
|
|
132
|
+
Classify a legacy migration input: which findings HS-X can migrate
|
|
133
|
+
automatically, which need human review, and whether the replacement app needs
|
|
134
|
+
the Cloudflare runtime.
|
|
135
|
+
|
|
136
|
+
Arguments:
|
|
137
|
+
path Migration input JSON file, or a directory
|
|
138
|
+
containing migration.json
|
|
139
|
+
|
|
140
|
+
Flags:
|
|
141
|
+
--cwd <dir> Resolve <path> relative to <dir>
|
|
142
|
+
--json Emit JSON result
|
|
143
|
+
|
|
144
|
+
Exits 1 when human-review findings remain (so CI can gate on it).
|
|
145
|
+
`;
|
|
146
|
+
case 'report':
|
|
147
|
+
return `hs-x migrate report <path>
|
|
148
|
+
|
|
149
|
+
Summarize migration readiness: detected features, automatable vs human-review
|
|
150
|
+
finding counts, runtime requirements, and suggested next steps.
|
|
151
|
+
|
|
152
|
+
Arguments:
|
|
153
|
+
path Migration input JSON file, or a directory
|
|
154
|
+
containing migration.json
|
|
155
|
+
|
|
156
|
+
Flags:
|
|
157
|
+
--cwd <dir> Resolve <path> relative to <dir>
|
|
158
|
+
--json Emit JSON result
|
|
159
|
+
`;
|
|
160
|
+
case 'cards':
|
|
161
|
+
return `hs-x migrate cards <path>
|
|
162
|
+
|
|
163
|
+
Generate a replacement App Card project from a legacy CRM card migration
|
|
164
|
+
input, then validate the generated project. Fails (exit 1) if the input still
|
|
165
|
+
has human-review findings — resolve those first (see hs-x migrate inspect).
|
|
166
|
+
|
|
167
|
+
Arguments:
|
|
168
|
+
path Migration input JSON file, or a directory
|
|
169
|
+
containing migration.json
|
|
170
|
+
|
|
171
|
+
Flags:
|
|
172
|
+
--project <dir> Output directory (default: migrated-app)
|
|
173
|
+
--cwd <dir> Resolve paths relative to <dir>
|
|
174
|
+
--json Emit JSON result
|
|
175
|
+
`;
|
|
176
|
+
case 'swap-cards':
|
|
177
|
+
return `hs-x migrate swap-cards
|
|
178
|
+
|
|
179
|
+
Swap a live legacy CRM card for its replacement App Card via the HubSpot
|
|
180
|
+
developer API.
|
|
181
|
+
|
|
182
|
+
Flags:
|
|
183
|
+
--app-id <id> HubSpot app id (required)
|
|
184
|
+
--legacy-card-id <id> Legacy CRM card to retire (required)
|
|
185
|
+
--replacement-card-id <id> App Card that replaces it (required)
|
|
186
|
+
--helpdesk-card-id <id> Optional help-desk App Card variant
|
|
187
|
+
--local-hubspot Use the local HubSpot mock
|
|
188
|
+
--json Emit JSON result
|
|
189
|
+
|
|
190
|
+
Advanced:
|
|
191
|
+
--control-plane-url <u> Override control plane (HSX_CONTROL_PLANE_URL)
|
|
192
|
+
--account-id <id> HS-X account for lease/credential discovery
|
|
193
|
+
--user-id <id> Attribution for control-plane records
|
|
194
|
+
`;
|
|
195
|
+
default:
|
|
196
|
+
return undefined;
|
|
197
|
+
}
|
|
198
|
+
}
|
|
268
199
|
// ─── Native @effect/cli command (Phase 4) ───────────────────────────────────
|
|
269
200
|
// Subcommands inspect/report/cards (project analysis; fs lifted onto
|
|
270
201
|
// `@effect/platform` FileSystem/Path) + swap-cards (HubSpot developer client via
|
|
@@ -288,6 +219,24 @@ function readMigrationInputEff(path) {
|
|
|
288
219
|
const fs = yield* FileSystem.FileSystem;
|
|
289
220
|
const pathSvc = yield* Path.Path;
|
|
290
221
|
const info = yield* fs.stat(path).pipe(Effect.mapError((cause) => cliError('HSX_E_INTERNAL', `Could not read ${path}: ${String(cause)}`)));
|
|
222
|
+
if (info.type === 'Directory') {
|
|
223
|
+
const fixture = pathSvc.join(path, 'migration.json');
|
|
224
|
+
const hasFixture = yield* fs.exists(fixture).pipe(Effect.orElseSucceed(() => false));
|
|
225
|
+
if (!hasFixture) {
|
|
226
|
+
// Locked decision #4 (local-source half): a legacy PROJECT tree —
|
|
227
|
+
// hsproject.json names the platform version; component configs
|
|
228
|
+
// (serverless.json, webhooks/workflow-action hsmeta) assemble into
|
|
229
|
+
// the classifier input via detectMigrationSource.
|
|
230
|
+
const detected = yield* Effect.tryPromise({
|
|
231
|
+
try: () => detectMigrationSource(path),
|
|
232
|
+
catch: (cause) => cliError('HSX_E_INTERNAL', `Could not inspect ${path}: ${String(cause)}`),
|
|
233
|
+
});
|
|
234
|
+
if (detected.entry === 'unknown') {
|
|
235
|
+
return yield* Effect.fail(cliError('HSX_E_INPUT_BAD_FIELD', `${path} has no migration.json and no hsproject.json — nothing to migrate. Point at a legacy project root or a migration input file.`));
|
|
236
|
+
}
|
|
237
|
+
return detected.input;
|
|
238
|
+
}
|
|
239
|
+
}
|
|
291
240
|
const file = info.type === 'Directory' ? pathSvc.join(path, 'migration.json') : path;
|
|
292
241
|
const contents = yield* fs.readFileString(file).pipe(Effect.mapError((cause) => cliError('HSX_E_INTERNAL', `Could not read ${file}: ${String(cause)}`)));
|
|
293
242
|
return yield* Effect.try({
|
|
@@ -308,22 +257,20 @@ function writeProjectFilesEff(root, files) {
|
|
|
308
257
|
}
|
|
309
258
|
});
|
|
310
259
|
}
|
|
311
|
-
function writeStdout(text) {
|
|
312
|
-
return Effect.sync(() => process.stdout.write(text));
|
|
313
|
-
}
|
|
314
260
|
function migrateInspectBody(opts) {
|
|
315
261
|
return Effect.gen(function* () {
|
|
316
262
|
const cwd = yield* Cwd;
|
|
317
263
|
const pathSvc = yield* Path.Path;
|
|
264
|
+
const reporter = yield* Reporter;
|
|
318
265
|
const root = Option.getOrElse(opts.cwd, () => cwd);
|
|
319
266
|
const input = yield* readMigrationInputEff(pathSvc.resolve(root, opts.path));
|
|
320
267
|
const result = classifyMigrationInput(input);
|
|
321
268
|
const response = { ok: true, command: 'migrate inspect', ...result };
|
|
322
269
|
if (opts.json) {
|
|
323
|
-
yield*
|
|
270
|
+
yield* reporter.raw(`${JSON.stringify(response, null, 2)}\n`);
|
|
324
271
|
}
|
|
325
272
|
else {
|
|
326
|
-
yield*
|
|
273
|
+
yield* reporter.raw(renderMigrationInspect(response));
|
|
327
274
|
}
|
|
328
275
|
if (result.manualWork.length > 0) {
|
|
329
276
|
return yield* Effect.fail(exitWith(1));
|
|
@@ -334,6 +281,7 @@ function migrateReportBody(opts) {
|
|
|
334
281
|
return Effect.gen(function* () {
|
|
335
282
|
const cwd = yield* Cwd;
|
|
336
283
|
const pathSvc = yield* Path.Path;
|
|
284
|
+
const reporter = yield* Reporter;
|
|
337
285
|
const root = Option.getOrElse(opts.cwd, () => cwd);
|
|
338
286
|
const input = yield* readMigrationInputEff(pathSvc.resolve(root, opts.path));
|
|
339
287
|
const result = classifyMigrationInput(input);
|
|
@@ -351,10 +299,10 @@ function migrateReportBody(opts) {
|
|
|
351
299
|
nextSteps: migrationNextSteps(result),
|
|
352
300
|
};
|
|
353
301
|
if (opts.json) {
|
|
354
|
-
yield*
|
|
302
|
+
yield* reporter.raw(`${JSON.stringify(response, null, 2)}\n`);
|
|
355
303
|
}
|
|
356
304
|
else {
|
|
357
|
-
yield*
|
|
305
|
+
yield* reporter.raw(renderMigrationReport(response));
|
|
358
306
|
}
|
|
359
307
|
});
|
|
360
308
|
}
|
|
@@ -369,10 +317,10 @@ function migrateCardsBody(opts) {
|
|
|
369
317
|
if (result.manualWork.length > 0 || !isLegacyCardMigrationInput(input)) {
|
|
370
318
|
const response = { ok: false, command: 'migrate cards', ...result };
|
|
371
319
|
if (opts.json) {
|
|
372
|
-
yield*
|
|
320
|
+
yield* reporter.raw(`${JSON.stringify(response, null, 2)}\n`);
|
|
373
321
|
}
|
|
374
322
|
else {
|
|
375
|
-
yield*
|
|
323
|
+
yield* reporter.raw(renderMigrationInspect(response));
|
|
376
324
|
}
|
|
377
325
|
return yield* Effect.fail(exitWith(1));
|
|
378
326
|
}
|
|
@@ -389,7 +337,7 @@ function migrateCardsBody(opts) {
|
|
|
389
337
|
validation,
|
|
390
338
|
};
|
|
391
339
|
if (opts.json) {
|
|
392
|
-
yield*
|
|
340
|
+
yield* reporter.raw(`${JSON.stringify(response, null, 2)}\n`);
|
|
393
341
|
}
|
|
394
342
|
else {
|
|
395
343
|
yield* reporter.header(pathSvc.basename(outputRoot));
|
|
@@ -417,6 +365,104 @@ function migrateCardsBody(opts) {
|
|
|
417
365
|
}
|
|
418
366
|
});
|
|
419
367
|
}
|
|
368
|
+
/**
|
|
369
|
+
* The one-command flow (launch headline): detect → classify → generate the
|
|
370
|
+
* dupe project from every PORTABLE piece → report skipped pieces and
|
|
371
|
+
* pre-cutover decisions → guided next step into `hs-x dev`. Decision-type
|
|
372
|
+
* findings (forwarding/url-contract) never block dupe generation — the dupe
|
|
373
|
+
* is a testbed (locked decision #2); they gate CUTOVER, which is a separate
|
|
374
|
+
* command. Exit 1 only when nothing portable could be generated.
|
|
375
|
+
*/
|
|
376
|
+
function migrateRunBody(opts) {
|
|
377
|
+
return Effect.gen(function* () {
|
|
378
|
+
const cwd = yield* Cwd;
|
|
379
|
+
const pathSvc = yield* Path.Path;
|
|
380
|
+
const reporter = yield* Reporter;
|
|
381
|
+
const root = Option.getOrElse(opts.cwd, () => cwd);
|
|
382
|
+
const sourcePath = pathSvc.resolve(root, opts.path);
|
|
383
|
+
const input = yield* readMigrationInputEff(sourcePath);
|
|
384
|
+
const result = classifyMigrationInput(input);
|
|
385
|
+
let generated;
|
|
386
|
+
let generationError;
|
|
387
|
+
try {
|
|
388
|
+
generated = generateMigratedHsxProject(input);
|
|
389
|
+
}
|
|
390
|
+
catch (error) {
|
|
391
|
+
generationError = error instanceof Error ? error.message : String(error);
|
|
392
|
+
}
|
|
393
|
+
const hasCapabilities = generated !== undefined &&
|
|
394
|
+
Object.keys(generated.files).some((file) => file.startsWith('src/workers/') || file.includes('/cards/'));
|
|
395
|
+
if (!generated || !hasCapabilities) {
|
|
396
|
+
const response = {
|
|
397
|
+
ok: false,
|
|
398
|
+
command: 'migrate run',
|
|
399
|
+
...result,
|
|
400
|
+
...(generationError ? { error: generationError } : {}),
|
|
401
|
+
};
|
|
402
|
+
if (opts.json) {
|
|
403
|
+
yield* reporter.raw(`${JSON.stringify(response, null, 2)}\n`);
|
|
404
|
+
}
|
|
405
|
+
else {
|
|
406
|
+
yield* reporter.raw(renderMigrationInspect(response));
|
|
407
|
+
yield* reporter.error('HSX_E_MIGRATE_NOTHING_PORTABLE', generationError ?? 'No portable components detected in this source.', {
|
|
408
|
+
hint: 'Resolve the findings above, then re-run `hs-x migrate run`.',
|
|
409
|
+
docs_url: 'https://hs-x.dev/docs/guides/migrate',
|
|
410
|
+
});
|
|
411
|
+
yield* reporter.done(undefined, 1);
|
|
412
|
+
}
|
|
413
|
+
return yield* Effect.fail(exitWith(1));
|
|
414
|
+
}
|
|
415
|
+
const outputRoot = pathSvc.resolve(root, Option.getOrUndefined(opts.out) ?? 'migrated-app');
|
|
416
|
+
yield* writeProjectFilesEff(outputRoot, generated.files);
|
|
417
|
+
const validation = yield* Effect.promise(() => validateProject({ root: outputRoot }));
|
|
418
|
+
const response = {
|
|
419
|
+
ok: validation.ok,
|
|
420
|
+
command: 'migrate run',
|
|
421
|
+
outputRoot,
|
|
422
|
+
createdFiles: Object.keys(generated.files).sort(),
|
|
423
|
+
inspect: result,
|
|
424
|
+
validation,
|
|
425
|
+
};
|
|
426
|
+
if (opts.json) {
|
|
427
|
+
yield* reporter.raw(`${JSON.stringify(response, null, 2)}\n`);
|
|
428
|
+
}
|
|
429
|
+
else {
|
|
430
|
+
yield* reporter.header(pathSvc.basename(outputRoot));
|
|
431
|
+
const genStep = yield* reporter.step('Generated migrated HS-X project (the dupe)');
|
|
432
|
+
yield* genStep.ok(`${response.createdFiles.length} files ${outputRoot}`);
|
|
433
|
+
if (result.automatable.length > 0) {
|
|
434
|
+
yield* reporter.info(`Ported automatically:`);
|
|
435
|
+
for (const finding of result.automatable)
|
|
436
|
+
yield* reporter.info(` - ${finding.message}`);
|
|
437
|
+
}
|
|
438
|
+
if (result.manualWork.length > 0) {
|
|
439
|
+
yield* reporter.info('Decisions before cutover (the dupe is a testbed; these do not block it):');
|
|
440
|
+
for (const finding of result.manualWork) {
|
|
441
|
+
yield* reporter.info(` - [${finding.code}] ${finding.message} docs: https://hs-x.dev/docs/guides/migrate#${finding.code.replace(/\./g, '-')}`);
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
const vStep = yield* reporter.step('Validation');
|
|
445
|
+
if (validation.ok) {
|
|
446
|
+
yield* vStep.ok();
|
|
447
|
+
}
|
|
448
|
+
else {
|
|
449
|
+
yield* vStep.fail(`${validation.diagnostics.length} issue(s)`);
|
|
450
|
+
for (const d of validation.diagnostics) {
|
|
451
|
+
yield* reporter.error(d.code ?? 'diagnostic', d.message ?? JSON.stringify(d));
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
yield* reporter.info('');
|
|
455
|
+
yield* reporter.info('Next steps:');
|
|
456
|
+
yield* reporter.info(` cd ${pathSvc.relative(root, outputRoot) || '.'} && bun install`);
|
|
457
|
+
yield* reporter.info(' hs-x dev # test every migrated capability locally');
|
|
458
|
+
yield* reporter.info(' hs-x deploy # upload the dupe as a NET-NEW app when ready');
|
|
459
|
+
yield* reporter.done(validation.ok ? 'Migrated' : 'Failed', validation.ok ? 0 : 1);
|
|
460
|
+
}
|
|
461
|
+
if (!validation.ok) {
|
|
462
|
+
return yield* Effect.fail(exitWith(1));
|
|
463
|
+
}
|
|
464
|
+
});
|
|
465
|
+
}
|
|
420
466
|
function migrateSwapCardsBody(opts, argv) {
|
|
421
467
|
return Effect.gen(function* () {
|
|
422
468
|
const reporter = yield* Reporter;
|
|
@@ -451,7 +497,7 @@ function migrateSwapCardsBody(opts, argv) {
|
|
|
451
497
|
swap,
|
|
452
498
|
};
|
|
453
499
|
if (opts.json) {
|
|
454
|
-
yield*
|
|
500
|
+
yield* reporter.raw(`${JSON.stringify(response, null, 2)}\n`);
|
|
455
501
|
}
|
|
456
502
|
else {
|
|
457
503
|
yield* reporter.header(legacyCrmCardId);
|
|
@@ -504,7 +550,67 @@ function swapCardsArgv(opts) {
|
|
|
504
550
|
argv.push('--user-id', opts.userId.value);
|
|
505
551
|
return argv;
|
|
506
552
|
}
|
|
553
|
+
const migrateRunPathArg = Args.text({ name: 'path' }).pipe(Args.withDefault('.'));
|
|
554
|
+
const migrateOutOption = Options.text('out').pipe(Options.optional);
|
|
555
|
+
/**
|
|
556
|
+
* `hs-x migrate cutover [path]` — DRY-RUN ONLY today (locked decision #2:
|
|
557
|
+
* cutover never executes against a real app without explicit human go).
|
|
558
|
+
* Prints the ordered plan of what cutover will change on the ORIGINAL app,
|
|
559
|
+
* in place, plus the open decisions gating execution.
|
|
560
|
+
*/
|
|
561
|
+
function migrateCutoverBody(opts) {
|
|
562
|
+
return Effect.gen(function* () {
|
|
563
|
+
const cwd = yield* Cwd;
|
|
564
|
+
const pathSvc = yield* Path.Path;
|
|
565
|
+
const reporter = yield* Reporter;
|
|
566
|
+
const root = Option.getOrElse(opts.cwd, () => cwd);
|
|
567
|
+
const sourcePath = pathSvc.resolve(root, opts.path);
|
|
568
|
+
if (opts.execute) {
|
|
569
|
+
return yield* Effect.fail(cliError('HSX_E_CUTOVER_EXECUTE_GATED', 'Cutover execution against a real app requires an explicit human go and is not enabled in this build.'));
|
|
570
|
+
}
|
|
571
|
+
const detected = yield* Effect.tryPromise({
|
|
572
|
+
try: () => detectMigrationSource(sourcePath),
|
|
573
|
+
catch: (cause) => cliError('HSX_E_INTERNAL', `Could not inspect ${sourcePath}: ${String(cause)}`),
|
|
574
|
+
});
|
|
575
|
+
if (detected.entry === 'unknown') {
|
|
576
|
+
return yield* Effect.fail(cliError('HSX_E_INPUT_BAD_FIELD', `${sourcePath} has no migration.json and no hsproject.json — nothing to plan. Point at the ORIGINAL legacy app's source.`));
|
|
577
|
+
}
|
|
578
|
+
const classification = classifyMigrationInput(detected.input);
|
|
579
|
+
const plan = planCutover(detected, classification);
|
|
580
|
+
if (opts.json) {
|
|
581
|
+
yield* reporter.raw(`${JSON.stringify({ ok: true, command: 'migrate cutover', mode: 'dry-run', ...plan }, null, 2)}\n`);
|
|
582
|
+
return;
|
|
583
|
+
}
|
|
584
|
+
yield* reporter.header(`cutover plan (dry-run) — ${plan.sourceVersion} source`);
|
|
585
|
+
yield* reporter.info('ONE-WAY: the original app cannot downgrade from 2026.03 after cutover.');
|
|
586
|
+
yield* reporter.info('');
|
|
587
|
+
for (const phase of ['preflight', 'in-place', 'post']) {
|
|
588
|
+
const phaseSteps = plan.steps.filter((step) => step.phase === phase);
|
|
589
|
+
if (phaseSteps.length === 0)
|
|
590
|
+
continue;
|
|
591
|
+
yield* reporter.info(`${phase.toUpperCase()}:`);
|
|
592
|
+
for (const step of phaseSteps) {
|
|
593
|
+
yield* reporter.info(` - ${step.title}${step.gatedBy ? ` [gated: ${step.gatedBy}]` : ''}`);
|
|
594
|
+
yield* reporter.info(` ${step.detail}`);
|
|
595
|
+
}
|
|
596
|
+
yield* reporter.info('');
|
|
597
|
+
}
|
|
598
|
+
if (plan.openDecisions.length > 0) {
|
|
599
|
+
yield* reporter.info(`${plan.openDecisions.length} open decision(s) gate execution — resolve them, re-run this plan, then request execution.`);
|
|
600
|
+
}
|
|
601
|
+
else {
|
|
602
|
+
yield* reporter.info('No open decisions. Execution still requires an explicit human go (see LAUNCH-PLAN NEEDS HUMAN).');
|
|
603
|
+
}
|
|
604
|
+
yield* reporter.done('Planned');
|
|
605
|
+
});
|
|
606
|
+
}
|
|
607
|
+
const migrateExecuteOption = Options.boolean('execute').pipe(Options.withDefault(false));
|
|
608
|
+
const migrateCutoverPathArg = Args.text({ name: 'path' }).pipe(Args.withDefault('.'));
|
|
609
|
+
const migrateCutoverCmd = Command.make('cutover', { path: migrateCutoverPathArg, cwd: cwdOption, json: jsonOption, execute: migrateExecuteOption }, (opts) => runHandler('migrate', { json: opts.json }, migrateCutoverBody(opts)));
|
|
610
|
+
const migrateRunCmd = Command.make('run', { path: migrateRunPathArg, out: migrateOutOption, cwd: cwdOption, json: jsonOption }, (opts) => runHandler('migrate', { json: opts.json }, migrateRunBody(opts)));
|
|
507
611
|
export const migrateCmd = Command.make('migrate').pipe(Command.withSubcommands([
|
|
612
|
+
migrateRunCmd,
|
|
613
|
+
migrateCutoverCmd,
|
|
508
614
|
migrateInspectCmd,
|
|
509
615
|
migrateReportCmd,
|
|
510
616
|
migrateCardsCmd,
|