@foldspace_npm/harness 0.1.10 → 0.1.12
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/CLAUDE.md +328 -0
- package/README.md +24 -5
- package/bin/cli.mjs +29 -1
- package/package.json +4 -2
- package/recipes/INDEX.md +15 -0
- package/recipes/README.md +46 -0
- package/recipes/find-by-name/README.md +47 -0
- package/recipes/find-by-name/agent/actions/find_project_id.ts +101 -0
- package/recipes/find-by-name/agent/api/projects.ts +36 -0
- package/recipes/find-by-name/agent/projects.ts +40 -0
- package/recipes/find-by-name/fixtures/projects.all.json +29 -0
- package/recipes/find-by-name/fixtures/projects.empty-account.json +4 -0
- package/recipes/find-by-name/fixtures/projects.none.json +4 -0
- package/recipes/find-by-name/recipe.json +10 -0
- package/recipes/pick-from-a-list/README.md +44 -0
- package/recipes/pick-from-a-list/agent/actions/choose_project.ts +161 -0
- package/recipes/pick-from-a-list/agent/api/projects.ts +36 -0
- package/recipes/pick-from-a-list/agent/projects.ts +40 -0
- package/recipes/pick-from-a-list/agent/views/brand.ts +14 -0
- package/recipes/pick-from-a-list/agent/views/picker.ts +119 -0
- package/recipes/pick-from-a-list/fixtures/projects.all.json +29 -0
- package/recipes/pick-from-a-list/fixtures/projects.empty-account.json +4 -0
- package/recipes/pick-from-a-list/recipe.json +10 -0
- package/recipes/swap-the-login-method/README.md +40 -0
- package/recipes/swap-the-login-method/agent/utils.ts +73 -0
- package/recipes/swap-the-login-method/fixtures/anything.ok.json +8 -0
- package/recipes/swap-the-login-method/recipe.json +12 -0
- package/recipes/swap-the-login-method/variants/utils.cookies.ts +64 -0
- package/recipes/who-is-the-user/README.md +56 -0
- package/recipes/who-is-the-user/agent/identify.ts +89 -0
- package/recipes/who-is-the-user/fixtures/profile.ok.json +6 -0
- package/recipes/who-is-the-user/recipe.json +9 -0
- package/src/cli-help.mjs +1 -0
- package/src/cli-registry.mjs +48 -3
- package/src/init.mjs +16 -8
- package/src/runtime/config.ts +1 -1
- package/src/runtime/http.ts +104 -53
- package/src/runtime/index.ts +4 -1
- package/src/runtime/match.ts +1 -1
- package/src/runtime/render.ts +42 -1
- package/src/upgrade.mjs +469 -0
- package/templates/agent-starter/CLAUDE.md +11 -220
- package/templates/agent-starter/README.md +1 -1
- package/templates/agent-starter/agent/actions/_example.ts +9 -2
- package/templates/agent-starter/agent/utils.ts +2 -0
package/src/upgrade.mjs
ADDED
|
@@ -0,0 +1,469 @@
|
|
|
1
|
+
import { execFileSync, spawnSync } from "node:child_process";
|
|
2
|
+
import fs from "node:fs";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import readline from "node:readline/promises";
|
|
5
|
+
import { stdin as input } from "node:process";
|
|
6
|
+
import { fileURLToPath } from "node:url";
|
|
7
|
+
|
|
8
|
+
export const HARNESS_PACKAGE = "@foldspace_npm/harness";
|
|
9
|
+
export const INSTRUCTION_IMPORT = `@node_modules/${HARNESS_PACKAGE}/CLAUDE.md`;
|
|
10
|
+
export const CACHE_TTL_MS = 12 * 60 * 60 * 1000;
|
|
11
|
+
export const NPM_VIEW_TIMEOUT_MS = 2000;
|
|
12
|
+
export const UPGRADE_NEXT = "Ask the user, then run foldspace upgrade --yes";
|
|
13
|
+
|
|
14
|
+
const packageRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
|
15
|
+
const instructionTemplatePath = path.join(
|
|
16
|
+
packageRoot,
|
|
17
|
+
"templates",
|
|
18
|
+
"agent-starter",
|
|
19
|
+
"CLAUDE.md",
|
|
20
|
+
);
|
|
21
|
+
const tokenPattern = /\{\{([A-Z0-9_]+)\}\}/g;
|
|
22
|
+
|
|
23
|
+
export function resolveProjectDir(env = process.env, cwd = process.cwd()) {
|
|
24
|
+
return env.FOLDSPACE_PROJECT_DIR || cwd;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function parseUpgradeArgs(argv) {
|
|
28
|
+
const flags = new Set();
|
|
29
|
+
for (const token of argv) {
|
|
30
|
+
if (token === "--check" || token === "--yes" || token === "--refresh-instructions") {
|
|
31
|
+
if (flags.has(token)) {
|
|
32
|
+
throw new Error(`option '${token}' was provided twice`);
|
|
33
|
+
}
|
|
34
|
+
flags.add(token);
|
|
35
|
+
continue;
|
|
36
|
+
}
|
|
37
|
+
throw new Error(`unknown option '${token}'`);
|
|
38
|
+
}
|
|
39
|
+
if (flags.has("--check") && flags.has("--yes")) {
|
|
40
|
+
throw new Error("options '--check' and '--yes' cannot be used together");
|
|
41
|
+
}
|
|
42
|
+
if (flags.has("--check") && flags.has("--refresh-instructions")) {
|
|
43
|
+
throw new Error(
|
|
44
|
+
"options '--check' and '--refresh-instructions' cannot be used together",
|
|
45
|
+
);
|
|
46
|
+
}
|
|
47
|
+
return {
|
|
48
|
+
checkOnly: flags.has("--check"),
|
|
49
|
+
yes: flags.has("--yes"),
|
|
50
|
+
refreshInstructions: flags.has("--refresh-instructions"),
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function normalizeVersion(spec) {
|
|
55
|
+
if (typeof spec !== "string" || !spec.trim()) return null;
|
|
56
|
+
const trimmed = spec.trim();
|
|
57
|
+
if (trimmed.startsWith("file:") || trimmed.startsWith("link:") || trimmed.startsWith("github:")) {
|
|
58
|
+
return null;
|
|
59
|
+
}
|
|
60
|
+
const match = trimmed.match(/^[~^]?(\d+\.\d+\.\d+)/);
|
|
61
|
+
return match ? match[1] : null;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export function compareVersions(left, right) {
|
|
65
|
+
const a = parseSemver(left);
|
|
66
|
+
const b = parseSemver(right);
|
|
67
|
+
if (!a || !b) return 0;
|
|
68
|
+
for (let index = 0; index < 3; index += 1) {
|
|
69
|
+
if (a[index] > b[index]) return 1;
|
|
70
|
+
if (a[index] < b[index]) return -1;
|
|
71
|
+
}
|
|
72
|
+
return 0;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function parseSemver(value) {
|
|
76
|
+
const version = normalizeVersion(value);
|
|
77
|
+
if (!version) return null;
|
|
78
|
+
return version.split(".").map((part) => Number(part));
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export function isBehind(current, latest) {
|
|
82
|
+
if (!current || !latest) return false;
|
|
83
|
+
return compareVersions(current, latest) < 0;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function readJson(filePath) {
|
|
87
|
+
return JSON.parse(fs.readFileSync(filePath, "utf8"));
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function writeJson(filePath, value) {
|
|
91
|
+
fs.writeFileSync(filePath, `${JSON.stringify(value, null, 2)}\n`);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export function readPackageManifest(projectDir) {
|
|
95
|
+
const manifestPath = path.join(projectDir, "package.json");
|
|
96
|
+
if (!fs.existsSync(manifestPath)) return null;
|
|
97
|
+
try {
|
|
98
|
+
return readJson(manifestPath);
|
|
99
|
+
} catch {
|
|
100
|
+
return null;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export function isHarnessSourceTree(projectDir) {
|
|
105
|
+
const manifest = readPackageManifest(projectDir);
|
|
106
|
+
return Boolean(
|
|
107
|
+
manifest?.name === HARNESS_PACKAGE &&
|
|
108
|
+
fs.existsSync(path.join(projectDir, "templates", "agent-starter")),
|
|
109
|
+
);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export function readPinnedVersion(manifest) {
|
|
113
|
+
if (!manifest || typeof manifest !== "object") return null;
|
|
114
|
+
const spec =
|
|
115
|
+
manifest.devDependencies?.[HARNESS_PACKAGE] ||
|
|
116
|
+
manifest.dependencies?.[HARNESS_PACKAGE] ||
|
|
117
|
+
null;
|
|
118
|
+
return normalizeVersion(spec);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
export function readInstalledVersion(projectDir) {
|
|
122
|
+
const installedPath = path.join(
|
|
123
|
+
projectDir,
|
|
124
|
+
"node_modules",
|
|
125
|
+
HARNESS_PACKAGE,
|
|
126
|
+
"package.json",
|
|
127
|
+
);
|
|
128
|
+
if (!fs.existsSync(installedPath)) return null;
|
|
129
|
+
try {
|
|
130
|
+
return normalizeVersion(readJson(installedPath).version);
|
|
131
|
+
} catch {
|
|
132
|
+
return null;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
export function hasLiveInstructionImport(content) {
|
|
137
|
+
return typeof content === "string" && content.includes(INSTRUCTION_IMPORT);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
export function instructionValuesFromProject(projectDir) {
|
|
141
|
+
const configPath = path.join(projectDir, "foldspace.dev.json");
|
|
142
|
+
if (!fs.existsSync(configPath)) {
|
|
143
|
+
throw new Error("foldspace.dev.json is required to refresh CLAUDE.md");
|
|
144
|
+
}
|
|
145
|
+
const config = readJson(configPath);
|
|
146
|
+
const targetName = config.defaultTarget;
|
|
147
|
+
const target = config.targets?.[targetName];
|
|
148
|
+
if (!target?.productId || !target?.agentApiName) {
|
|
149
|
+
throw new Error(
|
|
150
|
+
`foldspace.dev.json target '${targetName || "default"}' is missing productId or agentApiName`,
|
|
151
|
+
);
|
|
152
|
+
}
|
|
153
|
+
let domain = null;
|
|
154
|
+
if (typeof target.startUrl === "string") {
|
|
155
|
+
try {
|
|
156
|
+
domain = new URL(target.startUrl).hostname;
|
|
157
|
+
} catch {
|
|
158
|
+
domain = null;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
if (!domain && Array.isArray(target.hosts) && target.hosts[0]) {
|
|
162
|
+
domain = String(target.hosts[0]).replace(/^\*\./, "");
|
|
163
|
+
}
|
|
164
|
+
if (!domain) {
|
|
165
|
+
throw new Error("Could not resolve the app domain from foldspace.dev.json");
|
|
166
|
+
}
|
|
167
|
+
const manifest = readPackageManifest(projectDir);
|
|
168
|
+
return {
|
|
169
|
+
DISPLAY_NAME: manifest?.name || path.basename(projectDir),
|
|
170
|
+
PRODUCT_ID: String(target.productId),
|
|
171
|
+
AGENT_API_NAME: String(target.agentApiName),
|
|
172
|
+
APP_DOMAIN: domain,
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
export function renderInstructionStub(values, template = fs.readFileSync(instructionTemplatePath, "utf8")) {
|
|
177
|
+
const rendered = template.replace(tokenPattern, (_match, key) => {
|
|
178
|
+
if (!(key in values)) {
|
|
179
|
+
throw new Error(`Missing value for {{${key}}} in CLAUDE.md stub`);
|
|
180
|
+
}
|
|
181
|
+
return values[key];
|
|
182
|
+
});
|
|
183
|
+
const unresolved = rendered.match(tokenPattern);
|
|
184
|
+
if (unresolved) {
|
|
185
|
+
throw new Error(`Unresolved template token ${unresolved[0]} in CLAUDE.md stub`);
|
|
186
|
+
}
|
|
187
|
+
return rendered;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
export function refreshInstructionStub(projectDir, options = {}) {
|
|
191
|
+
const claudePath = path.join(projectDir, "CLAUDE.md");
|
|
192
|
+
const previous = fs.existsSync(claudePath)
|
|
193
|
+
? fs.readFileSync(claudePath, "utf8")
|
|
194
|
+
: null;
|
|
195
|
+
const next = renderInstructionStub(
|
|
196
|
+
options.values || instructionValuesFromProject(projectDir),
|
|
197
|
+
options.template,
|
|
198
|
+
);
|
|
199
|
+
if (previous === next) {
|
|
200
|
+
return { written: false, backedUp: false, importPresent: true };
|
|
201
|
+
}
|
|
202
|
+
let backedUp = false;
|
|
203
|
+
if (previous !== null) {
|
|
204
|
+
fs.writeFileSync(path.join(projectDir, "CLAUDE.md.bak"), previous);
|
|
205
|
+
backedUp = true;
|
|
206
|
+
}
|
|
207
|
+
fs.writeFileSync(claudePath, next);
|
|
208
|
+
return { written: true, backedUp, importPresent: true };
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
function cachePath(projectDir) {
|
|
212
|
+
return path.join(projectDir, ".foldspace-dev", "harness-update.json");
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
function readCache(projectDir, now) {
|
|
216
|
+
const filePath = cachePath(projectDir);
|
|
217
|
+
if (!fs.existsSync(filePath)) return null;
|
|
218
|
+
try {
|
|
219
|
+
const cached = readJson(filePath);
|
|
220
|
+
if (typeof cached.checkedAt !== "number" || typeof cached.latest !== "string") {
|
|
221
|
+
return null;
|
|
222
|
+
}
|
|
223
|
+
if (now - cached.checkedAt > CACHE_TTL_MS) return null;
|
|
224
|
+
return cached;
|
|
225
|
+
} catch {
|
|
226
|
+
return null;
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
function writeCache(projectDir, latest, now) {
|
|
231
|
+
const dir = path.dirname(cachePath(projectDir));
|
|
232
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
233
|
+
writeJson(cachePath(projectDir), { checkedAt: now, latest });
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
export function fetchLatestFromNpm(options = {}) {
|
|
237
|
+
const spawn = options.spawnSyncFn || spawnSync;
|
|
238
|
+
const result = spawn("npm", ["view", HARNESS_PACKAGE, "version"], {
|
|
239
|
+
encoding: "utf8",
|
|
240
|
+
timeout: options.timeoutMs ?? NPM_VIEW_TIMEOUT_MS,
|
|
241
|
+
env: options.env || process.env,
|
|
242
|
+
});
|
|
243
|
+
if (result.error || result.status !== 0) return null;
|
|
244
|
+
return normalizeVersion(result.stdout);
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
function nextAction(report) {
|
|
248
|
+
if (report.status === "outdated") {
|
|
249
|
+
return report.instructionsImport === false
|
|
250
|
+
? `${UPGRADE_NEXT}. If CLAUDE.md is still a snapshot, also pass --refresh-instructions`
|
|
251
|
+
: UPGRADE_NEXT;
|
|
252
|
+
}
|
|
253
|
+
if (report.status === "upgraded") {
|
|
254
|
+
return "Run npm run build. Deploy only if product users should receive the rebuilt runtime.";
|
|
255
|
+
}
|
|
256
|
+
return null;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
export function checkForUpdate(projectDir, options = {}) {
|
|
260
|
+
const env = options.env || process.env;
|
|
261
|
+
const now = options.now ?? Date.now();
|
|
262
|
+
const manifest = readPackageManifest(projectDir);
|
|
263
|
+
const harnessSource = isHarnessSourceTree(projectDir);
|
|
264
|
+
const pinned = harnessSource ? null : readPinnedVersion(manifest);
|
|
265
|
+
const installed = harnessSource
|
|
266
|
+
? normalizeVersion(manifest?.version)
|
|
267
|
+
: readInstalledVersion(projectDir);
|
|
268
|
+
const claudePath = path.join(projectDir, "CLAUDE.md");
|
|
269
|
+
const instructionsImport = fs.existsSync(claudePath)
|
|
270
|
+
? hasLiveInstructionImport(fs.readFileSync(claudePath, "utf8"))
|
|
271
|
+
: false;
|
|
272
|
+
|
|
273
|
+
const skipped =
|
|
274
|
+
options.skipCheck === true ||
|
|
275
|
+
(options.skipCheck !== false && env.FOLDSPACE_SKIP_UPDATE_CHECK === "1");
|
|
276
|
+
const consumer = !harnessSource && Boolean(pinned || installed);
|
|
277
|
+
|
|
278
|
+
let latest = null;
|
|
279
|
+
let status = "current";
|
|
280
|
+
|
|
281
|
+
if (!consumer) {
|
|
282
|
+
status = "not_a_consumer";
|
|
283
|
+
} else if (skipped) {
|
|
284
|
+
status = "check_skipped";
|
|
285
|
+
} else {
|
|
286
|
+
const cached = options.cache === false ? null : readCache(projectDir, now);
|
|
287
|
+
if (cached?.latest) {
|
|
288
|
+
latest = cached.latest;
|
|
289
|
+
} else {
|
|
290
|
+
const fetched =
|
|
291
|
+
typeof options.fetchLatest === "function"
|
|
292
|
+
? options.fetchLatest()
|
|
293
|
+
: fetchLatestFromNpm(options);
|
|
294
|
+
latest = normalizeVersion(fetched);
|
|
295
|
+
if (latest && options.cache !== false) {
|
|
296
|
+
writeCache(projectDir, latest, now);
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
if (!latest) {
|
|
300
|
+
status = "check_skipped";
|
|
301
|
+
} else {
|
|
302
|
+
const outdated = isBehind(pinned, latest) || isBehind(installed, latest);
|
|
303
|
+
status = outdated ? "outdated" : "current";
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
const report = {
|
|
308
|
+
kind: "foldspace.cli.update",
|
|
309
|
+
package: HARNESS_PACKAGE,
|
|
310
|
+
pinned,
|
|
311
|
+
installed,
|
|
312
|
+
latest,
|
|
313
|
+
outdated: status === "outdated",
|
|
314
|
+
status,
|
|
315
|
+
instructionsImport,
|
|
316
|
+
next: null,
|
|
317
|
+
};
|
|
318
|
+
report.next = nextAction(report);
|
|
319
|
+
return report;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
export function writeExactPin(projectDir, version) {
|
|
323
|
+
const manifestPath = path.join(projectDir, "package.json");
|
|
324
|
+
const manifest = readPackageManifest(projectDir);
|
|
325
|
+
if (!manifest) {
|
|
326
|
+
throw new Error(`package.json not found in ${projectDir}`);
|
|
327
|
+
}
|
|
328
|
+
if (manifest.dependencies?.[HARNESS_PACKAGE] !== undefined) {
|
|
329
|
+
manifest.dependencies[HARNESS_PACKAGE] = version;
|
|
330
|
+
} else {
|
|
331
|
+
manifest.devDependencies = manifest.devDependencies || {};
|
|
332
|
+
manifest.devDependencies[HARNESS_PACKAGE] = version;
|
|
333
|
+
}
|
|
334
|
+
writeJson(manifestPath, manifest);
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
function defaultInstall(projectDir) {
|
|
338
|
+
execFileSync("npm", ["install", "--ignore-scripts"], {
|
|
339
|
+
cwd: projectDir,
|
|
340
|
+
stdio: "inherit",
|
|
341
|
+
env: process.env,
|
|
342
|
+
});
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
function isInteractive(options = {}) {
|
|
346
|
+
if (typeof options.interactive === "boolean") return options.interactive;
|
|
347
|
+
return Boolean(options.stdin?.isTTY ?? input.isTTY);
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
function isAffirmative(value) {
|
|
351
|
+
const normalized = value.trim().toLowerCase();
|
|
352
|
+
return normalized === "" || normalized === "y" || normalized === "yes";
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
async function ask(question, options = {}) {
|
|
356
|
+
if (typeof options.ask === "function") return options.ask(question);
|
|
357
|
+
const rl = readline.createInterface({
|
|
358
|
+
input: options.stdin || input,
|
|
359
|
+
output: options.stderr || process.stderr,
|
|
360
|
+
});
|
|
361
|
+
try {
|
|
362
|
+
return await rl.question(question);
|
|
363
|
+
} finally {
|
|
364
|
+
rl.close();
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
export async function runUpgrade(argv, options = {}) {
|
|
369
|
+
const parsed = parseUpgradeArgs(argv);
|
|
370
|
+
const projectDir = options.projectDir || resolveProjectDir(options.env);
|
|
371
|
+
const log = options.log || ((message) => {
|
|
372
|
+
console.error(message);
|
|
373
|
+
});
|
|
374
|
+
|
|
375
|
+
if (isHarnessSourceTree(projectDir) && (parsed.yes || parsed.refreshInstructions)) {
|
|
376
|
+
throw new Error(
|
|
377
|
+
"this directory is the harness source, not a consumer project",
|
|
378
|
+
);
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
const report = checkForUpdate(projectDir, options);
|
|
382
|
+
|
|
383
|
+
if (parsed.checkOnly || (!parsed.yes && !isInteractive(options) && !parsed.refreshInstructions)) {
|
|
384
|
+
return report;
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
if (parsed.refreshInstructions && !parsed.yes && report.status !== "outdated") {
|
|
388
|
+
const refreshed = refreshInstructionStub(projectDir, options);
|
|
389
|
+
return {
|
|
390
|
+
...report,
|
|
391
|
+
status: refreshed.written ? "instructions_refreshed" : "current",
|
|
392
|
+
instructionsImport: true,
|
|
393
|
+
refreshed: refreshed.written,
|
|
394
|
+
backedUp: refreshed.backedUp,
|
|
395
|
+
next: refreshed.written
|
|
396
|
+
? "Imported platform instructions from the installed harness."
|
|
397
|
+
: report.next,
|
|
398
|
+
};
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
if (report.status === "not_a_consumer") {
|
|
402
|
+
throw new Error(
|
|
403
|
+
`no ${HARNESS_PACKAGE} dependency found in ${projectDir}`,
|
|
404
|
+
);
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
if (parsed.yes && report.status === "check_skipped") {
|
|
408
|
+
throw new Error("cannot upgrade without a registry version");
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
let shouldInstall = parsed.yes && report.status === "outdated";
|
|
412
|
+
let shouldRefresh = parsed.refreshInstructions;
|
|
413
|
+
|
|
414
|
+
if (!parsed.yes && !parsed.checkOnly && isInteractive(options) && report.status === "outdated") {
|
|
415
|
+
log(
|
|
416
|
+
`${HARNESS_PACKAGE} ${report.pinned || report.installed} -> ${report.latest} is available`,
|
|
417
|
+
);
|
|
418
|
+
const answer = await ask("Upgrade the exact pin in this project? [Y/n] ", options);
|
|
419
|
+
shouldInstall = isAffirmative(answer);
|
|
420
|
+
if (!shouldInstall) {
|
|
421
|
+
return { ...report, status: "declined", next: UPGRADE_NEXT };
|
|
422
|
+
}
|
|
423
|
+
if (!report.instructionsImport && !shouldRefresh) {
|
|
424
|
+
const refreshAnswer = await ask(
|
|
425
|
+
"Replace CLAUDE.md with an import of the package instructions? [Y/n] ",
|
|
426
|
+
options,
|
|
427
|
+
);
|
|
428
|
+
shouldRefresh = isAffirmative(refreshAnswer);
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
if (!shouldInstall && !shouldRefresh) {
|
|
433
|
+
return report;
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
if (shouldInstall) {
|
|
437
|
+
if (!report.latest) {
|
|
438
|
+
throw new Error("cannot upgrade without a registry version");
|
|
439
|
+
}
|
|
440
|
+
writeExactPin(projectDir, report.latest);
|
|
441
|
+
const install = options.install || defaultInstall;
|
|
442
|
+
install(projectDir, report.latest);
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
let refreshed = { written: false, backedUp: false, importPresent: report.instructionsImport };
|
|
446
|
+
if (shouldRefresh) {
|
|
447
|
+
refreshed = refreshInstructionStub(projectDir, options);
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
const nextReport = {
|
|
451
|
+
...report,
|
|
452
|
+
pinned: shouldInstall ? report.latest : report.pinned,
|
|
453
|
+
installed: shouldInstall ? report.latest : report.installed,
|
|
454
|
+
outdated: false,
|
|
455
|
+
status: shouldInstall ? "upgraded" : refreshed.written ? "instructions_refreshed" : report.status,
|
|
456
|
+
instructionsImport: shouldRefresh ? true : report.instructionsImport,
|
|
457
|
+
refreshed: refreshed.written,
|
|
458
|
+
backedUp: refreshed.backedUp,
|
|
459
|
+
};
|
|
460
|
+
nextReport.next = nextAction(nextReport);
|
|
461
|
+
return nextReport;
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
export function nagIfOutdated(report, write = (message) => console.error(message)) {
|
|
465
|
+
if (!report?.outdated) return;
|
|
466
|
+
write(
|
|
467
|
+
`foldspace: ${HARNESS_PACKAGE} ${report.pinned || report.installed} is behind ${report.latest}. ${UPGRADE_NEXT}.`,
|
|
468
|
+
);
|
|
469
|
+
}
|