@hozu/cli 0.10.0 → 0.12.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/commands/add-widget.d.ts.map +1 -1
- package/dist/commands/add-widget.js +5 -4
- package/dist/commands/add-widget.js.map +1 -1
- package/dist/commands/app.d.ts.map +1 -1
- package/dist/commands/app.js +10 -0
- package/dist/commands/app.js.map +1 -1
- package/dist/commands/build.d.ts.map +1 -1
- package/dist/commands/build.js +5 -1
- package/dist/commands/build.js.map +1 -1
- package/dist/commands/call.d.ts +16 -0
- package/dist/commands/call.d.ts.map +1 -0
- package/dist/commands/call.js +100 -0
- package/dist/commands/call.js.map +1 -0
- package/dist/commands/check.d.ts +10 -1
- package/dist/commands/check.d.ts.map +1 -1
- package/dist/commands/check.js +43 -28
- package/dist/commands/check.js.map +1 -1
- package/dist/commands/explain.d.ts.map +1 -1
- package/dist/commands/explain.js +2 -1
- package/dist/commands/explain.js.map +1 -1
- package/dist/commands/impact.js +1 -1
- package/dist/commands/impact.js.map +1 -1
- package/dist/commands/inspect.d.ts.map +1 -1
- package/dist/commands/inspect.js +9 -0
- package/dist/commands/inspect.js.map +1 -1
- package/dist/commands/map.d.ts.map +1 -1
- package/dist/commands/map.js +7 -2
- package/dist/commands/map.js.map +1 -1
- package/dist/commands/migrate.d.ts +17 -16
- package/dist/commands/migrate.d.ts.map +1 -1
- package/dist/commands/migrate.js +205 -297
- package/dist/commands/migrate.js.map +1 -1
- package/dist/commands/scaffold.d.ts.map +1 -1
- package/dist/commands/scaffold.js +8 -4
- package/dist/commands/scaffold.js.map +1 -1
- package/dist/contract.d.ts +84 -1
- package/dist/contract.d.ts.map +1 -1
- package/dist/load.d.ts +2 -1
- package/dist/load.d.ts.map +1 -1
- package/dist/load.js +5 -4
- package/dist/load.js.map +1 -1
- package/dist/main.d.ts.map +1 -1
- package/dist/main.js +33 -5
- package/dist/main.js.map +1 -1
- package/dist/migrate/ast.d.ts +31 -0
- package/dist/migrate/ast.d.ts.map +1 -0
- package/dist/migrate/ast.js +61 -0
- package/dist/migrate/ast.js.map +1 -0
- package/dist/migrate/record-ir.d.ts +2 -0
- package/dist/migrate/record-ir.d.ts.map +1 -0
- package/dist/migrate/record-ir.js +16 -0
- package/dist/migrate/record-ir.js.map +1 -0
- package/dist/migrate/step-0.11.d.ts +15 -0
- package/dist/migrate/step-0.11.d.ts.map +1 -0
- package/dist/migrate/step-0.11.js +88 -0
- package/dist/migrate/step-0.11.js.map +1 -0
- package/dist/migrate/step-0.12.d.ts +6 -0
- package/dist/migrate/step-0.12.d.ts.map +1 -0
- package/dist/migrate/step-0.12.js +14 -0
- package/dist/migrate/step-0.12.js.map +1 -0
- package/dist/migrate/steps.d.ts +28 -0
- package/dist/migrate/steps.d.ts.map +1 -0
- package/dist/migrate/steps.js +42 -0
- package/dist/migrate/steps.js.map +1 -0
- package/dist/output.d.ts.map +1 -1
- package/dist/output.js +2 -0
- package/dist/output.js.map +1 -1
- package/package.json +8 -8
- package/schema/call.schema.json +136 -0
- package/schema/check.schema.json +26 -2
- package/schema/explain.schema.json +14 -1
- package/schema/impact.schema.json +22 -1
- package/schema/inspect.schema.json +75 -4
- package/schema/map.schema.json +17 -0
- package/schema/migrate.schema.json +740 -0
- package/schema/plan.schema.json +4 -2
- package/schema/render.schema.json +3 -1
- package/schema/validate.schema.json +3 -1
- package/dist/commands/migrate-agent.d.ts +0 -12
- package/dist/commands/migrate-agent.d.ts.map +0 -1
- package/dist/commands/migrate-agent.js +0 -6
- package/dist/commands/migrate-agent.js.map +0 -1
- package/dist/commands/migrate-app.d.ts +0 -15
- package/dist/commands/migrate-app.d.ts.map +0 -1
- package/dist/commands/migrate-app.js +0 -334
- package/dist/commands/migrate-app.js.map +0 -1
- package/dist/commands/migrate-ast.d.ts +0 -63
- package/dist/commands/migrate-ast.d.ts.map +0 -1
- package/dist/commands/migrate-ast.js +0 -257
- package/dist/commands/migrate-ast.js.map +0 -1
- package/dist/commands/migrate-forms.d.ts +0 -4
- package/dist/commands/migrate-forms.d.ts.map +0 -1
- package/dist/commands/migrate-forms.js +0 -120
- package/dist/commands/migrate-forms.js.map +0 -1
- package/dist/commands/migrate-freshness.d.ts +0 -4
- package/dist/commands/migrate-freshness.d.ts.map +0 -1
- package/dist/commands/migrate-freshness.js +0 -47
- package/dist/commands/migrate-freshness.js.map +0 -1
- package/dist/commands/migrate-head.d.ts +0 -8
- package/dist/commands/migrate-head.d.ts.map +0 -1
- package/dist/commands/migrate-head.js +0 -132
- package/dist/commands/migrate-head.js.map +0 -1
- package/dist/commands/migrate-links.d.ts +0 -5
- package/dist/commands/migrate-links.d.ts.map +0 -1
- package/dist/commands/migrate-links.js +0 -55
- package/dist/commands/migrate-links.js.map +0 -1
- package/dist/commands/migrate-lists.d.ts +0 -9
- package/dist/commands/migrate-lists.d.ts.map +0 -1
- package/dist/commands/migrate-lists.js +0 -225
- package/dist/commands/migrate-lists.js.map +0 -1
- package/dist/commands/migrate-normalize.d.ts +0 -27
- package/dist/commands/migrate-normalize.d.ts.map +0 -1
- package/dist/commands/migrate-normalize.js +0 -252
- package/dist/commands/migrate-normalize.js.map +0 -1
- package/dist/commands/migrate-ops.d.ts +0 -4
- package/dist/commands/migrate-ops.d.ts.map +0 -1
- package/dist/commands/migrate-ops.js +0 -192
- package/dist/commands/migrate-ops.js.map +0 -1
- package/dist/commands/migrate-parts.d.ts +0 -7
- package/dist/commands/migrate-parts.d.ts.map +0 -1
- package/dist/commands/migrate-parts.js +0 -166
- package/dist/commands/migrate-parts.js.map +0 -1
- package/dist/commands/migrate-stale.d.ts +0 -24
- package/dist/commands/migrate-stale.d.ts.map +0 -1
- package/dist/commands/migrate-stale.js +0 -114
- package/dist/commands/migrate-stale.js.map +0 -1
package/dist/commands/migrate.js
CHANGED
|
@@ -1,26 +1,32 @@
|
|
|
1
1
|
import { spawnSync } from 'node:child_process';
|
|
2
2
|
import { existsSync, mkdirSync, readdirSync, readFileSync, rmSync, statSync, writeFileSync } from 'node:fs';
|
|
3
|
+
import { createRequire } from 'node:module';
|
|
3
4
|
import { dirname, extname, join, relative, resolve } from 'node:path';
|
|
4
|
-
import { fileURLToPath } from 'node:url';
|
|
5
|
-
import { runnerOf, writeAgentFiles } from 'create-hozu';
|
|
5
|
+
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
6
6
|
import { HozuCliError } from '../errors.js';
|
|
7
7
|
import { load } from '../load.js';
|
|
8
|
+
import { chain, compareMinor, minorOf, OLDEST } from '../migrate/steps.js';
|
|
8
9
|
import { runCheck } from './check.js';
|
|
9
|
-
import {
|
|
10
|
-
import { migrateApp, migratePackage } from './migrate-app.js';
|
|
11
|
-
import { forget, lineOf, parse, walk } from './migrate-ast.js';
|
|
12
|
-
import { migrateForms } from './migrate-forms.js';
|
|
13
|
-
import { migrateFreshness } from './migrate-freshness.js';
|
|
14
|
-
import { migrateHead } from './migrate-head.js';
|
|
15
|
-
import { migrateLinks } from './migrate-links.js';
|
|
16
|
-
import { migrateLists } from './migrate-lists.js';
|
|
17
|
-
import { differences, normalize07 } from './migrate-normalize.js';
|
|
18
|
-
import { migrateOps } from './migrate-ops.js';
|
|
19
|
-
import { migrateParts } from './migrate-parts.js';
|
|
10
|
+
import { runSkill } from './skill.js';
|
|
20
11
|
const SKIP = /(^|\/)(node_modules|dist|\.[^/]+)(\/|$)/;
|
|
21
|
-
const
|
|
12
|
+
const cliVersion = () => {
|
|
13
|
+
const pkg = new URL('../../package.json', import.meta.url);
|
|
14
|
+
return JSON.parse(readFileSync(fileURLToPath(pkg), 'utf8')).version;
|
|
15
|
+
};
|
|
16
|
+
export function installedCore(config) {
|
|
17
|
+
let d = dirname(config);
|
|
18
|
+
for (;;) {
|
|
19
|
+
const pkg = join(d, 'node_modules/@hozu/core/package.json');
|
|
20
|
+
if (existsSync(pkg))
|
|
21
|
+
return JSON.parse(readFileSync(pkg, 'utf8')).version;
|
|
22
|
+
const up = dirname(d);
|
|
23
|
+
if (up === d)
|
|
24
|
+
return null;
|
|
25
|
+
d = up;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
22
28
|
export function sources(dir) {
|
|
23
|
-
const out =
|
|
29
|
+
const out = [];
|
|
24
30
|
const visit = (d) => {
|
|
25
31
|
for (const name of readdirSync(d).sort()) {
|
|
26
32
|
const path = join(d, name);
|
|
@@ -29,329 +35,231 @@ export function sources(dir) {
|
|
|
29
35
|
if (statSync(path).isDirectory())
|
|
30
36
|
visit(path);
|
|
31
37
|
else if (extname(name) === '.ts' && !name.endsWith('.d.ts'))
|
|
32
|
-
out.
|
|
38
|
+
out.push(path);
|
|
33
39
|
}
|
|
34
40
|
};
|
|
35
41
|
visit(dir);
|
|
36
42
|
return out;
|
|
37
43
|
}
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
const
|
|
44
|
+
/** The IR as the app's installed Hozu builds it, in a child process with the app's own transform hook. */
|
|
45
|
+
function recordWithInstalled(config) {
|
|
46
|
+
const require = createRequire(config);
|
|
47
|
+
let register;
|
|
41
48
|
try {
|
|
42
|
-
|
|
49
|
+
register = require.resolve('@hozu/transform/register');
|
|
43
50
|
}
|
|
44
51
|
catch {
|
|
45
|
-
|
|
46
|
-
core: null,
|
|
47
|
-
irVersion: null,
|
|
48
|
-
skipped: `the stale check failed: ${child.stderr.toString().split('\n').find(Boolean) ?? 'no output'}`,
|
|
49
|
-
stale: [],
|
|
50
|
-
routes: {},
|
|
51
|
-
errors: [],
|
|
52
|
-
ir: null,
|
|
53
|
-
};
|
|
52
|
+
throw new HozuCliError('config', 'hozu migrate needs the app’s installed @hozu/transform to read the old IR', ['npm install, then run hozu migrate again']);
|
|
54
53
|
}
|
|
55
|
-
|
|
56
|
-
const
|
|
57
|
-
[
|
|
58
|
-
'any',
|
|
59
|
-
/AsyncLocalStorage/,
|
|
60
|
-
'request state in AsyncLocalStorage (hand-made i18n?): the URL holds the language (site.locales)',
|
|
61
|
-
'i18n',
|
|
62
|
-
],
|
|
63
|
-
[
|
|
64
|
-
'any',
|
|
65
|
-
/accept-language/i,
|
|
66
|
-
'hand-made language negotiation: the URL holds the language (site.locales), no Accept-Language redirect',
|
|
67
|
-
'i18n',
|
|
68
|
-
],
|
|
69
|
-
[
|
|
70
|
-
'any',
|
|
71
|
-
/Session\s*=\s*z\.object\(\{[^}]*\b(?:lang|locale|language)\s*:/,
|
|
72
|
-
'a language in the session: the URL holds the language (site.locales), not the session',
|
|
73
|
-
'i18n',
|
|
74
|
-
],
|
|
75
|
-
[
|
|
76
|
-
'resolvers',
|
|
77
|
-
/<html\b[^>]*\blang\b|text\/html|<!doctype/i,
|
|
78
|
-
'HTML from a resolver is HZ053: make it a ui.page with head.failed',
|
|
79
|
-
'pages',
|
|
80
|
-
],
|
|
81
|
-
];
|
|
82
|
-
const REDIRECT = "a redirect Response: declare output: 'redirect' and return redirect(ui.link(…))";
|
|
83
|
-
const statusOf = (n) => {
|
|
84
|
-
if (n?.type === 'Literal' && typeof n.value === 'number')
|
|
85
|
-
return n.value;
|
|
86
|
-
if (n?.type !== 'ObjectExpression')
|
|
87
|
-
return null;
|
|
88
|
-
const p = n.properties.find((x) => x.type === 'Property' && (x.key.name ?? x.key.value) === 'status');
|
|
89
|
-
return p?.value.type === 'Literal' && typeof p.value.value === 'number' ? p.value.value : null;
|
|
90
|
-
};
|
|
91
|
-
function responses(file, source) {
|
|
92
|
-
let program;
|
|
54
|
+
const self = fileURLToPath(import.meta.url);
|
|
55
|
+
const script = join(dirname(self), `../migrate/record-ir${extname(self)}`);
|
|
56
|
+
const child = spawnSync(process.execPath, ['--no-warnings', '--import', pathToFileURL(register).href, script, config], { cwd: dirname(config), maxBuffer: 1 << 28, env: { ...process.env, NODE_OPTIONS: '' } });
|
|
93
57
|
try {
|
|
94
|
-
|
|
58
|
+
return JSON.parse(child.stdout.toString());
|
|
95
59
|
}
|
|
96
60
|
catch {
|
|
97
|
-
|
|
61
|
+
throw new HozuCliError('build', `hozu migrate could not build the app with its installed Hozu: ${child.stderr.toString().split('\n').find(Boolean) ?? 'no output'}`, ['npx hozu check # fix the app on its current version first']);
|
|
98
62
|
}
|
|
63
|
+
}
|
|
64
|
+
/** JSON pointers where two IRs differ, each with both values, at most 50. */
|
|
65
|
+
export function differences(before, after, at = '') {
|
|
99
66
|
const out = [];
|
|
100
|
-
|
|
101
|
-
const
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
c.object.name === 'Response' &&
|
|
107
|
-
['json', 'redirect'].includes(c.property.name);
|
|
108
|
-
if (!built && !helper)
|
|
67
|
+
const show = (v) => {
|
|
68
|
+
const s = v === undefined ? 'absent' : JSON.stringify(v);
|
|
69
|
+
return s.length > 80 ? `${s.slice(0, 77)}…` : s;
|
|
70
|
+
};
|
|
71
|
+
const visit = (a, b, path) => {
|
|
72
|
+
if (out.length >= 50)
|
|
109
73
|
return;
|
|
110
|
-
|
|
111
|
-
if (status === null || status < 300 || status >= 500)
|
|
74
|
+
if (JSON.stringify(a) === JSON.stringify(b))
|
|
112
75
|
return;
|
|
113
|
-
const
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
});
|
|
124
|
-
});
|
|
76
|
+
const objects = a && b && typeof a === 'object' && typeof b === 'object' && Array.isArray(a) === Array.isArray(b);
|
|
77
|
+
if (!objects) {
|
|
78
|
+
out.push(`${path || '/'}: ${show(a)} → ${show(b)}`);
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
const keys = [...new Set([...Object.keys(a), ...Object.keys(b)])].sort();
|
|
82
|
+
for (const k of keys)
|
|
83
|
+
visit(a[k], b[k], `${path}/${k.replace(/~/g, '~0').replace(/\//g, '~1')}`);
|
|
84
|
+
};
|
|
85
|
+
visit(before, after, at);
|
|
125
86
|
return out;
|
|
126
87
|
}
|
|
127
|
-
|
|
128
|
-
|
|
88
|
+
function raisePackages(pkgPath, version, write) {
|
|
89
|
+
if (!existsSync(pkgPath))
|
|
90
|
+
return [];
|
|
91
|
+
const source = readFileSync(pkgPath, 'utf8');
|
|
92
|
+
const pkg = JSON.parse(source);
|
|
129
93
|
const out = [];
|
|
130
|
-
const
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
export function rewriteSources(dir, config, before, routes) {
|
|
142
|
-
const files = new Map(before);
|
|
143
|
-
const notes = [];
|
|
144
|
-
const passes = [
|
|
145
|
-
migrateOps,
|
|
146
|
-
migrateHead,
|
|
147
|
-
migrateFreshness,
|
|
148
|
-
(s, f) => migrateLinks(s, f, routes),
|
|
149
|
-
migrateForms,
|
|
150
|
-
migrateLists,
|
|
151
|
-
];
|
|
152
|
-
for (const pass of passes)
|
|
153
|
-
for (const [file, source] of files) {
|
|
154
|
-
forget();
|
|
155
|
-
const r = pass(source, file);
|
|
156
|
-
files.set(file, r.code);
|
|
157
|
-
notes.push(...r.notes);
|
|
94
|
+
for (const field of ['dependencies', 'devDependencies'])
|
|
95
|
+
for (const [name, range] of Object.entries(pkg[field] ?? {})) {
|
|
96
|
+
if (!name.startsWith('@hozu/') && name !== 'create-hozu')
|
|
97
|
+
continue;
|
|
98
|
+
if (/^(workspace|file|link):/.test(range))
|
|
99
|
+
continue;
|
|
100
|
+
const next = `^${version}`;
|
|
101
|
+
if (range === next)
|
|
102
|
+
continue;
|
|
103
|
+
pkg[field][name] = next;
|
|
104
|
+
out.push({ name, from: range, to: next });
|
|
158
105
|
}
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
for (const [f, s] of parts.files)
|
|
163
|
-
files.set(f, s);
|
|
164
|
-
notes.push(...parts.notes.filter((n) => !notes.some((m) => m.file === n.file && m.message === n.message)));
|
|
165
|
-
if (!parts.files.size)
|
|
166
|
-
break;
|
|
167
|
-
}
|
|
168
|
-
forget();
|
|
169
|
-
const app = migrateApp(dir, config, files);
|
|
170
|
-
for (const [f, s] of app.write)
|
|
171
|
-
files.set(f, s);
|
|
172
|
-
notes.push(...app.notes);
|
|
173
|
-
for (const f of app.remove)
|
|
174
|
-
files.delete(f);
|
|
175
|
-
for (const [file, source] of files)
|
|
176
|
-
notes.push(...printed(file, source));
|
|
177
|
-
forget();
|
|
178
|
-
return { files, notes, app };
|
|
106
|
+
if (out.length && write)
|
|
107
|
+
writeFileSync(pkgPath, `${JSON.stringify(pkg, null, 2)}\n`);
|
|
108
|
+
return out;
|
|
179
109
|
}
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
110
|
+
const installCommand = (dir) => existsSync(join(dir, 'pnpm-lock.yaml'))
|
|
111
|
+
? 'pnpm install'
|
|
112
|
+
: existsSync(join(dir, 'yarn.lock'))
|
|
113
|
+
? 'yarn install'
|
|
114
|
+
: 'npm install';
|
|
115
|
+
export async function runMigrate(cwd, options) {
|
|
116
|
+
const config = resolve(cwd, options.config ?? 'hozu.config.ts');
|
|
184
117
|
if (!existsSync(config))
|
|
185
118
|
throw new HozuCliError('config', `No config found at ${config}`, [
|
|
186
|
-
'
|
|
119
|
+
'run hozu migrate in the app directory',
|
|
187
120
|
]);
|
|
188
121
|
const dir = dirname(config);
|
|
189
|
-
const rel = (
|
|
190
|
-
const
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
const
|
|
197
|
-
const
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
122
|
+
const rel = (p) => relative(cwd, p) || '.';
|
|
123
|
+
const installed = options.versions?.installed ?? installedCore(config);
|
|
124
|
+
if (!installed)
|
|
125
|
+
throw new HozuCliError('config', 'No installed @hozu/core: install the app’s dependencies first', [
|
|
126
|
+
installCommand(dir),
|
|
127
|
+
]);
|
|
128
|
+
const target = options.versions?.target ?? cliVersion();
|
|
129
|
+
const from = minorOf(installed);
|
|
130
|
+
const to = minorOf(target);
|
|
131
|
+
if (compareMinor(from, OLDEST) < 0)
|
|
132
|
+
throw new HozuCliError('usage', `hozu migrate starts at ${OLDEST}.0; this app is on ${installed}. Upgrade it to ${OLDEST} by hand with the CHANGELOG first`, ['https://github.com/olevatorr/Hozu/blob/main/CHANGELOG.md']);
|
|
133
|
+
if (compareMinor(from, to) > 0)
|
|
134
|
+
throw new HozuCliError('usage', `The app is on ${installed}, newer than this CLI (${target})`, [
|
|
135
|
+
`npx @hozu/cli@${installed} …`,
|
|
136
|
+
]);
|
|
137
|
+
const record = join(dir, `.hozu/migrate-${from}.json`);
|
|
138
|
+
const out = {
|
|
139
|
+
ok: true,
|
|
140
|
+
from: installed,
|
|
141
|
+
to: target,
|
|
142
|
+
phase: 'current',
|
|
143
|
+
dryRun: options.dryRun,
|
|
144
|
+
steps: [],
|
|
145
|
+
changed: [],
|
|
146
|
+
notes: [],
|
|
147
|
+
packages: [],
|
|
148
|
+
record: null,
|
|
149
|
+
ir: { compared: false, skipped: null, differences: [] },
|
|
150
|
+
guide: [],
|
|
151
|
+
check: null,
|
|
152
|
+
next: [],
|
|
204
153
|
};
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
const
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
const runner = runnerOf(process.env.npm_config_user_agent);
|
|
222
|
-
const agent = targets.length === 2 ? 'both' : targets[0].guide === 'CLAUDE.md' ? 'claude' : 'agents';
|
|
223
|
-
const existing = new Map(targets.map((t) => [
|
|
224
|
-
t.guide,
|
|
225
|
-
existsSync(join(dir, t.guide)) ? readFileSync(join(dir, t.guide), 'utf8') : null,
|
|
226
|
-
]));
|
|
227
|
-
const skills = new Map(targets.map((t) => [t.skill, contentOf(join(dir, t.skill))]));
|
|
228
|
-
await writeAgentFiles(dir, agent, { name: dir.split('/').pop(), runner });
|
|
229
|
-
for (const t of targets) {
|
|
230
|
-
if (contentOf(join(dir, t.skill)) !== skills.get(t.skill))
|
|
231
|
-
changed.push(rel(join(dir, t.skill)));
|
|
232
|
-
const file = join(dir, t.guide);
|
|
233
|
-
const r = migrateGuide(existing.get(t.guide) ?? null, t, dir, runner);
|
|
234
|
-
if (r.kind === 'custom') {
|
|
235
|
-
guide.push({ file: rel(file), state: 'custom', block: r.block });
|
|
236
|
-
}
|
|
237
|
-
else {
|
|
238
|
-
if (r.kind !== 'current')
|
|
239
|
-
write(file, r.code);
|
|
240
|
-
guide.push({ file: rel(file), state: r.kind, block: null });
|
|
154
|
+
if (compareMinor(from, to) < 0) {
|
|
155
|
+
const plan = chain(from, to);
|
|
156
|
+
if (!plan)
|
|
157
|
+
throw new HozuCliError('usage', `No migration path from ${from} to ${to}`, []);
|
|
158
|
+
out.phase = 'rewrite';
|
|
159
|
+
out.steps = plan.map((s) => ({ from: s.from, to: s.to, summary: s.summary }));
|
|
160
|
+
const ir = (options.recordIR ?? recordWithInstalled)(config);
|
|
161
|
+
for (const file of sources(dir)) {
|
|
162
|
+
let code = readFileSync(file, 'utf8');
|
|
163
|
+
const original = code;
|
|
164
|
+
let edits = 0;
|
|
165
|
+
for (const step of plan) {
|
|
166
|
+
const r = step.rewrite(rel(file), code);
|
|
167
|
+
code = r.code;
|
|
168
|
+
edits += r.count;
|
|
169
|
+
out.notes.push(...r.notes);
|
|
241
170
|
}
|
|
171
|
+
if (code === original)
|
|
172
|
+
continue;
|
|
173
|
+
out.changed.push({ file: rel(file), edits });
|
|
174
|
+
if (!options.dryRun)
|
|
175
|
+
writeFileSync(file, code);
|
|
242
176
|
}
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
next.push(`upgrade every @hozu/* dependency to 0.8: ${upgradeCommand(dir)}`);
|
|
250
|
-
next.push('npx hozu migrate 0.8 # again after the upgrade: nothing to rewrite, then it runs hozu check');
|
|
251
|
-
if (on07.ir)
|
|
252
|
-
next.push(`keep ${RECORD} until that second run: it holds the 0.7 IR it compares with (a reinstall keeps it; delete it afterwards)`);
|
|
253
|
-
}
|
|
254
|
-
else if (options.runCheck !== false) {
|
|
255
|
-
const loaded = await load(configArg, cwd);
|
|
256
|
-
if (existsSync(record)) {
|
|
257
|
-
const before = normalize07(JSON.parse(readFileSync(record, 'utf8')));
|
|
258
|
-
ir.compared = true;
|
|
259
|
-
ir.differences = differences(before, loaded.build(false).ir);
|
|
260
|
-
next.push(`delete ${RECORD}: the IR comparison with 0.7 is done`);
|
|
177
|
+
for (const step of plan)
|
|
178
|
+
out.changed.push(...(step.files?.(dir, !options.dryRun) ?? []));
|
|
179
|
+
out.packages = raisePackages(join(dir, 'package.json'), target, !options.dryRun);
|
|
180
|
+
if (!options.dryRun) {
|
|
181
|
+
mkdirSync(dirname(record), { recursive: true });
|
|
182
|
+
writeFileSync(join(dir, `.hozu/migrate-${to}.json`), JSON.stringify({ from, to, ir }));
|
|
261
183
|
}
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
184
|
+
out.record = rel(join(dir, `.hozu/migrate-${to}.json`));
|
|
185
|
+
out.next.push(`${installCommand(dir)} # installs Hozu ${target}`, 'npx hozu migrate # again: compares the IR with the old one and runs hozu check');
|
|
186
|
+
return out;
|
|
187
|
+
}
|
|
188
|
+
const saved = join(dir, `.hozu/migrate-${to}.json`);
|
|
189
|
+
if (!existsSync(saved)) {
|
|
190
|
+
out.next.push('Nothing to migrate: the app is on this CLI’s version.');
|
|
191
|
+
return out;
|
|
267
192
|
}
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
check,
|
|
193
|
+
out.phase = 'verify';
|
|
194
|
+
out.record = rel(saved);
|
|
195
|
+
const { from: was, ir: before } = JSON.parse(readFileSync(saved, 'utf8'));
|
|
196
|
+
const plan = chain(was, to) ?? [];
|
|
197
|
+
out.steps = plan.map((s) => ({ from: s.from, to: s.to, summary: s.summary }));
|
|
198
|
+
const loaded = await load(options.config, cwd);
|
|
199
|
+
const normalized = plan.reduce((ir, step) => step.normalize(ir), before);
|
|
200
|
+
out.ir = {
|
|
201
|
+
compared: true,
|
|
202
|
+
skipped: null,
|
|
203
|
+
differences: differences(normalized, loaded.build(false).ir),
|
|
280
204
|
};
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
const
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
205
|
+
if (!options.dryRun) {
|
|
206
|
+
const agents = ['.claude/skills/hozu', '.agents/skills/hozu'].filter((p) => existsSync(join(dir, p)));
|
|
207
|
+
if (agents.length) {
|
|
208
|
+
const written = await runSkill(dir, undefined);
|
|
209
|
+
out.guide = written.written.map((f) => rel(resolve(dir, f)));
|
|
210
|
+
for (const c of written.custom)
|
|
211
|
+
out.notes.push({
|
|
212
|
+
file: rel(resolve(dir, c.guide)),
|
|
213
|
+
line: 1,
|
|
214
|
+
message: `not a Hozu template: replace its Hozu section with this block\n${c.block}`,
|
|
215
|
+
see: null,
|
|
216
|
+
});
|
|
291
217
|
}
|
|
292
|
-
};
|
|
293
|
-
if (existsSync(dir))
|
|
294
|
-
visit(dir);
|
|
295
|
-
return out.join('\u0000');
|
|
296
|
-
}
|
|
297
|
-
function installedCore(config) {
|
|
298
|
-
let d = dirname(config);
|
|
299
|
-
for (;;) {
|
|
300
|
-
const pkg = join(d, 'node_modules/@hozu/core/package.json');
|
|
301
|
-
if (existsSync(pkg))
|
|
302
|
-
return JSON.parse(readFileSync(pkg, 'utf8')).version;
|
|
303
|
-
const up = dirname(d);
|
|
304
|
-
if (up === d)
|
|
305
|
-
return null;
|
|
306
|
-
d = up;
|
|
307
218
|
}
|
|
219
|
+
if (options.check !== false) {
|
|
220
|
+
out.check = await runCheck(loaded, cwd, false);
|
|
221
|
+
if (!out.check.ok)
|
|
222
|
+
out.next.push('npx hozu check # fix each diagnostic; accept intended lock changes with --update-lock');
|
|
223
|
+
}
|
|
224
|
+
out.ok = out.ir.differences.length === 0 && (out.check?.ok ?? true);
|
|
225
|
+
if (out.ir.differences.length)
|
|
226
|
+
out.next.push('Review each IR difference: it is a behaviour change the migration did not declare');
|
|
227
|
+
if (out.ok && !options.dryRun) {
|
|
228
|
+
rmSync(saved);
|
|
229
|
+
out.next.push('Done. Commit the migration; hozu.lock.json was not touched.');
|
|
230
|
+
}
|
|
231
|
+
return out;
|
|
308
232
|
}
|
|
309
|
-
|
|
310
|
-
const pkg = existsSync(join(dir, 'package.json'))
|
|
311
|
-
? JSON.parse(readFileSync(join(dir, 'package.json'), 'utf8'))
|
|
312
|
-
: {};
|
|
313
|
-
const names = [...Object.keys(pkg.dependencies ?? {}), ...Object.keys(pkg.devDependencies ?? {})]
|
|
314
|
-
.filter((n) => n.startsWith('@hozu/'))
|
|
315
|
-
.sort();
|
|
316
|
-
const tool = existsSync(join(dir, 'pnpm-lock.yaml'))
|
|
317
|
-
? 'pnpm add'
|
|
318
|
-
: existsSync(join(dir, 'yarn.lock'))
|
|
319
|
-
? 'yarn add'
|
|
320
|
-
: 'npm install';
|
|
321
|
-
return `${tool} ${names.map((n) => `${n}@^0.8.0`).join(' ')}`;
|
|
322
|
-
}
|
|
233
|
+
const typesOf = (t) => t.skipped ? 'skipped (npm install -D typescript)' : t.ok ? 'ok' : `${t.errors.length} errors`;
|
|
323
234
|
export function describeMigrate(r) {
|
|
324
235
|
const lines = [];
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
for (const n of behaviour)
|
|
338
|
-
lines.push(` ! ${n.file}:${n.line} ${n.message}${n.see ? ` (hozu docs ${n.see})` : ''}`);
|
|
236
|
+
const title = r.phase === 'rewrite' ? 'rewrite' : r.phase === 'verify' ? 'verify' : 'nothing to do';
|
|
237
|
+
lines.push(`hozu migrate ${r.from} → ${r.to}: ${title}${r.dryRun ? ' (dry run, nothing written)' : ''}`);
|
|
238
|
+
for (const s of r.steps)
|
|
239
|
+
lines.push(` ${s.from} → ${s.to}: ${s.summary}`);
|
|
240
|
+
if (r.phase === 'rewrite') {
|
|
241
|
+
lines.push(` rewrote ${r.changed.length} ${r.changed.length === 1 ? 'file' : 'files'}`);
|
|
242
|
+
for (const c of r.changed)
|
|
243
|
+
lines.push(` ${c.file} (${c.edits} ${c.edits === 1 ? 'edit' : 'edits'})`);
|
|
244
|
+
for (const p of r.packages)
|
|
245
|
+
lines.push(` ${p.name}: ${p.from} → ${p.to}`);
|
|
246
|
+
if (r.record)
|
|
247
|
+
lines.push(` ${r.dryRun ? 'would save' : 'saved'} the old IR to ${r.record}`);
|
|
339
248
|
}
|
|
340
|
-
|
|
341
|
-
lines.push(
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
let step = 3;
|
|
345
|
-
if (r.ir.skipped)
|
|
346
|
-
lines.push(`${step++}. IR comparison with 0.7: skipped (${r.ir.skipped})`);
|
|
249
|
+
if (r.notes.length)
|
|
250
|
+
lines.push(' by hand:');
|
|
251
|
+
for (const n of r.notes)
|
|
252
|
+
lines.push(` ${n.file}:${n.line} ${n.message}${n.see ? ` (hozu docs ${n.see})` : ''}`);
|
|
347
253
|
if (r.ir.compared)
|
|
348
254
|
lines.push(r.ir.differences.length
|
|
349
|
-
?
|
|
350
|
-
:
|
|
255
|
+
? ` the IR differs from ${r.from === r.to ? 'the saved one' : r.from} after the declared mapping, at ${r.ir.differences.length} ${r.ir.differences.length === 1 ? 'place' : 'places'}:\n${r.ir.differences.map((d) => ` ! ${d}`).join('\n')}`
|
|
256
|
+
: ' the IR equals the old one after the declared mapping');
|
|
257
|
+
for (const g of r.guide)
|
|
258
|
+
lines.push(` updated ${g}`);
|
|
351
259
|
if (r.check)
|
|
352
|
-
lines.push(
|
|
260
|
+
lines.push(` hozu check: ${r.check.ok ? 'ok' : 'failed'} · types ${typesOf(r.check.types)} · ${r.check.validate.summary.errors} errors, ${r.check.validate.summary.warnings} warnings`);
|
|
353
261
|
for (const n of r.next)
|
|
354
|
-
lines.push(
|
|
262
|
+
lines.push(`next: ${n}`);
|
|
355
263
|
lines.push('Migrate never writes hozu.lock.json and never deletes a contract.');
|
|
356
264
|
return `${lines.join('\n')}\n`;
|
|
357
265
|
}
|