@foldspace_npm/harness 0.1.20 → 0.1.22
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 +8 -3
- package/README.md +49 -0
- package/bin/attach.mjs +8 -23
- package/bin/badge.mjs +7 -7
- package/bin/cli.mjs +21 -2
- package/package.json +1 -1
- package/src/badge-core.mjs +2 -2
- package/src/cli-registry.mjs +10 -6
- package/src/upgrade.mjs +71 -4
- package/templates/agent-starter/docs/app-profile.md +4 -2
package/CLAUDE.md
CHANGED
|
@@ -13,7 +13,11 @@ something they can **see on their own page** — not at "it builds".
|
|
|
13
13
|
`npx --yes @foldspace_npm/harness@latest upgrade --check`). If
|
|
14
14
|
`update.outdated` is true, say the pinned and latest versions and **ask
|
|
15
15
|
before upgrading**; then `foldspace upgrade --yes` and `npm run build`. Never
|
|
16
|
-
upgrade as a side effect of build, attach or implementing an action.
|
|
16
|
+
upgrade as a side effect of build, attach or implementing an action.
|
|
17
|
+
`build` and `attach` check too: behind prints one line and carries on; a
|
|
18
|
+
version Foldspace **withdrew** refuses to run and says why. Tell the user
|
|
19
|
+
the reason and ask to upgrade. Only if they decide not to, rerun with
|
|
20
|
+
`FOLDSPACE_SKIP_UPDATE_CHECK=1` - never set it on your own. If the
|
|
17
21
|
project `CLAUDE.md` does not contain
|
|
18
22
|
`@node_modules/@foldspace_npm/harness/CLAUDE.md`, ask before
|
|
19
23
|
`foldspace upgrade --refresh-instructions`.
|
|
@@ -77,8 +81,9 @@ at the app or testing in it, run `npx foldspace badge working` (a bar across
|
|
|
77
81
|
the top of the window says Foldspace is building here, leave it alone) and say
|
|
78
82
|
so in chat: *"I'm going to work in that Chrome window for a few minutes -
|
|
79
83
|
please leave it alone until I tell you it's ready."* When it is their turn,
|
|
80
|
-
`npx foldspace badge ready
|
|
81
|
-
|
|
84
|
+
`npx foldspace badge ready` takes the pill away (the small corner tag comes
|
|
85
|
+
back) and in chat you say what to try. No bar for their turn: they may type
|
|
86
|
+
whatever, whenever; the pill only ever asks them to wait. `foldspace badge label` puts the small
|
|
82
87
|
corner tag back; `off` removes it, for a screenshot they will share.
|
|
83
88
|
|
|
84
89
|
Navigation, uploads, extraction and anything that writes are a later session.
|
package/README.md
CHANGED
|
@@ -151,6 +151,55 @@ foldspace upgrade --yes # only after the user agrees
|
|
|
151
151
|
`--yes` rewrites the exact pin and runs `npm install --ignore-scripts`. Rebuild
|
|
152
152
|
afterward; that is still not a deploy.
|
|
153
153
|
|
|
154
|
+
`foldspace build` and `foldspace attach` run the same check before they start
|
|
155
|
+
(`attach --status` and `--stop` do not). A project that is behind gets one line
|
|
156
|
+
on stderr and carries on. A project on a **withdrawn** version (below) refuses
|
|
157
|
+
to run and prints the reason. To build on it anyway, once the user has declined
|
|
158
|
+
the upgrade:
|
|
159
|
+
|
|
160
|
+
```bash
|
|
161
|
+
FOLDSPACE_SKIP_UPDATE_CHECK=1 npm run build
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
That turns the check off entirely; it does not fix the withdrawn version.
|
|
165
|
+
|
|
166
|
+
### Withdrawing a release (maintainers)
|
|
167
|
+
|
|
168
|
+
When a published version is broken badly enough that nobody should build on
|
|
169
|
+
it, withdraw it with `npm deprecate`. The registry is the list of withdrawn
|
|
170
|
+
versions, so projects already installed on it see the withdrawal without
|
|
171
|
+
installing anything new. Only owners of `@foldspace_npm/harness` on npm can
|
|
172
|
+
run it.
|
|
173
|
+
|
|
174
|
+
1. **Publish the fix first.** If the withdrawn version is also `latest`, the
|
|
175
|
+
message tells users to upgrade to a version that is itself withdrawn.
|
|
176
|
+
2. **Withdraw the broken version**, with a message that says what is wrong and
|
|
177
|
+
which version to move to. That message is exactly what users, and the coding
|
|
178
|
+
agent working for them, see when `build` or `attach` refuses to run:
|
|
179
|
+
|
|
180
|
+
```bash
|
|
181
|
+
npm deprecate @foldspace_npm/harness@0.1.14 "attach loads the wrong bundle; upgrade to 0.1.15"
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
A range works too: `npm deprecate "@foldspace_npm/harness@<0.1.15" "…"`.
|
|
185
|
+
3. **Check it took:**
|
|
186
|
+
|
|
187
|
+
```bash
|
|
188
|
+
npm view @foldspace_npm/harness@0.1.14 deprecated
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
To undo a withdrawal, deprecate with an empty message:
|
|
192
|
+
|
|
193
|
+
```bash
|
|
194
|
+
npm deprecate @foldspace_npm/harness@0.1.14 ""
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
**It is not instant.** Each project caches the registry's answer for 12 hours
|
|
198
|
+
in `.foldspace-dev/harness-update.json`. A project that checked recently keeps
|
|
199
|
+
its "fine" answer until that expires, so a withdrawal reaches every project
|
|
200
|
+
within 12 hours. It only reaches projects on a harness version that has this
|
|
201
|
+
check.
|
|
202
|
+
|
|
154
203
|
### Lint handlers before they ship
|
|
155
204
|
|
|
156
205
|
`foldspace build` runs `foldspace lint` first. Errors skip bundling; warnings
|
package/bin/attach.mjs
CHANGED
|
@@ -225,7 +225,8 @@ const SDK_PATTERNS = [
|
|
|
225
225
|
// label - the small corner tag (default): who is attached, which mode
|
|
226
226
|
// working - a pill at the foot of the window: the agent is building in this
|
|
227
227
|
// window, please leave it alone
|
|
228
|
-
// ready - the
|
|
228
|
+
// ready - the pill goes away and the small tag comes back: their turn.
|
|
229
|
+
// No bar for that - they may type whatever, whenever
|
|
229
230
|
// The pill sits bottom-left, never across the top: a bar there covered the
|
|
230
231
|
// app's own header and menu (seen on a CRM, 2026-09-22). The agent's panel
|
|
231
232
|
// is on the right, so bottom-left is the corner nothing else uses.
|
|
@@ -252,34 +253,18 @@ const badgeSrc = `(() => {
|
|
|
252
253
|
if (s.state === "working") {
|
|
253
254
|
// Attention, not decoration: a pulsing marker, the ask in bold, and an
|
|
254
255
|
// animated line underneath so it reads as "in progress" at a glance.
|
|
255
|
-
b.style.cssText = base + "top:auto;bottom:16px;left:16px;right:auto;max-width:min(560px,calc(100vw - 32px));border-radius:8px;overflow:hidden;background:#
|
|
256
|
+
b.style.cssText = base + "top:auto;bottom:16px;left:16px;right:auto;max-width:min(560px,calc(100vw - 32px));border-radius:8px;overflow:hidden;background:#3247F2;padding:10px 14px 13px;font-size:13px;letter-spacing:.02em;text-align:left;line-height:1.3;";
|
|
256
257
|
b.innerHTML =
|
|
257
258
|
'<style>@keyframes fsPulse{0%,100%{opacity:1}50%{opacity:.25}}@keyframes fsSweep{0%{left:-30%}100%{left:100%}}' +
|
|
258
|
-
'#foldspace-dev-badge .fs-dot{display:inline-block;width:10px;height:10px;border-radius:50%;background:#
|
|
259
|
+
'#foldspace-dev-badge .fs-dot{display:inline-block;width:10px;height:10px;border-radius:50%;background:#FFD314;margin-right:10px;vertical-align:middle;animation:fsPulse 1s ease-in-out infinite}' +
|
|
259
260
|
'#foldspace-dev-badge .fs-line{position:absolute;left:0;right:0;bottom:0;height:3px;background:rgba(255,255,255,.15);overflow:hidden}' +
|
|
260
|
-
'#foldspace-dev-badge .fs-line::after{content:"";position:absolute;top:0;width:30%;height:100%;background:#
|
|
261
|
-
'<span class="fs-dot"></span>Foldspace is building your agent on this page. <b style="color:#
|
|
261
|
+
'#foldspace-dev-badge .fs-line::after{content:"";position:absolute;top:0;width:30%;height:100%;background:#FFD314;animation:fsSweep 1.4s linear infinite}</style>' +
|
|
262
|
+
'<span class="fs-dot"></span>Foldspace is building your agent on this page. <b style="color:#FFD314">Please leave this window alone</b> until the agent says it is ready.' +
|
|
262
263
|
(s.text ? " " + s.text.replace(/[<>&]/g, (c) => ({ "<": "<", ">": ">", "&": "&" })[c]) : "") +
|
|
263
264
|
'<span class="fs-line"></span>';
|
|
264
|
-
} else if (s.state === "ready") {
|
|
265
|
-
// Calm, and useful: the question to try, with a Copy button so nobody
|
|
266
|
-
// retypes it into the panel. The bar accepts clicks only on that button.
|
|
267
|
-
const esc = (t) => String(t).replace(/[<>&"]/g, (c) => ({ "<": "<", ">": ">", "&": "&", '"': """ })[c]);
|
|
268
|
-
const question = s.text ? esc(s.text) : "";
|
|
269
|
-
b.style.cssText = base + "top:auto;bottom:16px;left:16px;right:auto;max-width:min(560px,calc(100vw - 32px));border-radius:8px;overflow:hidden;background:#0F9D58;padding:10px 14px;font-size:13px;letter-spacing:.02em;text-align:left;line-height:1.3;pointer-events:none;";
|
|
270
|
-
b.innerHTML =
|
|
271
|
-
'<style>#foldspace-dev-badge .fs-copy{pointer-events:auto;margin-left:12px;padding:4px 10px;border:1px solid rgba(255,255,255,.7);border-radius:4px;background:transparent;color:#fff;font:inherit;font-size:12px;cursor:pointer;letter-spacing:0}' +
|
|
272
|
-
'#foldspace-dev-badge .fs-copy:hover{background:rgba(255,255,255,.15)}</style>' +
|
|
273
|
-
'<b>Ready - your turn.</b> ' +
|
|
274
|
-
(question ? 'Try asking: <span class="fs-q">' + question + '</span><button class="fs-copy" type="button">Copy</button>' : "Open your agent and try it.");
|
|
275
|
-
const btn = b.querySelector(".fs-copy");
|
|
276
|
-
if (btn) btn.addEventListener("click", () => {
|
|
277
|
-
const q = b.querySelector(".fs-q")?.textContent || "";
|
|
278
|
-
const done = () => { btn.textContent = "Copied"; setTimeout(() => { btn.textContent = "Copy"; }, 1500); };
|
|
279
|
-
if (navigator.clipboard?.writeText) navigator.clipboard.writeText(q).then(done, done);
|
|
280
|
-
else { const ta = document.createElement("textarea"); ta.value = q; document.body.appendChild(ta); ta.select(); try { document.execCommand("copy"); } catch (e) {} ta.remove(); done(); }
|
|
281
|
-
});
|
|
282
265
|
} else {
|
|
266
|
+
// "ready" and "label" both draw the small tag: their turn needs no bar -
|
|
267
|
+
// they may type whatever, whenever. Only the wait ever gets a pill.
|
|
283
268
|
b.style.cssText = base + "background:#3247F2;padding:5px 10px;border-bottom-right-radius:4px;";
|
|
284
269
|
b.textContent = LABEL;
|
|
285
270
|
}
|
package/bin/badge.mjs
CHANGED
|
@@ -2,14 +2,14 @@
|
|
|
2
2
|
/**
|
|
3
3
|
* foldspace badge <working|ready|label|off> [--text "<what to show>"]
|
|
4
4
|
*
|
|
5
|
-
* Sets the
|
|
6
|
-
* The agent and the human share that one Chrome window, so the
|
|
7
|
-
*
|
|
5
|
+
* Sets the pill the attach badge draws at the foot of the test window.
|
|
6
|
+
* The agent and the human share that one Chrome window, so the pill says
|
|
7
|
+
* when to wait:
|
|
8
8
|
* working - "Foldspace is building your agent on this page. Please leave
|
|
9
|
-
* this window alone until
|
|
10
|
-
* ready -
|
|
11
|
-
*
|
|
12
|
-
* label -
|
|
9
|
+
* this window alone until the agent says it is ready."
|
|
10
|
+
* ready - the pill goes away, the small corner tag comes back: their
|
|
11
|
+
* turn. No bar for that - they may type whatever, whenever.
|
|
12
|
+
* label - the small corner tag
|
|
13
13
|
* off - remove it
|
|
14
14
|
* The state is kept in the page's sessionStorage, so it survives navigation;
|
|
15
15
|
* attach's badge script redraws within a second. Nothing else on the page is
|
package/bin/cli.mjs
CHANGED
|
@@ -7,6 +7,7 @@ import { fileURLToPath } from "node:url";
|
|
|
7
7
|
import { runInit } from "../src/init.mjs";
|
|
8
8
|
import {
|
|
9
9
|
checkForUpdate,
|
|
10
|
+
gateForCommand,
|
|
10
11
|
nagIfOutdated,
|
|
11
12
|
resolveProjectDir,
|
|
12
13
|
runUpgrade,
|
|
@@ -54,6 +55,18 @@ function runScript(scriptName, args) {
|
|
|
54
55
|
});
|
|
55
56
|
}
|
|
56
57
|
|
|
58
|
+
// build and attach are where a stale or withdrawn harness does its damage, so
|
|
59
|
+
// they check before running. Returns false when the command must not run.
|
|
60
|
+
function passesVersionGate() {
|
|
61
|
+
const gate = gateForCommand(checkForUpdate(resolveProjectDir())) || {};
|
|
62
|
+
if (gate.message) console.error(`foldspace: ${gate.message}`);
|
|
63
|
+
if (gate.block) {
|
|
64
|
+
process.exitCode = 1;
|
|
65
|
+
return false;
|
|
66
|
+
}
|
|
67
|
+
return true;
|
|
68
|
+
}
|
|
69
|
+
|
|
57
70
|
function printHelp(topic, json) {
|
|
58
71
|
const document = helpDocument(registry, topic);
|
|
59
72
|
if (!document) {
|
|
@@ -184,8 +197,14 @@ if (!command || command === "--help" || command === "-h") {
|
|
|
184
197
|
try {
|
|
185
198
|
const definition = commandByName(command);
|
|
186
199
|
const normalized = normalizeCommandArgs(definition, args);
|
|
187
|
-
|
|
188
|
-
|
|
200
|
+
const control =
|
|
201
|
+
command === "attach" ? classifyAttachControl(normalized) : null;
|
|
202
|
+
const gated =
|
|
203
|
+
command === "build" ||
|
|
204
|
+
(command === "attach" && (!control || control === "daemon"));
|
|
205
|
+
if (gated && !passesVersionGate()) {
|
|
206
|
+
// gate already reported why
|
|
207
|
+
} else if (command === "attach") {
|
|
189
208
|
if (control) {
|
|
190
209
|
Promise.resolve()
|
|
191
210
|
.then(() =>
|
package/package.json
CHANGED
package/src/badge-core.mjs
CHANGED
|
@@ -10,8 +10,8 @@ export function badgePayload(state, text) {
|
|
|
10
10
|
}
|
|
11
11
|
const payload = { state: wanted };
|
|
12
12
|
if (text !== undefined) {
|
|
13
|
-
if (wanted !== "working"
|
|
14
|
-
throw new Error(`--text only applies to working
|
|
13
|
+
if (wanted !== "working") {
|
|
14
|
+
throw new Error(`--text only applies to working (got ${wanted}); their turn needs no bar.`);
|
|
15
15
|
}
|
|
16
16
|
const clean = String(text).replace(/\s+/g, " ").trim();
|
|
17
17
|
if (!clean) throw new Error("--text is empty.");
|
package/src/cli-registry.mjs
CHANGED
|
@@ -112,12 +112,13 @@ export const CLI_COMMANDS = Object.freeze([
|
|
|
112
112
|
usage: "foldspace build [--watch]",
|
|
113
113
|
risk: "local-write",
|
|
114
114
|
environment: "node",
|
|
115
|
-
environmentVariables: ["FOLDSPACE_PROJECT_DIR"],
|
|
115
|
+
environmentVariables: ["FOLDSPACE_PROJECT_DIR", "FOLDSPACE_SKIP_UPDATE_CHECK"],
|
|
116
116
|
capabilities: ["artifact.build"],
|
|
117
117
|
positionals: [],
|
|
118
118
|
options: [flag("--watch", "Rebuild when action source changes")],
|
|
119
119
|
prerequisites: ["agent/actions/index.ts"],
|
|
120
120
|
effects: [
|
|
121
|
+
"Checks the harness version first: behind prints one line, a withdrawn version refuses to run (FOLDSPACE_SKIP_UPDATE_CHECK=1 skips the check)",
|
|
121
122
|
"Runs foldspace lint first; errors skip bundling",
|
|
122
123
|
"Writes dist/index.js when lint has no errors",
|
|
123
124
|
],
|
|
@@ -201,6 +202,7 @@ export const CLI_COMMANDS = Object.freeze([
|
|
|
201
202
|
"FOLDSPACE_PROJECT_DIR",
|
|
202
203
|
"CDP_PORT",
|
|
203
204
|
"AGENT_API_NAME",
|
|
205
|
+
"FOLDSPACE_SKIP_UPDATE_CHECK",
|
|
204
206
|
],
|
|
205
207
|
capabilities: [
|
|
206
208
|
"browser.cdp",
|
|
@@ -260,6 +262,7 @@ export const CLI_COMMANDS = Object.freeze([
|
|
|
260
262
|
"sdkUrl in foldspace.dev.json when using --bootstrap",
|
|
261
263
|
],
|
|
262
264
|
effects: [
|
|
265
|
+
"Checks the harness version first, except --status and --stop: behind prints one line, a withdrawn version refuses to run (FOLDSPACE_SKIP_UPDATE_CHECK=1 skips the check)",
|
|
263
266
|
"May reload and instrument matching target pages",
|
|
264
267
|
"Refuses a Chrome that is not the profile inject launched",
|
|
265
268
|
"Test mode is off unless --test-mode is passed: conversations appear in the dashboard",
|
|
@@ -321,7 +324,7 @@ export const CLI_COMMANDS = Object.freeze([
|
|
|
321
324
|
name: "badge",
|
|
322
325
|
entry: "badge.mjs",
|
|
323
326
|
group: "verify",
|
|
324
|
-
summary: "Set the
|
|
327
|
+
summary: "Set the pill at the foot of the test window: working (leave it alone), ready (pill away), label or off",
|
|
325
328
|
usage: 'foldspace badge <working|ready|label|off> [--text "<what to show>"]',
|
|
326
329
|
risk: "browser-session",
|
|
327
330
|
environment: "local-chrome",
|
|
@@ -332,18 +335,18 @@ export const CLI_COMMANDS = Object.freeze([
|
|
|
332
335
|
name: "state",
|
|
333
336
|
required: true,
|
|
334
337
|
description:
|
|
335
|
-
"working: the agent is building here, leave the window alone | ready: the
|
|
338
|
+
"working: the agent is building here, leave the window alone | ready: the pill goes away, their turn | label: the small corner tag | off: remove it",
|
|
336
339
|
}),
|
|
337
340
|
],
|
|
338
341
|
options: [
|
|
339
|
-
value("--text", "text", "working
|
|
342
|
+
value("--text", "text", "working only: one extra line under the ask (up to 160 characters)"),
|
|
340
343
|
],
|
|
341
344
|
prerequisites: ["foldspace attach is running (it draws the badge)"],
|
|
342
345
|
effects: [
|
|
343
346
|
"Writes one key to the test window's sessionStorage; touches nothing else on the page",
|
|
344
|
-
"The
|
|
347
|
+
"The pill survives navigation inside the test window",
|
|
345
348
|
],
|
|
346
|
-
next: ["
|
|
349
|
+
next: ["Say in chat whose turn it is and what to try"],
|
|
347
350
|
}),
|
|
348
351
|
Object.freeze({
|
|
349
352
|
name: "deploy",
|
|
@@ -464,6 +467,7 @@ export function createCliRegistry({ packageName, packageVersion, env = process.e
|
|
|
464
467
|
"attach diagnostics are attach-internal; interpret them from the lifecycle log, not as CLI commands.",
|
|
465
468
|
"Coding agents should run attach --daemon; foreground attach is for humans watching the terminal.",
|
|
466
469
|
"If update.outdated is true, ask the user before foldspace upgrade --yes.",
|
|
470
|
+
"If update.broken is true, this version was withdrawn and build and attach refuse to run. Ask the user to upgrade; set FOLDSPACE_SKIP_UPDATE_CHECK=1 only if they decline.",
|
|
467
471
|
"observe, ask and run are read-only second clients on the test window and work while attach runs.",
|
|
468
472
|
"There is no customer-run publication step: putting actions in front of end users is done with Foldspace's team.",
|
|
469
473
|
],
|
package/src/upgrade.mjs
CHANGED
|
@@ -227,10 +227,10 @@ function readCache(projectDir, now) {
|
|
|
227
227
|
}
|
|
228
228
|
}
|
|
229
229
|
|
|
230
|
-
function writeCache(projectDir,
|
|
230
|
+
function writeCache(projectDir, entry, now) {
|
|
231
231
|
const dir = path.dirname(cachePath(projectDir));
|
|
232
232
|
fs.mkdirSync(dir, { recursive: true });
|
|
233
|
-
writeJson(cachePath(projectDir), { checkedAt: now,
|
|
233
|
+
writeJson(cachePath(projectDir), { checkedAt: now, ...entry });
|
|
234
234
|
}
|
|
235
235
|
|
|
236
236
|
export function fetchLatestFromNpm(options = {}) {
|
|
@@ -244,7 +244,27 @@ export function fetchLatestFromNpm(options = {}) {
|
|
|
244
244
|
return normalizeVersion(result.stdout);
|
|
245
245
|
}
|
|
246
246
|
|
|
247
|
+
/**
|
|
248
|
+
* A version we have withdrawn is marked with `npm deprecate`, so the registry
|
|
249
|
+
* is the list of known-broken versions and an old install can still read it.
|
|
250
|
+
* Returns the deprecation message, "" when the version is not deprecated, or
|
|
251
|
+
* null when the registry could not be asked.
|
|
252
|
+
*/
|
|
253
|
+
export function fetchDeprecationFromNpm(version, options = {}) {
|
|
254
|
+
const spawn = options.spawnSyncFn || spawnSync;
|
|
255
|
+
const result = spawn("npm", ["view", `${HARNESS_PACKAGE}@${version}`, "deprecated"], {
|
|
256
|
+
encoding: "utf8",
|
|
257
|
+
timeout: options.timeoutMs ?? NPM_VIEW_TIMEOUT_MS,
|
|
258
|
+
env: options.env || process.env,
|
|
259
|
+
});
|
|
260
|
+
if (result.error || result.status !== 0) return null;
|
|
261
|
+
return String(result.stdout || "").trim();
|
|
262
|
+
}
|
|
263
|
+
|
|
247
264
|
function nextAction(report) {
|
|
265
|
+
if (report.broken) {
|
|
266
|
+
return `This harness version was withdrawn: ${report.deprecated}. ${UPGRADE_NEXT}`;
|
|
267
|
+
}
|
|
248
268
|
if (report.status === "outdated") {
|
|
249
269
|
return report.instructionsImport === false
|
|
250
270
|
? `${UPGRADE_NEXT}. If CLAUDE.md is still a snapshot, also pass --refresh-instructions`
|
|
@@ -275,7 +295,11 @@ export function checkForUpdate(projectDir, options = {}) {
|
|
|
275
295
|
(options.skipCheck !== false && env.FOLDSPACE_SKIP_UPDATE_CHECK === "1");
|
|
276
296
|
const consumer = !harnessSource && Boolean(pinned || installed);
|
|
277
297
|
|
|
298
|
+
// The version that actually runs is the installed one; the pin is a fallback
|
|
299
|
+
// for a project that has not been installed yet.
|
|
300
|
+
const running = installed || pinned;
|
|
278
301
|
let latest = null;
|
|
302
|
+
let deprecated = null;
|
|
279
303
|
let status = "current";
|
|
280
304
|
|
|
281
305
|
if (!consumer) {
|
|
@@ -284,16 +308,30 @@ export function checkForUpdate(projectDir, options = {}) {
|
|
|
284
308
|
status = "check_skipped";
|
|
285
309
|
} else {
|
|
286
310
|
const cached = options.cache === false ? null : readCache(projectDir, now);
|
|
287
|
-
|
|
311
|
+
const cachedDeprecation =
|
|
312
|
+
cached?.deprecation?.version === running ? cached.deprecation : null;
|
|
313
|
+
if (cached?.latest && cachedDeprecation) {
|
|
288
314
|
latest = cached.latest;
|
|
315
|
+
deprecated = cachedDeprecation.message || null;
|
|
289
316
|
} else {
|
|
290
317
|
const fetched =
|
|
291
318
|
typeof options.fetchLatest === "function"
|
|
292
319
|
? options.fetchLatest()
|
|
293
320
|
: fetchLatestFromNpm(options);
|
|
294
321
|
latest = normalizeVersion(fetched);
|
|
322
|
+
const message =
|
|
323
|
+
typeof options.fetchDeprecation === "function"
|
|
324
|
+
? options.fetchDeprecation(running)
|
|
325
|
+
: fetchDeprecationFromNpm(running, options);
|
|
326
|
+
deprecated = message || null;
|
|
295
327
|
if (latest && options.cache !== false) {
|
|
296
|
-
|
|
328
|
+
const entry = { latest };
|
|
329
|
+
// Only cache an answer the registry actually gave, so an offline
|
|
330
|
+
// run is asked again next time instead of remembered as "fine".
|
|
331
|
+
if (typeof message === "string") {
|
|
332
|
+
entry.deprecation = { version: running, message };
|
|
333
|
+
}
|
|
334
|
+
writeCache(projectDir, entry, now);
|
|
297
335
|
}
|
|
298
336
|
}
|
|
299
337
|
if (!latest) {
|
|
@@ -311,6 +349,8 @@ export function checkForUpdate(projectDir, options = {}) {
|
|
|
311
349
|
installed,
|
|
312
350
|
latest,
|
|
313
351
|
outdated: status === "outdated",
|
|
352
|
+
deprecated,
|
|
353
|
+
broken: Boolean(deprecated),
|
|
314
354
|
status,
|
|
315
355
|
instructionsImport,
|
|
316
356
|
next: null,
|
|
@@ -319,6 +359,33 @@ export function checkForUpdate(projectDir, options = {}) {
|
|
|
319
359
|
return report;
|
|
320
360
|
}
|
|
321
361
|
|
|
362
|
+
/**
|
|
363
|
+
* What `build` and `attach` do with an update report before they run:
|
|
364
|
+
* `block: true` stops the command, `message` (if any) is printed to stderr.
|
|
365
|
+
* Never upgrades - that stays the user's decision (CLAUDE.md, step 1).
|
|
366
|
+
* The escape hatch is FOLDSPACE_SKIP_UPDATE_CHECK=1: the report comes back
|
|
367
|
+
* check_skipped, so nothing here blocks.
|
|
368
|
+
*/
|
|
369
|
+
export function gateForCommand(report) {
|
|
370
|
+
// A check that could not run never stops a build.
|
|
371
|
+
if (!report) return { block: false, message: null };
|
|
372
|
+
if (report.broken) {
|
|
373
|
+
return {
|
|
374
|
+
block: true,
|
|
375
|
+
message:
|
|
376
|
+
`${HARNESS_PACKAGE} ${report.installed || report.pinned} was withdrawn: ${report.deprecated}. ` +
|
|
377
|
+
`${UPGRADE_NEXT}. If the user declines, rerun with FOLDSPACE_SKIP_UPDATE_CHECK=1 to build on this version anyway.`,
|
|
378
|
+
};
|
|
379
|
+
}
|
|
380
|
+
if (report.outdated) {
|
|
381
|
+
return {
|
|
382
|
+
block: false,
|
|
383
|
+
message: `${HARNESS_PACKAGE} ${report.installed || report.pinned} is behind ${report.latest}. ${UPGRADE_NEXT}.`,
|
|
384
|
+
};
|
|
385
|
+
}
|
|
386
|
+
return { block: false, message: null };
|
|
387
|
+
}
|
|
388
|
+
|
|
322
389
|
export function writeExactPin(projectDir, version) {
|
|
323
390
|
const manifestPath = path.join(projectDir, "package.json");
|
|
324
391
|
const manifest = readPackageManifest(projectDir);
|
|
@@ -14,8 +14,10 @@ How to re-run any probe below:
|
|
|
14
14
|
|
|
15
15
|
```bash
|
|
16
16
|
npm run inject
|
|
17
|
-
# Sign in
|
|
18
|
-
#
|
|
17
|
+
# Sign in, then read the running app with foldspace observe. It reads the
|
|
18
|
+
# window you signed in to, and works while foldspace attach runs.
|
|
19
|
+
npx foldspace observe screen --click "<a label from observe menu>" --match "<words for the data>"
|
|
20
|
+
npx foldspace observe read "<the GET path screen returned>"
|
|
19
21
|
```
|
|
20
22
|
|
|
21
23
|
Start URL: `{{START_URL}}`
|