staysfixed 0.7.0 → 0.7.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/CHANGELOG.md CHANGED
@@ -4,6 +4,43 @@ All notable changes to this project are recorded here. The format follows
4
4
  [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and the version
5
5
  numbers follow [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
6
6
 
7
+ ## [0.7.2] — 2026-08-30
8
+
9
+ Found the same way as 0.7.1, one step further along: install from npm into an
10
+ empty folder, run `init`, then run `doctor` on what `init` just wrote.
11
+
12
+ ### Fixed
13
+
14
+ - **`doctor` read the commented-out examples in a settings file as settings.**
15
+ `staysfixed init` comments out every option that does not apply to your
16
+ project rather than leaving it out, so nothing is hidden from the person
17
+ reading the file — and `doctor` searched the raw text. On a folder holding one
18
+ script and nothing else it announced *"Electron desktop apps: **Covered.** It
19
+ opens release/mac-arm64/Your App.app"*, and an Android app beside it, both read
20
+ out of comments and both false.
21
+
22
+ A surface reported as covered when nothing will ever be walked on it is the
23
+ worst answer this tool can give, because every clean result after it is
24
+ believed. Comments are taken away before anything is read out of the file now,
25
+ with strings respected so an address keeps its two slashes — and the file is
26
+ still never loaded, because a settings file may be JavaScript and doctor must
27
+ not run somebody's code to answer a question about their machine.
28
+
29
+ ## [0.7.1] — 2026-08-30
30
+
31
+ Found by installing 0.7.0 from npm into an empty folder and running it the way a
32
+ stranger would, which is the only way this was ever going to be found.
33
+
34
+ ### Fixed
35
+
36
+ - **`init` asked for a build step on a script that runs straight from source.** A
37
+ plain Node command-line tool is recorded as "not built", because there is
38
+ nothing to build — and that was read as "it has not been built yet". So a fresh
39
+ install told its owner to go and name the command that builds a file sitting
40
+ right there, in the same breath as offering to run it. Being sent shopping for
41
+ nothing is the fastest way to make somebody stop reading the page, and this
42
+ project's README promises it never happens. A test holds it shut.
43
+
7
44
  ## [0.7.0] — 2026-08-30
8
45
 
9
46
  The release where the front door was found to be locked.
@@ -81,6 +118,11 @@ printed, which is the only way this kind of mistake is ever found.
81
118
  - **It reported Docker as present because the command was on the path**, on a
82
119
  machine where Docker Desktop was shut and nothing it promises would have worked.
83
120
  It asks the engine for its version now.
121
+ - **It said an iPhone app "cannot be done here at all" on a Mac with Xcode and
122
+ three simulator runtimes on it.** The real reason was that this repository has
123
+ no iPhone app in it, which is not a limit of the machine. Those two reasons are
124
+ two different sentences now, and the object carries
125
+ `surfaces[].notInThisProject` so an agent can tell them apart too.
84
126
  - No probe in `doctor` uses a shell variable or a loop any more. One machine in
85
127
  this config reaches Windows through an OpenSSH server that hands the command down
86
128
  through a second shell, and every `$p` is expanded to nothing before the shell
package/README.md CHANGED
@@ -364,6 +364,8 @@ to ask the question a different way.
364
364
  | Ask a machine `command -v powershell.exe` over ssh to find out whether Windows sits behind it — a question that answers "no" on a machine with Windows right there, because that path is added by an interactive login shell and ssh does not run one. It also read a **refusal** as an answer, so `github.com` was listed as a machine to run checks on, and as a Windows desktop | Asks the filesystem for the three places PowerShell actually lives, using the one list the code that later drives it uses. Reads standard output only, and matches the whole line, so a host that quotes your command back cannot answer for itself |
365
365
  | Name only the first eight machines in an ssh config and drop the rest without a word | Dials sixteen, and anything past that is named as not dialled rather than left out |
366
366
  | Report Docker as present because the command is on the path, on a Mac where Docker Desktop is shut and nothing it promises would work | Asks the engine for its version, and says "installed but not answering" when that is the truth |
367
+ | Read the **commented-out examples** in a settings file as settings. `staysfixed init` comments out every option that does not apply to your project, so nothing is hidden from you — and doctor searched the raw text. On a folder holding one script it announced "Electron desktop apps: **Covered.** It opens release/mac-arm64/Your App.app", and an Android app beside it. A surface called covered when nothing will ever be walked on it is the worst answer this tool can give | Comments are taken away before anything is read out of the file, with strings respected so an address keeps its two slashes. The file is still never loaded — doctor must not run your code to answer a question about your machine |
368
+ | Ask for a build step on a plain command-line tool. A script that runs from source is recorded as "not built", because there is nothing to build, and that was read as "it has not been built yet" — so a fresh install told its owner to name the command that builds a file sitting right there, in the same breath as offering to run it | Nothing is asked for when there is nothing to build, or when a command to run it has already been worked out |
367
369
 
368
370
  ---
369
371
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "staysfixed",
3
- "version": "0.7.0",
3
+ "version": "0.7.2",
4
4
  "description": "Prove that what already worked still works after an agent changed the code. Picture checks, guards for fixed bugs, a pre-release walkthrough, and known-good markers — as a CLI and as an MCP server.",
5
5
  "type": "module",
6
6
  "license": "MIT",
package/src/v2/doctor.js CHANGED
@@ -668,6 +668,59 @@ function hasModule(cwd, name) {
668
668
  }
669
669
  }
670
670
 
671
+ /**
672
+ * The settings file with everything commented out taken away.
673
+ *
674
+ * `staysfixed init` writes a settings file whose whole point is that the options which do
675
+ * NOT apply to your project are commented out rather than left out, so nothing is hidden
676
+ * from the person reading it. Doctor reads that file as TEXT — never by loading it, because
677
+ * a settings file may be JavaScript and doctor must not run somebody's code to answer a
678
+ * question about their machine — and it used to search the raw text.
679
+ *
680
+ * Which means it found the examples. On a folder containing one `cli.js` and nothing else,
681
+ * doctor announced "Electron desktop apps: Covered. It opens release/mac-arm64/Your App.app"
682
+ * and "An Android app is here", both read out of commented-out lines, and both false. A
683
+ * surface reported as covered when nothing will ever be walked on it is the worst answer
684
+ * this tool can give.
685
+ *
686
+ * Strings are respected, so an address like "http://localhost:3000" survives: the two
687
+ * slashes inside it are not the start of a comment.
688
+ *
689
+ * @param {string} text
690
+ * @returns {string} The same text with comments blanked, line numbering unchanged.
691
+ */
692
+ export function withoutComments(text) {
693
+ let out = '';
694
+ /** @type {"code"|"line"|"block"|"'"|'"'|'`'} */
695
+ let mode = 'code';
696
+ for (let i = 0; i < text.length; i += 1) {
697
+ const c = text[i];
698
+ const next = text[i + 1];
699
+ if (mode === 'code') {
700
+ if (c === '/' && next === '/') { mode = 'line'; out += ' '; i += 1; continue; }
701
+ if (c === '/' && next === '*') { mode = 'block'; out += ' '; i += 1; continue; }
702
+ if (c === "'" || c === '"' || c === '`') mode = /** @type {any} */ (c);
703
+ out += c;
704
+ continue;
705
+ }
706
+ if (mode === 'line') {
707
+ if (c === '\n') { mode = 'code'; out += c; continue; }
708
+ out += ' ';
709
+ continue;
710
+ }
711
+ if (mode === 'block') {
712
+ if (c === '*' && next === '/') { mode = 'code'; out += ' '; i += 1; continue; }
713
+ out += c === '\n' ? c : ' ';
714
+ continue;
715
+ }
716
+ // Inside a string. A backslash escapes whatever comes next, quote included.
717
+ if (c === '\\') { out += c + (next ?? ''); i += 1; continue; }
718
+ if (c === mode) mode = 'code';
719
+ out += c;
720
+ }
721
+ return out;
722
+ }
723
+
671
724
  /**
672
725
  * Is there a desktop app in this project to check?
673
726
  *
@@ -687,7 +740,10 @@ function findDesktopApp(cwd) {
687
740
  const configFile = findConfigFile(cwd);
688
741
  if (configFile) {
689
742
  try {
690
- const text = readFileSync(configFile, 'utf8');
743
+ // Comments taken away first. See `withoutComments`: the examples in a settings file
744
+ // are commented out on purpose, and reading them as settings reported a desktop app
745
+ // in a folder that contains one script.
746
+ const text = withoutComments(readFileSync(configFile, 'utf8'));
691
747
  const named = /["']?binary["']?\s*:\s*["'`]([^"'`]+)["'`]/.exec(text);
692
748
  if (named) return { where: named[1], how: 'your settings name it under app.binary' };
693
749
  if (/["']?kind["']?\s*:\s*["'`]electron["'`]/.test(text)) {
@@ -761,7 +817,9 @@ async function phoneApps(root, configFile) {
761
817
  let settings = '';
762
818
  if (configFile) {
763
819
  try {
764
- settings = readFileSync(configFile, 'utf8');
820
+ // Comments taken away first, for the same reason `findDesktopApp` does it: a
821
+ // commented-out `apk:` line is an example, not an Android app.
822
+ settings = withoutComments(readFileSync(configFile, 'utf8'));
765
823
  } catch {
766
824
  settings = '';
767
825
  }
package/src/v2/init.js CHANGED
@@ -432,7 +432,17 @@ function productNeeds(product, project) {
432
432
  // A command-line program that has to be built before it can be run. This is what a product
433
433
  // nothing in package.json names looks like on a fresh clone: the source is there, the
434
434
  // program is real, and the file that would be run does not exist yet.
435
- if (product.kind === 'cli' && !product.built.found) {
435
+ //
436
+ // `built.found === false` is NOT that on its own, and reading it as though it were sent a
437
+ // plain Node command-line tool's owner shopping for a build step it does not have. A
438
+ // script that runs straight from source is recorded as `found: false, how: "nothing to
439
+ // build — it runs from source"`, and this file used to ask a person to "name the command
440
+ // that builds it" about a file that was sitting right there and that the very same run
441
+ // had already worked out how to run. Two signals rule it out, and either is enough: the
442
+ // detector saying there is nothing to build, and a command already worked out for it.
443
+ const nothingToBuild = /nothing to build/i.test(String(product.built.how ?? ''));
444
+ const alreadyRunnable = Array.isArray(suggest.commands) && suggest.commands.length > 0;
445
+ if (product.kind === 'cli' && !product.built.found && !nothingToBuild && !alreadyRunnable) {
436
446
  const build = typeof suggest.buildWith === 'string' ? String(suggest.buildWith) : null;
437
447
  needs.push({
438
448
  what: `${product.name}, built`,