wize-dev-kit 0.26.1 → 0.27.2
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/ARCH.md +2 -1
- package/CHANGELOG.md +64 -0
- package/README.es.md +28 -6
- package/README.md +28 -6
- package/README.pt-BR.md +28 -6
- package/adapters/continue/render.js +2 -3
- package/adapters/copilot/render.js +3 -3
- package/adapters/cursor/render.js +2 -3
- package/adapters/generic/render.js +2 -3
- package/adapters/opencode/render.js +3 -4
- package/adapters/windsurf/render.js +2 -3
- package/docs/WORKTREES.md +150 -0
- package/docs/WORKTREES.pt-BR.md +150 -0
- package/docs/harnesses/antigravity.md +2 -0
- package/docs/harnesses/antigravity.pt-BR.md +2 -0
- package/docs/harnesses/claude-code.md +2 -0
- package/docs/harnesses/claude-code.pt-BR.md +2 -0
- package/docs/harnesses/codex.md +3 -1
- package/docs/harnesses/codex.pt-BR.md +3 -1
- package/docs/harnesses/continue.md +2 -0
- package/docs/harnesses/continue.pt-BR.md +2 -0
- package/docs/harnesses/copilot.md +2 -0
- package/docs/harnesses/copilot.pt-BR.md +2 -0
- package/docs/harnesses/cursor.md +2 -0
- package/docs/harnesses/cursor.pt-BR.md +2 -0
- package/docs/harnesses/generic.md +2 -0
- package/docs/harnesses/generic.pt-BR.md +2 -0
- package/docs/harnesses/hermes.md +2 -0
- package/docs/harnesses/hermes.pt-BR.md +2 -0
- package/docs/harnesses/kimi-code.md +2 -0
- package/docs/harnesses/kimi-code.pt-BR.md +2 -0
- package/docs/harnesses/kiro.md +2 -0
- package/docs/harnesses/kiro.pt-BR.md +2 -0
- package/docs/harnesses/opencode.md +2 -0
- package/docs/harnesses/opencode.pt-BR.md +2 -0
- package/docs/harnesses/windsurf.md +2 -0
- package/docs/harnesses/windsurf.pt-BR.md +2 -0
- package/package.json +1 -1
- package/src/orchestrator-skills/module.yaml +1 -1
- package/src/orchestrator-skills/wize/skill.md +17 -7
- package/src/orchestrator-skills/wize-help/skill.md +20 -7
- package/src/orchestrator-skills/wize-update/skill.md +33 -27
- package/tools/installer/commands/doctor.js +186 -40
- package/tools/installer/commands/document-project.js +13 -3
- package/tools/installer/commands/hooks.js +215 -0
- package/tools/installer/commands/hydrate.js +298 -0
- package/tools/installer/commands/sync.js +50 -23
- package/tools/installer/commands/update.js +596 -120
- package/tools/installer/commands/version-check.js +69 -10
- package/tools/installer/document-project/modes/full-rescan.js +1 -1
- package/tools/installer/document-project/modes/initial-scan.js +5 -4
- package/tools/installer/document-project/modes/quick.js +11 -4
- package/tools/installer/git-context.js +219 -0
- package/tools/installer/git-ops.js +211 -0
- package/tools/installer/install-plan.js +136 -0
- package/tools/installer/marker.js +67 -0
- package/tools/installer/project-root.js +68 -0
- package/tools/installer/project-status.js +171 -0
- package/tools/installer/render-shared.js +170 -4
- package/tools/installer/render-targets.js +112 -0
- package/tools/installer/semver.js +59 -0
- package/tools/installer/setup-helpers.js +89 -31
- package/tools/installer/templates/hydrate.sh +163 -0
- package/tools/installer/templates/wize-kit-update.yml +38 -0
- package/tools/installer/toml.js +133 -0
- package/tools/installer/util.js +16 -0
- package/tools/installer/version-check.js +167 -37
- package/tools/installer/wize-cli.js +216 -78
- package/tools/installer/sync.js +0 -29
|
@@ -1,56 +1,39 @@
|
|
|
1
|
-
// `wize-dev-kit update` —
|
|
2
|
-
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
1
|
+
// `wize-dev-kit update` — converge a project to the kit binary that is running.
|
|
2
|
+
//
|
|
3
|
+
// The tracked changes an update needs are small: the `kit_version` pin in
|
|
4
|
+
// .wize/config/project.toml, the managed .gitignore block, the bootstrap block
|
|
5
|
+
// in a kit-generated AGENTS.md, the managed .wize/hooks/hydrate.sh when one is
|
|
6
|
+
// installed, and the removal of generated `wize` alias files from the index. In a git repo the default mode
|
|
7
|
+
// prepares them as one commit on `chore/wize-kit-<version>` inside a throwaway
|
|
8
|
+
// worktree, so the user's checkout, branch, index and dirty files are never
|
|
9
|
+
// touched and nothing is ever committed on the default branch. `--pr` pushes
|
|
10
|
+
// that branch and opens a pull request. `--in-place` (and any non-git
|
|
11
|
+
// directory) edits the working tree directly, re-renders the adapters and
|
|
12
|
+
// never commits.
|
|
13
|
+
//
|
|
14
|
+
// Never touched: user.toml, tea.toml, .wize/custom/, artifacts, knowledge.
|
|
5
15
|
|
|
6
16
|
'use strict';
|
|
7
17
|
|
|
8
18
|
const fs = require('node:fs');
|
|
19
|
+
const os = require('node:os');
|
|
9
20
|
const path = require('node:path');
|
|
10
|
-
const {
|
|
21
|
+
const { ALIAS_PATHS, computeGitignore } = require('../setup-helpers.js');
|
|
22
|
+
const { readToml, parseToml, loadProjectConfig, rewriteTomlValue } = require('../toml.js');
|
|
23
|
+
const { compareVersions } = require('../semver.js');
|
|
24
|
+
const { renderTargets, outputSpecs } = require('../render-targets.js');
|
|
25
|
+
const { writeMarker } = require('../marker.js');
|
|
26
|
+
const { kitVersionOf } = require('../project-status.js');
|
|
27
|
+
const {
|
|
28
|
+
gitContext, defaultBranch, hasRemote, worktreeList, statusPorcelain, dirtyTrackedSet, localBranchExists, refExists
|
|
29
|
+
} = require('../git-context.js');
|
|
30
|
+
const ops = require('../git-ops.js');
|
|
31
|
+
const { isKitAgentsMd, upsertBootstrapBlock } = require('../render-shared.js');
|
|
32
|
+
const { refreshedHook, HOOK_REL } = require('./hooks.js');
|
|
11
33
|
|
|
12
|
-
|
|
13
|
-
// `key = "value"` scalars, and `key = ["a", "b"]` string arrays.
|
|
14
|
-
function readToml(file) {
|
|
15
|
-
if (!fs.existsSync(file)) return {};
|
|
16
|
-
const content = fs.readFileSync(file, 'utf-8');
|
|
17
|
-
const out = {};
|
|
18
|
-
let section = null;
|
|
19
|
-
for (const raw of content.split('\n')) {
|
|
20
|
-
const line = raw.replace(/#.*$/, '').trim();
|
|
21
|
-
if (!line) continue;
|
|
22
|
-
const head = line.match(/^\[([^\]]+)\]$/);
|
|
23
|
-
if (head) { section = head[1]; out[section] = out[section] || {}; continue; }
|
|
24
|
-
const kv = line.match(/^([a-zA-Z_][\w-]*)\s*=\s*(.+)$/);
|
|
25
|
-
if (!kv) continue;
|
|
26
|
-
const [, key, valRaw] = kv;
|
|
27
|
-
const v = parseTomlValue(valRaw.trim());
|
|
28
|
-
if (section) out[section][key] = v;
|
|
29
|
-
else out[key] = v;
|
|
30
|
-
}
|
|
31
|
-
return out;
|
|
32
|
-
}
|
|
34
|
+
const PROJECT_TOML = '.wize/config/project.toml';
|
|
33
35
|
|
|
34
|
-
|
|
35
|
-
if (raw.startsWith('"') && raw.endsWith('"')) return raw.slice(1, -1);
|
|
36
|
-
if (raw.startsWith("'") && raw.endsWith("'")) return raw.slice(1, -1);
|
|
37
|
-
if (raw.startsWith('[') && raw.endsWith(']')) {
|
|
38
|
-
return raw.slice(1, -1).split(',').map(s => s.trim().replace(/^["']|["']$/g, '')).filter(Boolean);
|
|
39
|
-
}
|
|
40
|
-
if (raw === 'true') return true;
|
|
41
|
-
if (raw === 'false') return false;
|
|
42
|
-
if (!isNaN(Number(raw))) return Number(raw);
|
|
43
|
-
return raw;
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
function loadProjectConfig(projectRoot) {
|
|
47
|
-
return readToml(path.join(projectRoot, '.wize/config/project.toml'));
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
function loadInstalledKitVersion(kitRoot) {
|
|
51
|
-
try { return require(path.join(kitRoot, 'package.json')).version; }
|
|
52
|
-
catch { return null; }
|
|
53
|
-
}
|
|
36
|
+
const loadInstalledKitVersion = kitVersionOf;
|
|
54
37
|
|
|
55
38
|
// Returns a list of CHANGELOG entries (markdown blocks) for versions strictly
|
|
56
39
|
// between `from` and `to`. Best-effort — if CHANGELOG layout is unusual,
|
|
@@ -69,104 +52,597 @@ function changelogBetween(kitRoot, fromVersion, toVersion) {
|
|
|
69
52
|
matches[i].end = i + 1 < matches.length ? matches[i + 1].start : content.length;
|
|
70
53
|
}
|
|
71
54
|
return matches
|
|
72
|
-
.filter(x =>
|
|
55
|
+
.filter(x => compareVersions(x.v, fromVersion) > 0 && compareVersions(x.v, toVersion) <= 0)
|
|
73
56
|
.map(x => content.slice(x.start, x.end).trim());
|
|
74
57
|
}
|
|
75
58
|
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
59
|
+
const excerpt = (entry) => entry.split('\n').slice(0, 4).join('\n');
|
|
60
|
+
|
|
61
|
+
// ---- plan ------------------------------------------------------------------
|
|
62
|
+
|
|
63
|
+
// A plan source answers two questions about one tree: what does a file say,
|
|
64
|
+
// and which files under these paths are tracked. Paths are relative to the
|
|
65
|
+
// project root.
|
|
66
|
+
function fsSource(root) {
|
|
67
|
+
return {
|
|
68
|
+
read(rel) {
|
|
69
|
+
try {
|
|
70
|
+
const file = path.join(root, rel);
|
|
71
|
+
if (!fs.lstatSync(file).isFile()) return null;
|
|
72
|
+
return fs.readFileSync(file, 'utf-8');
|
|
73
|
+
} catch { return null; }
|
|
74
|
+
},
|
|
75
|
+
special(rel) {
|
|
76
|
+
try { const st = fs.lstatSync(path.join(root, rel)); return !st.isFile(); } catch { return false; }
|
|
77
|
+
},
|
|
78
|
+
tracked: (paths) => ops.lsFiles(root, paths)
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function refSource(root, ref) {
|
|
83
|
+
return {
|
|
84
|
+
read: (rel) => ops.showFile(root, ref, rel),
|
|
85
|
+
special: (rel) => ops.isSpecialAt(root, ref, rel),
|
|
86
|
+
tracked: (paths) => ops.lsTree(root, ref, paths)
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// The tracked changes needed to converge a tree to kit version `to`. Pure:
|
|
91
|
+
// reads through `source` (a directory or a fs/ref source) and writes nothing.
|
|
92
|
+
// Returns { ok, from, to, files: [{ path, content, mode }], untrack, converged },
|
|
93
|
+
// or { ok: false, error: 'no-project' | 'toml-write' }.
|
|
94
|
+
//
|
|
95
|
+
// `untrack` is limited to tracked files under the exact ALIAS_PATHS: the
|
|
96
|
+
// managed block ignores those by construction, and other tracked-but-ignored
|
|
97
|
+
// files (repos that deliberately commit wize-* outputs) stay as they are.
|
|
98
|
+
function planTrackedChanges(treeRootOrSource, to, { refreshGitignore = true } = {}) {
|
|
99
|
+
const source = typeof treeRootOrSource === 'string' ? fsSource(treeRootOrSource) : treeRootOrSource;
|
|
100
|
+
const toml = source.read(PROJECT_TOML);
|
|
101
|
+
const cfg = toml === null ? {} : parseToml(toml);
|
|
102
|
+
if (!cfg.project) return { ok: false, error: 'no-project' };
|
|
103
|
+
const from = cfg.project.kit_version || '0.0.0';
|
|
104
|
+
|
|
105
|
+
const files = [];
|
|
106
|
+
const nextToml = rewriteTomlValue(toml, 'project', 'kit_version', to);
|
|
107
|
+
if (nextToml === null) return { ok: false, error: 'toml-write', from };
|
|
108
|
+
if (nextToml !== toml) files.push({ path: PROJECT_TOML, content: nextToml, mode: 'pin' });
|
|
109
|
+
|
|
110
|
+
let untrack = [];
|
|
111
|
+
if (refreshGitignore) {
|
|
112
|
+
// A symlinked .gitignore is the user's own arrangement: left alone.
|
|
113
|
+
const gi = source.special('.gitignore') ? { changed: false } : computeGitignore(source.read('.gitignore'));
|
|
114
|
+
if (gi.changed) files.push({ path: '.gitignore', content: gi.next, mode: gi.mode });
|
|
115
|
+
untrack = source.tracked(ALIAS_PATHS);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// Only an AGENTS.md this kit generated gets the block; a foreign one is never edited.
|
|
119
|
+
const agents = source.read('AGENTS.md');
|
|
120
|
+
if (isKitAgentsMd(agents)) {
|
|
121
|
+
const next = upsertBootstrapBlock(agents);
|
|
122
|
+
if (next !== agents) files.push({ path: 'AGENTS.md', content: next, mode: 'bootstrap block' });
|
|
81
123
|
}
|
|
82
|
-
|
|
124
|
+
const hook = source.read(HOOK_REL);
|
|
125
|
+
const hookNext = hook === null ? null : refreshedHook(hook);
|
|
126
|
+
if (hookNext !== null) files.push({ path: HOOK_REL, content: hookNext, mode: 'managed hook', exec: true });
|
|
127
|
+
return { ok: true, from, to, files, untrack, converged: files.length === 0 && untrack.length === 0 };
|
|
83
128
|
}
|
|
84
129
|
|
|
85
|
-
//
|
|
86
|
-
//
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
delete require.cache[require.resolve(renderPath)]; // make `update` idempotent in the same process
|
|
97
|
-
try {
|
|
98
|
-
const mod = require(renderPath);
|
|
99
|
-
const out = mod.render(kitRoot, projectRoot, { profiles });
|
|
100
|
-
const n = out && Array.isArray(out.written) ? out.written.length : 0;
|
|
101
|
-
results.push({ code, written: n });
|
|
102
|
-
log(` ✓ ${code}: ${n} file(s) refreshed`);
|
|
103
|
-
} catch (err) {
|
|
104
|
-
results.push({ code, error: err.message });
|
|
105
|
-
log(` ✖ ${code}: ${err.message}`);
|
|
106
|
-
}
|
|
130
|
+
// Writes the plan into `treeRoot`. Refuses (returns { error }) before writing
|
|
131
|
+
// anything when a target, or a directory above it, is a symlink: a write
|
|
132
|
+
// would land outside the tree. A throwing caller never sees partial writes.
|
|
133
|
+
function unsafeTarget(treeRoot, rel) {
|
|
134
|
+
const parts = rel.split('/');
|
|
135
|
+
let cur = treeRoot;
|
|
136
|
+
for (const part of parts) {
|
|
137
|
+
cur = path.join(cur, part);
|
|
138
|
+
let st;
|
|
139
|
+
try { st = fs.lstatSync(cur); } catch { return false; }
|
|
140
|
+
if (st.isSymbolicLink()) return true;
|
|
107
141
|
}
|
|
108
|
-
return
|
|
142
|
+
return false;
|
|
109
143
|
}
|
|
110
144
|
|
|
111
|
-
function
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
const
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
return
|
|
145
|
+
function writePlan(treeRoot, plan) {
|
|
146
|
+
const bad = plan.files.filter(f => unsafeTarget(treeRoot, f.path)).map(f => f.path);
|
|
147
|
+
if (bad.length) return { error: `refusing to write through a symlink: ${bad.join(', ')}; replace it with a regular file` };
|
|
148
|
+
for (const f of plan.files) {
|
|
149
|
+
const file = path.join(treeRoot, f.path);
|
|
150
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
151
|
+
fs.writeFileSync(file, f.content, 'utf-8');
|
|
152
|
+
if (f.exec) fs.chmodSync(file, 0o755);
|
|
153
|
+
}
|
|
154
|
+
return {};
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// ---- options and output ------------------------------------------------------
|
|
158
|
+
|
|
159
|
+
function readOpts(opts) {
|
|
160
|
+
const flag = (kebab, camel) => !!(opts[kebab] ?? opts[camel]);
|
|
161
|
+
return {
|
|
162
|
+
pr: !!opts.pr,
|
|
163
|
+
inPlace: flag('in-place', 'inPlace'),
|
|
164
|
+
check: !!opts.check,
|
|
165
|
+
dryRun: flag('dry-run', 'dryRun'),
|
|
166
|
+
json: !!opts.json,
|
|
167
|
+
base: opts.base,
|
|
168
|
+
allowDowngrade: flag('allow-downgrade', 'allowDowngrade'),
|
|
169
|
+
noRender: flag('no-render', 'noRender'),
|
|
170
|
+
refreshGitignore: opts.refreshGitignore !== false
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function printChangelog(log, kitRoot, entries) {
|
|
175
|
+
if (!entries.length) return;
|
|
176
|
+
log('\nWhat changed (excerpt from CHANGELOG):');
|
|
177
|
+
for (const e of entries) {
|
|
178
|
+
log(excerpt(e).split('\n').map(l => ' ' + l).join('\n'));
|
|
179
|
+
log(' …');
|
|
180
|
+
}
|
|
181
|
+
log(`\nFull details: ${path.join(kitRoot, 'CHANGELOG.md')}`);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// ---- base resolution (isolated mode) -----------------------------------------
|
|
185
|
+
|
|
186
|
+
// { base, def, warning } or { error }. With `fetch` false (check/dry-run) the
|
|
187
|
+
// remote is not contacted and origin/<default> is used if it exists.
|
|
188
|
+
function resolveBase(projectRoot, baseFlag, { fetch }) {
|
|
189
|
+
if (baseFlag !== undefined && baseFlag !== null) {
|
|
190
|
+
if (typeof baseFlag !== 'string' || !baseFlag.trim()) return { error: '--base needs a ref' };
|
|
191
|
+
const base = baseFlag.trim();
|
|
192
|
+
if (!refExists(projectRoot, base)) return { error: `base ${base} not found` };
|
|
193
|
+
return { base, def: defaultBranch(projectRoot) || base.replace(/^origin\//, ''), warning: null };
|
|
194
|
+
}
|
|
195
|
+
const def = defaultBranch(projectRoot);
|
|
196
|
+
if (!def) return { error: 'cannot determine default branch; pass --base <ref> or --in-place' };
|
|
197
|
+
const remoteRef = `origin/${def}`;
|
|
198
|
+
if (!fetch) {
|
|
199
|
+
if (refExists(projectRoot, remoteRef)) return { base: remoteRef, def, warning: null };
|
|
200
|
+
if (refExists(projectRoot, def)) return { base: def, def, warning: null };
|
|
201
|
+
return { error: `cannot find ${def}; pass --base <ref> or --in-place` };
|
|
202
|
+
}
|
|
203
|
+
const fetched = hasRemote(projectRoot) && ops.fetch(projectRoot, 'origin', def).ok;
|
|
204
|
+
if (fetched && refExists(projectRoot, remoteRef)) return { base: remoteRef, def, warning: null };
|
|
205
|
+
if (refExists(projectRoot, def)) return { base: def, def, warning: `using local ${def}; fetch failed` };
|
|
206
|
+
if (refExists(projectRoot, remoteRef)) return { base: remoteRef, def, warning: `using ${remoteRef}; fetch failed` };
|
|
207
|
+
return { error: `cannot find ${def}; pass --base <ref> or --in-place` };
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// ---- temp worktree -------------------------------------------------------------
|
|
211
|
+
|
|
212
|
+
// Runs fn(treeRoot) inside a detached worktree of `ref`, removing it on every
|
|
213
|
+
// exit path including SIGINT/SIGTERM.
|
|
214
|
+
function withTempWorktree(projectRoot, toplevel, ref, fn) {
|
|
215
|
+
const rel = path.relative(toplevel, projectRoot);
|
|
216
|
+
const tmp = fs.realpathSync(fs.mkdtempSync(path.join(os.tmpdir(), 'wize-update-')));
|
|
217
|
+
let cleaned = false;
|
|
218
|
+
const cleanup = () => {
|
|
219
|
+
if (cleaned) return;
|
|
220
|
+
cleaned = true;
|
|
221
|
+
ops.worktreeRemove(projectRoot, tmp);
|
|
222
|
+
ops.rmDir(tmp);
|
|
223
|
+
ops.worktreeForget(projectRoot, tmp);
|
|
224
|
+
};
|
|
225
|
+
const onInt = () => { cleanup(); process.exit(130); };
|
|
226
|
+
const onTerm = () => { cleanup(); process.exit(143); };
|
|
227
|
+
process.once('SIGINT', onInt);
|
|
228
|
+
process.once('SIGTERM', onTerm);
|
|
229
|
+
try {
|
|
230
|
+
const added = ops.worktreeAdd(projectRoot, tmp, ref);
|
|
231
|
+
if (!added.ok) return { error: `could not create a temp worktree: ${added.err}` };
|
|
232
|
+
return fn(path.join(tmp, rel), tmp);
|
|
233
|
+
} finally {
|
|
234
|
+
process.removeListener('SIGINT', onInt);
|
|
235
|
+
process.removeListener('SIGTERM', onTerm);
|
|
236
|
+
cleanup();
|
|
237
|
+
}
|
|
121
238
|
}
|
|
122
239
|
|
|
123
|
-
|
|
124
|
-
|
|
240
|
+
// 'converge' | 'downgrade' | 'bump': one verb for the commit and the PR.
|
|
241
|
+
function updateVerb(from, to) {
|
|
242
|
+
if (from === to) return 'converge';
|
|
243
|
+
return compareVersions(to, from) < 0 ? 'downgrade' : 'bump';
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
function updateSubject(from, to) {
|
|
247
|
+
const verb = updateVerb(from, to);
|
|
248
|
+
return verb === 'converge' ? `chore(wize): converge kit ${to}` : `chore(wize): ${verb} kit ${from} -> ${to}`;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
function commitMessage(from, to, plan) {
|
|
252
|
+
const subject = updateSubject(from, to);
|
|
253
|
+
const lines = [`Converge this project to wize-dev-kit ${to}.`, ''];
|
|
254
|
+
if (plan.files.length) {
|
|
255
|
+
lines.push('Updated:');
|
|
256
|
+
for (const f of plan.files) lines.push(`- ${f.path}`);
|
|
257
|
+
}
|
|
258
|
+
if (plan.untrack.length) {
|
|
259
|
+
if (plan.files.length) lines.push('');
|
|
260
|
+
lines.push('Untracked (generated, now git-ignored; still on disk and rebuilt by hydrate):');
|
|
261
|
+
for (const p of plan.untrack) lines.push(`- ${p}`);
|
|
262
|
+
}
|
|
263
|
+
return { subject, body: lines.join('\n') };
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
// ---- command ---------------------------------------------------------------------
|
|
267
|
+
|
|
268
|
+
async function cmdUpdate({ kitRoot, projectRoot: rawRoot, opts = {} } = {}) {
|
|
269
|
+
const o = readOpts(opts);
|
|
270
|
+
const log = o.json ? () => {} : (opts.log || console.log);
|
|
271
|
+
const errlog = opts.errlog || console.error;
|
|
272
|
+
const emit = (r) => { if (o.json) (opts.write || console.log)(JSON.stringify(r)); return r; };
|
|
273
|
+
|
|
274
|
+
let projectRoot;
|
|
275
|
+
try { projectRoot = fs.realpathSync(rawRoot); } catch { projectRoot = path.resolve(rawRoot); }
|
|
276
|
+
|
|
277
|
+
const to = loadInstalledKitVersion(kitRoot);
|
|
278
|
+
let from = null;
|
|
279
|
+
let mode = null;
|
|
280
|
+
let base = null;
|
|
281
|
+
let branch = null;
|
|
282
|
+
const result = (extra) => ({
|
|
283
|
+
changed: false, from, to, mode, base, branch, branch_exists: false, pr_url: null, adapters: [], converged: false, exitCode: 0, ...extra
|
|
284
|
+
});
|
|
285
|
+
const fail = (message, extra = {}) => {
|
|
286
|
+
errlog(`✖ ${message}`);
|
|
287
|
+
return emit(result({ exitCode: 1, error: message, ...extra }));
|
|
288
|
+
};
|
|
289
|
+
|
|
290
|
+
if (!to) return fail(`could not read the kit version from ${path.join(kitRoot, 'package.json')}`);
|
|
125
291
|
const cfg = loadProjectConfig(projectRoot);
|
|
126
|
-
if (!cfg.project)
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
292
|
+
if (!cfg.project) return fail('no .wize/config/project.toml here; run install first (npx wize-dev-kit install)');
|
|
293
|
+
if (o.pr && o.inPlace) return fail('--pr and --in-place cannot be combined');
|
|
294
|
+
|
|
295
|
+
const ctx = gitContext(projectRoot);
|
|
296
|
+
// An unborn HEAD (git init, nothing committed) has no base to branch from;
|
|
297
|
+
// there is no history to protect, so update edits the checkout directly.
|
|
298
|
+
const unborn = ctx.isRepo && !!ctx.branch && !ctx.head;
|
|
299
|
+
mode = !ctx.isRepo || o.inPlace || (unborn && !o.pr) ? 'in-place' : 'isolated';
|
|
300
|
+
if (o.pr && !ctx.isRepo) return fail('--pr needs a git repository');
|
|
301
|
+
const planOpts = { refreshGitignore: o.refreshGitignore };
|
|
302
|
+
const readOnly = o.check || o.dryRun;
|
|
303
|
+
|
|
304
|
+
if (mode === 'in-place') {
|
|
305
|
+
const plan = planTrackedChanges(fsSource(projectRoot), to, planOpts);
|
|
306
|
+
if (!plan.ok) return fail(planError(plan, null));
|
|
307
|
+
from = plan.from;
|
|
308
|
+
const pinAhead = compareVersions(to, from) < 0 && !o.allowDowngrade;
|
|
309
|
+
const changelog = changelogBetween(kitRoot, from, to);
|
|
310
|
+
if (readOnly) return reportCheck({ o, plan, mode, base: null, branch: null, branchExists: false, pinAhead, changelog, kitRoot, log, emit, result });
|
|
311
|
+
if (pinAhead) return fail(downgradeMessage(from, to));
|
|
312
|
+
return applyInPlace({ kitRoot, projectRoot, ctx, cfg, plan, o, to, from, changelog, log, fail, emit, result });
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
// isolated
|
|
316
|
+
const resolved = resolveBase(projectRoot, o.base, { fetch: !readOnly });
|
|
317
|
+
if (resolved.error) return fail(resolved.error);
|
|
318
|
+
base = resolved.base;
|
|
319
|
+
const def = resolved.def;
|
|
320
|
+
if (resolved.warning) log(`warning: ${resolved.warning}`);
|
|
321
|
+
|
|
322
|
+
const basePlan = planTrackedChanges(refSource(projectRoot, base), to, planOpts);
|
|
323
|
+
if (!basePlan.ok) return fail(planError(basePlan, base));
|
|
324
|
+
from = basePlan.from;
|
|
325
|
+
branch = `chore/wize-kit-${to}`;
|
|
326
|
+
const branchExists = localBranchExists(projectRoot, branch);
|
|
327
|
+
const pinAhead = compareVersions(to, from) < 0 && !o.allowDowngrade;
|
|
328
|
+
const changelog = changelogBetween(kitRoot, from, to);
|
|
329
|
+
|
|
330
|
+
if (readOnly) {
|
|
331
|
+
return reportCheck({ o, plan: basePlan, mode, base, branch, branchExists, pinAhead, changelog, kitRoot, log, emit, result });
|
|
332
|
+
}
|
|
333
|
+
if (pinAhead) return fail(downgradeMessage(from, to));
|
|
334
|
+
const extra = { branch_exists: branchExists };
|
|
335
|
+
|
|
336
|
+
log(`Wize Dev Kit update: ${from} → ${to} (isolated, base ${base})`);
|
|
337
|
+
const localPin = cfg.project.kit_version;
|
|
338
|
+
if (localPin && localPin !== from) {
|
|
339
|
+
if (compareVersions(from, localPin) > 0) log(`note: this checkout pins ${localPin}, ${base} pins ${from}; merge ${base} to pick that up`);
|
|
340
|
+
else log(`note: this checkout pins ${localPin}, ${base} pins ${from}; ${base} is behind this checkout, and the update branch builds on ${base}`);
|
|
341
|
+
}
|
|
342
|
+
if (basePlan.converged) {
|
|
343
|
+
log(`already converged at ${to}`);
|
|
344
|
+
return emit(result({ ...extra, converged: true }));
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
let plan = basePlan;
|
|
348
|
+
let startRef = base;
|
|
349
|
+
let prepared = false;
|
|
350
|
+
let preparedAt = null;
|
|
351
|
+
if (branchExists) {
|
|
352
|
+
const branchPlan = planTrackedChanges(refSource(projectRoot, branch), to, planOpts);
|
|
353
|
+
if (!branchPlan.ok) return fail(`${branch} has no usable project.toml; rename or delete that branch`, extra);
|
|
354
|
+
const wt = (worktreeList(projectRoot) || []).find(w => w.branch === branch);
|
|
355
|
+
if (branchPlan.converged) {
|
|
356
|
+
prepared = true;
|
|
357
|
+
preparedAt = wt ? wt.path : null;
|
|
358
|
+
} else if (wt) {
|
|
359
|
+
return fail(`${branch} is checked out at ${wt.path}; finish it there`, extra);
|
|
360
|
+
} else {
|
|
361
|
+
plan = branchPlan;
|
|
362
|
+
startRef = branch;
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
// A fresh CI checkout, or a second developer, has no local branch while the
|
|
367
|
+
// remote one already carries this update (an open PR, or a closed one whose
|
|
368
|
+
// branch was kept). Building another commit would only be rejected at push.
|
|
369
|
+
const remoteConverged = () => {
|
|
370
|
+
if (!hasRemote(projectRoot) || !ops.fetch(projectRoot, 'origin', branch).ok) return false;
|
|
371
|
+
const remotePlan = planTrackedChanges(refSource(projectRoot, 'FETCH_HEAD'), to, planOpts);
|
|
372
|
+
return remotePlan.ok && remotePlan.converged;
|
|
373
|
+
};
|
|
374
|
+
let remoteOnly = false;
|
|
375
|
+
if (o.pr && !prepared && !branchExists && remoteConverged()) {
|
|
376
|
+
prepared = true;
|
|
377
|
+
remoteOnly = true;
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
let commit = null;
|
|
381
|
+
if (remoteOnly) {
|
|
382
|
+
log(`already prepared on origin/${branch}`);
|
|
383
|
+
} else if (prepared) {
|
|
384
|
+
log(`already prepared on ${branch}${preparedAt ? ` (${preparedAt})` : ''}`);
|
|
385
|
+
} else {
|
|
386
|
+
const made = withTempWorktree(projectRoot, ctx.toplevel, startRef, (treeRoot, tmp) => {
|
|
387
|
+
const wrote = writePlan(treeRoot, plan);
|
|
388
|
+
if (wrote.error) return { error: wrote.error };
|
|
389
|
+
const staged = plan.files.map(f => f.path);
|
|
390
|
+
if (staged.length) {
|
|
391
|
+
const a = ops.add(treeRoot, staged);
|
|
392
|
+
if (!a.ok) return { error: `git add failed: ${a.err}` };
|
|
393
|
+
}
|
|
394
|
+
if (plan.untrack.length) {
|
|
395
|
+
const r = ops.rmCached(treeRoot, plan.untrack);
|
|
396
|
+
if (!r.ok) return { error: `git rm --cached failed: ${r.err}` };
|
|
397
|
+
}
|
|
398
|
+
const parent = ops.headSha(tmp);
|
|
399
|
+
const { subject, body } = commitMessage(from, to, plan);
|
|
400
|
+
let c;
|
|
401
|
+
try { c = ops.commit(treeRoot, subject, body); }
|
|
402
|
+
catch (err) { c = { ok: false, err: err.message }; }
|
|
403
|
+
if (!c.ok) return { error: `${c.err}\nthe commit hook rejected the update; nothing was created. Fix the hook, or run \`update --in-place\` on a feature branch yourself.` };
|
|
404
|
+
const sha = ops.headSha(tmp);
|
|
405
|
+
// A project hook may have staged more than the plan (git add -u / -A).
|
|
406
|
+
// The branch only ever carries the planned files.
|
|
407
|
+
const prefix = path.relative(tmp, treeRoot).split(path.sep).filter(Boolean).join('/');
|
|
408
|
+
const allowed = new Set([...plan.files.map(f => f.path), ...plan.untrack].map(p => (prefix ? `${prefix}/${p}` : p)));
|
|
409
|
+
const changed = ops.diffNames(tmp, parent, sha);
|
|
410
|
+
if (!changed) return { error: 'could not verify the update commit; nothing was created' };
|
|
411
|
+
const stray = changed.filter(p => !allowed.has(p));
|
|
412
|
+
if (stray.length) {
|
|
413
|
+
const shown = stray.slice(0, 5).join(', ') + (stray.length > 5 ? `, and ${stray.length - 5} more` : '');
|
|
414
|
+
return { error: `a commit hook changed files outside the update (${shown}); nothing was created. Fix the hook, or run \`update --in-place\` on a feature branch yourself.` };
|
|
415
|
+
}
|
|
416
|
+
const ref = branchExists ? ops.updateRef(projectRoot, branch, sha, parent) : ops.branchCreate(projectRoot, branch, sha);
|
|
417
|
+
if (!ref.ok) return { error: `could not move ${branch} to the new commit: ${ref.err}` };
|
|
418
|
+
return { sha };
|
|
419
|
+
});
|
|
420
|
+
if (made.error) return fail(made.error, extra);
|
|
421
|
+
commit = made.sha;
|
|
422
|
+
log(`✓ prepared ${branch} (${commit.slice(0, 7)}): ${plan.files.length} file(s) updated, ${plan.untrack.length} untracked`);
|
|
423
|
+
for (const f of plan.files) log(` ${f.path}`);
|
|
424
|
+
for (const p of plan.untrack) log(` untracked: ${p}`);
|
|
425
|
+
log(` review: git diff ${base}...${branch}`);
|
|
426
|
+
if (!o.pr) log(' open a PR: npx wize-dev-kit update --pr');
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
let prUrl = null;
|
|
430
|
+
if (o.pr) {
|
|
431
|
+
const pr = openPr({ projectRoot, branch, def, from, to, entries: changelog, log, skipPush: remoteOnly, remoteConverged });
|
|
432
|
+
if (pr.error) return fail(pr.error, { ...extra, branch_exists: true, commit });
|
|
433
|
+
prUrl = pr.url;
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
if (!prepared || o.pr) {
|
|
437
|
+
printChangelog(log, kitRoot, changelog);
|
|
438
|
+
log('\nRestart your harness after the change reaches this checkout.');
|
|
439
|
+
}
|
|
440
|
+
return emit(result({ ...extra, branch_exists: true, changed: !prepared, commit, pr_url: prUrl }));
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
function planError(plan, base) {
|
|
444
|
+
if (plan.error === 'toml-write') return 'could not write kit_version in project.toml';
|
|
445
|
+
return base
|
|
446
|
+
? `.wize/config/project.toml not found on ${base}; merge the kit install first or pass --base <ref>`
|
|
447
|
+
: 'no .wize/config/project.toml here; run install first (npx wize-dev-kit install)';
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
function downgradeMessage(from, to) {
|
|
451
|
+
return `project pinned to ${from}, this kit is ${to}; run: npx wize-dev-kit@latest update, or pass --allow-downgrade if that pin is not published`;
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
// ---- --check / --dry-run ----------------------------------------------------------
|
|
455
|
+
|
|
456
|
+
function reportCheck({ o, plan, mode, base, branch, branchExists, pinAhead, changelog, kitRoot, log, emit, result }) {
|
|
457
|
+
const report = {
|
|
458
|
+
from: plan.from,
|
|
459
|
+
to: plan.to,
|
|
460
|
+
mode,
|
|
461
|
+
base,
|
|
462
|
+
branch,
|
|
463
|
+
branch_exists: branchExists,
|
|
464
|
+
files: plan.files.map(f => f.path),
|
|
465
|
+
untrack: plan.untrack,
|
|
466
|
+
changelog: changelog.map(excerpt),
|
|
467
|
+
converged: plan.converged
|
|
468
|
+
};
|
|
469
|
+
const exitCode = o.dryRun ? 0 : pinAhead ? 12 : plan.converged ? 0 : 11;
|
|
470
|
+
if (o.json) {
|
|
471
|
+
emit(report);
|
|
472
|
+
return result({ ...report, exitCode, converged: plan.converged });
|
|
130
473
|
}
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
474
|
+
log(`Wize Dev Kit update --${o.dryRun ? 'dry-run' : 'check'}: ${plan.from} → ${plan.to} (${mode}${base ? `, base ${base}` : ''})`);
|
|
475
|
+
if (pinAhead) {
|
|
476
|
+
log(downgradeMessage(plan.from, plan.to));
|
|
477
|
+
} else if (plan.converged) {
|
|
478
|
+
log(`already converged at ${plan.to}`);
|
|
479
|
+
} else {
|
|
480
|
+
log('pending changes:');
|
|
481
|
+
for (const f of plan.files) log(` ${f.path}`);
|
|
482
|
+
for (const p of plan.untrack) log(` untrack: ${p}`);
|
|
483
|
+
if (branch) log(` branch: ${branch}${branchExists ? ' (exists)' : ''}`);
|
|
136
484
|
}
|
|
485
|
+
if (!plan.converged && !pinAhead) printChangelog(log, kitRoot, changelog);
|
|
486
|
+
return result({ ...report, exitCode, converged: plan.converged });
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
// ---- in-place -------------------------------------------------------------------
|
|
490
|
+
|
|
491
|
+
function applyInPlace({ kitRoot, projectRoot, ctx, cfg, plan, o, to, from, changelog, log, fail, emit, result }) {
|
|
492
|
+
const gitFailed = (what, bypass = false) => fail(`git could not report the status of ${what} (git failed, timed out or reports a dubious ownership); cannot verify that nothing uncommitted would be overwritten. Fix git (for example safe.directory) and retry${bypass ? ', or rerun with --no-render' : ''}`);
|
|
493
|
+
|
|
494
|
+
// Unborn HEAD: nothing is tracked yet, so there is no committed state to
|
|
495
|
+
// protect and every file shows as untracked; edit like a non-git project.
|
|
496
|
+
const guarded = ctx.isRepo && !(ctx.branch && !ctx.head);
|
|
137
497
|
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
498
|
+
if (guarded && plan.files.length) {
|
|
499
|
+
const dirty = statusPorcelain(projectRoot, plan.files.map(f => f.path));
|
|
500
|
+
if (dirty === null) return gitFailed('the files update must edit');
|
|
501
|
+
if (dirty.length) {
|
|
502
|
+
return fail(`uncommitted changes in files update must edit: ${dirty.map(l => l.slice(3)).join(', ')}; commit or stash them, or rerun without --in-place`);
|
|
503
|
+
}
|
|
141
504
|
}
|
|
142
505
|
|
|
143
|
-
|
|
144
|
-
|
|
506
|
+
if (guarded && !o.noRender) {
|
|
507
|
+
const targets = ((cfg.install && cfg.install.ide_targets) || ['claude-code', 'generic']).map(t => (typeof t === 'string' ? t : t && t.code)).filter(Boolean);
|
|
508
|
+
const specs = [...new Set(targets.flatMap(outputSpecs))];
|
|
509
|
+
const dirtyOut = specs.length ? statusPorcelain(projectRoot, specs) : [];
|
|
510
|
+
if (dirtyOut === null) return gitFailed('the generated files', true);
|
|
511
|
+
const tracked = dirtyOut.filter(l => !l.startsWith('??') && !l.startsWith('!!'));
|
|
512
|
+
if (tracked.length) {
|
|
513
|
+
return fail(`uncommitted changes in tracked generated files the re-render would overwrite: ${tracked.map(l => l.slice(3)).join(', ')}; commit or stash them, or rerun with --no-render`);
|
|
514
|
+
}
|
|
515
|
+
// Git pathspecs stop at a symlinked output directory, so also ask the
|
|
516
|
+
// renderer, in probe mode, which absolute landing paths it would write and
|
|
517
|
+
// veto the ones that are tracked with uncommitted changes.
|
|
518
|
+
const dirtyAbs = dirtyTrackedSet(projectRoot);
|
|
519
|
+
if (dirtyAbs === null) return gitFailed('the tracked files', true);
|
|
520
|
+
if (dirtyAbs.size) {
|
|
521
|
+
const probe = renderTargets({
|
|
522
|
+
kitRoot, projectRoot, targets: (cfg.install && cfg.install.ide_targets) || ['claude-code', 'generic'],
|
|
523
|
+
profiles: (cfg.install && cfg.install.profiles) || ['core'],
|
|
524
|
+
guard: (abs) => dirtyAbs.has(abs), probe: true
|
|
525
|
+
});
|
|
526
|
+
const vetoed = [...new Set(probe.adapters.flatMap(a => a.skippedTracked))].map(abs => path.relative(projectRoot, abs));
|
|
527
|
+
if (vetoed.length) {
|
|
528
|
+
const shown = vetoed.slice(0, 5).join(', ') + (vetoed.length > 5 ? `, and ${vetoed.length - 5} more` : '');
|
|
529
|
+
return fail(`uncommitted changes in tracked generated files the re-render would overwrite (through a symlinked directory or not): ${shown}; commit or stash them, or rerun with --no-render`);
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
}
|
|
145
533
|
|
|
146
|
-
log(
|
|
147
|
-
|
|
534
|
+
log(`Wize Dev Kit update: ${from} → ${to} (in-place)`);
|
|
535
|
+
if (plan.converged) log('Already converged. Re-rendering adapters anyway (idempotent).');
|
|
148
536
|
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
537
|
+
const wrote = writePlan(projectRoot, plan);
|
|
538
|
+
if (wrote.error) return fail(wrote.error);
|
|
539
|
+
const pinned = readToml(path.join(projectRoot, PROJECT_TOML));
|
|
540
|
+
if (!pinned.project || pinned.project.kit_version !== to) return fail('could not write kit_version in project.toml');
|
|
541
|
+
for (const f of plan.files) {
|
|
542
|
+
if (f.mode === 'pin') log(`✓ project.toml kit_version → ${to}`);
|
|
543
|
+
else if (f.path === '.gitignore') log(`✓ .gitignore ${f.mode}`);
|
|
544
|
+
else log(`✓ ${f.path} ${f.mode} refreshed`);
|
|
153
545
|
}
|
|
154
546
|
|
|
155
|
-
|
|
156
|
-
|
|
547
|
+
if (plan.untrack.length) {
|
|
548
|
+
const r = ops.rmCached(projectRoot, plan.untrack);
|
|
549
|
+
if (r.ok) log(`✓ untracked ${plan.untrack.length} generated alias file(s) (kept on disk)`);
|
|
550
|
+
else return fail(`git rm --cached failed: ${r.err}`);
|
|
551
|
+
}
|
|
157
552
|
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
553
|
+
let adapters = [];
|
|
554
|
+
let renderOk = true;
|
|
555
|
+
if (!o.noRender) {
|
|
556
|
+
const targets = (cfg.install && cfg.install.ide_targets) || ['claude-code', 'generic'];
|
|
557
|
+
const profiles = (cfg.install && cfg.install.profiles) || ['core'];
|
|
558
|
+
log('\nRe-rendering active IDE adapters:');
|
|
559
|
+
const rendered = renderTargets({ kitRoot, projectRoot, targets, profiles, log });
|
|
560
|
+
adapters = rendered.adapters;
|
|
561
|
+
renderOk = rendered.ok;
|
|
562
|
+
if (renderOk) {
|
|
563
|
+
writeMarker(projectRoot, {
|
|
564
|
+
kit_version: to,
|
|
565
|
+
pin: to,
|
|
566
|
+
targets,
|
|
567
|
+
profiles,
|
|
568
|
+
counts: {
|
|
569
|
+
written: adapters.reduce((n, a) => n + a.written, 0),
|
|
570
|
+
unchanged: adapters.reduce((n, a) => n + a.unchanged, 0)
|
|
571
|
+
},
|
|
572
|
+
delegated: false,
|
|
573
|
+
rendered_with: to
|
|
574
|
+
});
|
|
164
575
|
}
|
|
165
|
-
log(`\nFull details: ${path.join(kitRoot, 'CHANGELOG.md')}`);
|
|
166
576
|
}
|
|
167
577
|
|
|
168
|
-
|
|
169
|
-
|
|
578
|
+
if (ctx.isRepo) {
|
|
579
|
+
const def = defaultBranch(projectRoot);
|
|
580
|
+
if (ctx.detached || ctx.branch === def) {
|
|
581
|
+
log(`\ndo not commit this on ${ctx.detached ? 'a detached HEAD' : def}: create a branch first (or rerun without --in-place)`);
|
|
582
|
+
} else if (ctx.linked) {
|
|
583
|
+
log('\nwarning: pin bump on a feature branch conflicts at merge; prefer the default (isolated) mode');
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
printChangelog(log, kitRoot, changelog);
|
|
588
|
+
log('\nRestart your harness after the change reaches this checkout.');
|
|
589
|
+
return emit(result({ changed: true, adapters, converged: plan.converged, exitCode: renderOk ? 0 : 1 }));
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
// ---- --pr -------------------------------------------------------------------------
|
|
593
|
+
|
|
594
|
+
// Push (never forced) and open or find the PR. { url } or { error }. With
|
|
595
|
+
// `skipPush` the remote branch already carries the update. A push the remote
|
|
596
|
+
// rejects is fine too when the remote branch carries the same update, which
|
|
597
|
+
// happens when two checkouts built the same commit content separately.
|
|
598
|
+
function openPr({ projectRoot, branch, def, from, to, entries, log, skipPush = false, remoteConverged = () => false }) {
|
|
599
|
+
if (!hasRemote(projectRoot)) return { error: `no origin remote; branch ${branch} kept locally` };
|
|
600
|
+
if (!skipPush) {
|
|
601
|
+
const pushed = ops.push(projectRoot, 'origin', branch);
|
|
602
|
+
if (!pushed.ok) {
|
|
603
|
+
if (!/rejected|non-fast-forward|fetch first/i.test(pushed.err)) return { error: `git push failed: ${pushed.err}` };
|
|
604
|
+
if (!remoteConverged()) return { error: `remote ${branch} diverged; resolve manually` };
|
|
605
|
+
log(`origin/${branch} already carries this update`);
|
|
606
|
+
} else {
|
|
607
|
+
log(`✓ pushed ${branch} to origin`);
|
|
608
|
+
}
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
const title = updateSubject(from, to);
|
|
612
|
+
if (ops.ghAvailable()) {
|
|
613
|
+
const existing = ops.ghPrView(projectRoot, branch);
|
|
614
|
+
if (existing && String(existing.state).toUpperCase() === 'OPEN') {
|
|
615
|
+
log(`PR already open: ${existing.url}`);
|
|
616
|
+
return { url: existing.url };
|
|
617
|
+
}
|
|
618
|
+
const body = [
|
|
619
|
+
{ converge: `Converges the wize-dev-kit pin at ${to}.`, downgrade: `Downgrades the wize-dev-kit pin from ${from} to ${to}.`, bump: `Bumps the wize-dev-kit pin from ${from} to ${to}.` }[updateVerb(from, to)],
|
|
620
|
+
...(entries.length ? ['', entries.map(excerpt).join('\n\n')] : []),
|
|
621
|
+
'',
|
|
622
|
+
'After merge every worktree self-hydrates on its next session (`hydrate`).'
|
|
623
|
+
].join('\n');
|
|
624
|
+
const created = ops.ghPrCreate(projectRoot, { base: def, head: branch, title, body });
|
|
625
|
+
if (created.ok) {
|
|
626
|
+
log(`✓ PR opened: ${created.url}`);
|
|
627
|
+
return { url: created.url };
|
|
628
|
+
}
|
|
629
|
+
log(`warning: gh pr create failed: ${created.err.split('\n')[0]}`);
|
|
630
|
+
}
|
|
631
|
+
const web = ops.originWebUrl(projectRoot);
|
|
632
|
+
if (web) {
|
|
633
|
+
const url = `${web}/compare/${def}...${branch}?expand=1`;
|
|
634
|
+
log(`Open the PR: ${url}`);
|
|
635
|
+
return { url };
|
|
636
|
+
}
|
|
637
|
+
log(`open a PR for ${branch}`);
|
|
638
|
+
return { url: null };
|
|
170
639
|
}
|
|
171
640
|
|
|
172
|
-
module.exports = {
|
|
641
|
+
module.exports = {
|
|
642
|
+
cmdUpdate,
|
|
643
|
+
planTrackedChanges,
|
|
644
|
+
readToml,
|
|
645
|
+
loadProjectConfig,
|
|
646
|
+
loadInstalledKitVersion,
|
|
647
|
+
changelogBetween
|
|
648
|
+
};
|