@mutmutco/installer-launcher 0.1.3 → 0.1.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/launcher.js CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  // src/index.ts
4
4
  import { spawnSync as spawnSync2 } from "node:child_process";
5
- import { existsSync as existsSync4, readFileSync as readFileSync4 } from "node:fs";
5
+ import { existsSync as existsSync4, readFileSync as readFileSync4, statSync } from "node:fs";
6
6
  import { dirname as dirname2, join as join4, resolve } from "node:path";
7
7
  import { fileURLToPath, pathToFileURL as pathToFileURL2 } from "node:url";
8
8
 
@@ -121,9 +121,10 @@ function createFace({ product, color = false, columns }) {
121
121
  const content = Math.min(width - 6, Math.max(...body.map(visibleWidth)));
122
122
  const rule = paint(identity.accent, GLYPH.rule.repeat(content + 4));
123
123
  const frame = (left, right) => `${paint(identity.accent, left)}${rule}${paint(identity.accent, right)}`;
124
+ const bodyRow = (line) => line.startsWith(GLYPH.check) ? `${paint(PALETTE.green, GLYPH.check)}${line.slice(1)}` : line;
124
125
  return [
125
126
  frame(GLYPH.boxTop, GLYPH.boxTopEnd),
126
- ...body.map((line) => `${paint(identity.accent, GLYPH.bar)} ${line}${" ".repeat(Math.max(0, content - visibleWidth(line)))} ${paint(identity.accent, GLYPH.bar)}`),
127
+ ...body.map((line) => `${paint(identity.accent, GLYPH.bar)} ${bodyRow(line)}${" ".repeat(Math.max(0, content - visibleWidth(line)))} ${paint(identity.accent, GLYPH.bar)}`),
127
128
  frame(GLYPH.boxBottom, GLYPH.boxBottomEnd)
128
129
  ];
129
130
  };
@@ -132,6 +133,8 @@ function createFace({ product, color = false, columns }) {
132
133
  return { identity, width, welcome, step, relay, receipt, signOff, refusal, paint };
133
134
  }
134
135
  var RELAY_INDENT = " ".repeat(TITLE_COLUMN - 1);
136
+ var FRAMES = ["\u25D2", "\u25D0", "\u25D3", "\u25D1"];
137
+ var SPINNER_FRAMES = Object.freeze([...FRAMES]);
135
138
  var ALLOWED = new Set(Object.values(GLYPH));
136
139
 
137
140
  // src/autoupdate.ts
@@ -860,7 +863,7 @@ function wipeProductDir(dir) {
860
863
  }
861
864
 
862
865
  // src/index.ts
863
- var LAUNCHER_VERSION = true ? "0.1.3" : readVersionFromPackage();
866
+ var LAUNCHER_VERSION = true ? "0.1.7" : readVersionFromPackage();
864
867
  function defaultPrint(message) {
865
868
  process.stdout.write(`${message}
866
869
  `);
@@ -1072,19 +1075,58 @@ function needsShell(command) {
1072
1075
  function quoteForShell(arg) {
1073
1076
  return /[\s"&|<>^%]/.test(arg) ? `"${arg.replace(/"/g, '\\"')}"` : arg;
1074
1077
  }
1075
- function defaultRunEntry(entry, cwd) {
1078
+ var SUPPORTED_PROGRESS_PROTOCOLS = /* @__PURE__ */ new Set([1]);
1079
+ function parseProgress(raw) {
1080
+ if (raw === null || raw === void 0) return [];
1081
+ const progress = [];
1082
+ for (const line of raw.toString().split(/\r?\n/)) {
1083
+ if (!line) continue;
1084
+ try {
1085
+ const message = JSON.parse(line);
1086
+ if (!SUPPORTED_PROGRESS_PROTOCOLS.has(message.v) || typeof message.step !== "string") continue;
1087
+ const state = message.state ?? "ok";
1088
+ if (state !== "ok" && state !== "fail" && state !== "note") continue;
1089
+ if (message.ms !== void 0 && typeof message.ms !== "number") continue;
1090
+ progress.push({
1091
+ step: message.step,
1092
+ state,
1093
+ ...typeof message.ms === "number" ? { ms: message.ms } : {}
1094
+ });
1095
+ } catch {
1096
+ }
1097
+ }
1098
+ return progress;
1099
+ }
1100
+ function defaultRunEntry(entry, cwd, env) {
1076
1101
  const [command, ...args] = entry;
1077
1102
  const shell = needsShell(command);
1078
1103
  const commandLine = shell ? [command, ...args].map(quoteForShell).join(" ") : command;
1079
- const result = spawnSync2(commandLine, shell ? [] : args, {
1104
+ const spawn2 = (progress2) => spawnSync2(commandLine, shell ? [] : args, {
1080
1105
  cwd,
1081
- stdio: "inherit",
1106
+ stdio: progress2 ? ["inherit", "inherit", "inherit", "pipe"] : "inherit",
1082
1107
  shell,
1108
+ env: progress2 ? { ...env ?? process.env, MM_PROGRESS_FD: "3", MM_PROGRESS_PROTOCOL: "1" } : env ?? process.env,
1083
1109
  windowsHide: true
1084
1110
  });
1111
+ let result;
1112
+ if (process.platform === "win32" && shell) {
1113
+ result = spawn2(false);
1114
+ } else {
1115
+ try {
1116
+ result = spawn2(true);
1117
+ } catch {
1118
+ result = spawn2(false);
1119
+ }
1120
+ }
1085
1121
  if (result.error) return { ok: false, error: result.error.message };
1086
1122
  const code = typeof result.status === "number" ? result.status : void 0;
1087
- return { ok: result.status === 0, error: result.status === 0 ? void 0 : `exit code ${result.status}`, code };
1123
+ const progress = parseProgress(result.output?.[3]);
1124
+ return {
1125
+ ok: result.status === 0,
1126
+ error: result.status === 0 ? void 0 : `exit code ${result.status}`,
1127
+ code,
1128
+ ...progress.length > 0 ? { progress } : {}
1129
+ };
1088
1130
  }
1089
1131
  function faceProduct(config) {
1090
1132
  const known = { mmi: "mmi-hub", jerv: "jerv-hub" };
@@ -1092,10 +1134,11 @@ function faceProduct(config) {
1092
1134
  }
1093
1135
  function faceFor(config, options) {
1094
1136
  const tty = options.tty ?? Boolean(process.stdout.isTTY);
1137
+ if (!tty) return null;
1095
1138
  try {
1096
1139
  return createFace({
1097
1140
  product: faceProduct(config),
1098
- color: tty && !process.env.NO_COLOR && process.env.TERM !== "dumb",
1141
+ color: !process.env.NO_COLOR && process.env.TERM !== "dumb",
1099
1142
  columns: process.stdout.columns
1100
1143
  });
1101
1144
  } catch {
@@ -1105,9 +1148,13 @@ function faceFor(config, options) {
1105
1148
  function since(start) {
1106
1149
  return (Date.now() - start) / 1e3;
1107
1150
  }
1151
+ function outerConsoleOwnsOutcome() {
1152
+ return process.env.MM_OUTER_CONSOLE === "1";
1153
+ }
1108
1154
  function printReceipt(face, config, print, ready, lines) {
1109
1155
  const name = face ? face.identity.name : config.product;
1110
1156
  const headline = ready ? `${name} is ready.` : `${name} is installed, but not ready yet.`;
1157
+ if (ready && outerConsoleOwnsOutcome()) return;
1111
1158
  if (!face) {
1112
1159
  print(headline);
1113
1160
  for (const line of lines) print(line.trim());
@@ -1120,6 +1167,11 @@ function printReceipt(face, config, print, ready, lines) {
1120
1167
  function printStep(face, print, title, seconds, kind = "ok") {
1121
1168
  print(face ? face.step(title, seconds, kind) : title);
1122
1169
  }
1170
+ function printProgress(face, print, progress) {
1171
+ for (const message of progress ?? []) {
1172
+ printStep(face, print, message.step, message.ms === void 0 ? null : message.ms / 1e3, message.state);
1173
+ }
1174
+ }
1123
1175
  async function doInstall(config, dir, options, print) {
1124
1176
  const fetchImpl = options.fetchImpl ?? fetch;
1125
1177
  const face = faceFor(config, options);
@@ -1128,13 +1180,13 @@ async function doInstall(config, dir, options, print) {
1128
1180
  const manifest = await fetchVerifiedManifest(config, accessToken, fetchImpl);
1129
1181
  await downloadAndUnpack(config, dir, manifest, accessToken, { fetchImpl });
1130
1182
  writeState(dir, { version: manifest.version, updatedAt: (/* @__PURE__ */ new Date()).toISOString() });
1131
- printStep(face, print, `installed ${config.product} ${manifest.version} into ${dir}`, since(started));
1183
+ printStep(face, print, `installed ${config.product} ${manifest.version}`, since(started));
1132
1184
  return finishLastMile(config, dir, options, print, face, manifest.version);
1133
1185
  }
1134
1186
  async function doUpdate(config, dir, options, print) {
1135
1187
  const fetchImpl = options.fetchImpl ?? fetch;
1136
1188
  const face = faceFor(config, options);
1137
- if (face) for (const line of face.welcome()) print(line);
1189
+ if (face && !outerConsoleOwnsOutcome()) for (const line of face.welcome()) print(line);
1138
1190
  const accessToken = await fetchAccessTokenOrLogin(config, dir, options, print);
1139
1191
  const started = Date.now();
1140
1192
  const manifest = await fetchVerifiedManifest(config, accessToken, fetchImpl);
@@ -1148,32 +1200,69 @@ async function doUpdate(config, dir, options, print) {
1148
1200
  printStep(face, print, `updated ${config.product} to ${manifest.version}`, since(started));
1149
1201
  return finishLastMile(config, dir, options, print, face, manifest.version);
1150
1202
  }
1203
+ function payloadEnv(dir) {
1204
+ const stored = readTokens(dir);
1205
+ if (!stored?.accessToken) return void 0;
1206
+ return { ...process.env, MM_INSTALLER_TOKEN: stored.accessToken };
1207
+ }
1208
+ var NEVER_A_PROGRAM = /\.(tgz|tar|gz|bz2|xz|zip|json|txt|md|lock)$/i;
1209
+ function payloadFileAsCommand(entry, payload) {
1210
+ const first = entry[0];
1211
+ if (!first || first === "$self") return null;
1212
+ if (first.includes("/") || first.includes("\\")) return null;
1213
+ const candidate = join4(payload, first);
1214
+ if (!existsSync4(candidate)) return null;
1215
+ if (NEVER_A_PROGRAM.test(first)) return first;
1216
+ if (process.platform === "win32") return null;
1217
+ try {
1218
+ return (statSync(candidate).mode & 73) === 0 ? first : null;
1219
+ } catch {
1220
+ return null;
1221
+ }
1222
+ }
1151
1223
  function finishLastMile(config, dir, options, print, face, version) {
1152
1224
  const entry = readPayloadEntry(dir);
1153
1225
  const payload = payloadDir(dir);
1154
1226
  if (!entry) {
1155
1227
  printReceipt(face, config, print, true, [
1156
- ` Installed ${version} into ${dir}`,
1157
- ` next step: run ${join4(payload, config.binName)} to start ${config.product}.`
1228
+ `Installed ${version} into ${dir}`,
1229
+ `next step: run ${join4(payload, config.binName)} to start ${config.product}.`
1158
1230
  ]);
1159
1231
  return 0;
1160
1232
  }
1161
1233
  const command = resolveEntry(entry);
1234
+ const dataFile = payloadFileAsCommand(entry, payload);
1235
+ if (dataFile) {
1236
+ printStep(face, print, `The payload names ${dataFile} as its command, but that is a file, not a program`, null, "fail");
1237
+ printReceipt(face, config, print, false, [
1238
+ `Downloaded ${version} into ${dir}`,
1239
+ `This payload was built wrong: its entry must be a command, not one of its own files.`,
1240
+ `Nothing on this machine can finish it \u2014 report it to the ${config.product} maintainers.`
1241
+ ]);
1242
+ return 2;
1243
+ }
1162
1244
  const started = Date.now();
1163
- const result = (options.runEntry ?? defaultRunEntry)(command, payload);
1245
+ const result = (options.runEntry ?? defaultRunEntry)(command, payload, payloadEnv(dir));
1246
+ printProgress(face, print, result.progress);
1164
1247
  if (result.ok) {
1165
1248
  printStep(face, print, "Armed this machine", since(started));
1166
1249
  printReceipt(face, config, print, true, [
1167
- ` Installed ${version} into ${dir}`,
1168
- ` Check health any time: ${config.binName} doctor`
1250
+ `Installed ${version} into ${dir}`,
1251
+ `Check health any time: ${config.binName} doctor`
1169
1252
  ]);
1170
1253
  return 0;
1171
1254
  }
1172
1255
  const code = result.code ?? 1;
1173
- printStep(face, print, `Arming this machine did not finish (exit code ${code})`, null, "fail");
1256
+ printStep(
1257
+ face,
1258
+ print,
1259
+ result.code === void 0 ? `Arming this machine could not start: ${result.error ?? `could not run ${command[0]}`}` : `Arming this machine did not finish (exit code ${code})`,
1260
+ null,
1261
+ "fail"
1262
+ );
1174
1263
  printReceipt(face, config, print, false, [
1175
- ` Downloaded ${version} into ${dir}`,
1176
- ` Finish it with: (cd ${payload} && ${command.join(" ")})`
1264
+ `Downloaded ${version} into ${dir}`,
1265
+ `Finish it with: (cd ${payload} && ${command.join(" ")})`
1177
1266
  ]);
1178
1267
  return code;
1179
1268
  }
@@ -1191,7 +1280,7 @@ function doForward(config, dir, options, command, argv, print) {
1191
1280
  return 2;
1192
1281
  }
1193
1282
  const forwarded = [...resolveEntry(target), ...argv];
1194
- const result = (options.runEntry ?? defaultRunEntry)(forwarded, payloadDir(dir));
1283
+ const result = (options.runEntry ?? defaultRunEntry)(forwarded, payloadDir(dir), payloadEnv(dir));
1195
1284
  if (!result.ok && result.code === void 0) {
1196
1285
  print(`failed: ${result.error ?? `could not run ${forwarded[0]}`}`);
1197
1286
  return 1;
@@ -1242,7 +1331,7 @@ function doDoctor(config, dir, options, print) {
1242
1331
  const verbs = readPayloadVerbs(dir);
1243
1332
  const chains = target !== null && (verbs === "*" || Array.isArray(verbs) && verbs.includes("doctor"));
1244
1333
  if (!chains) return 0;
1245
- const result = (options.runEntry ?? defaultRunEntry)([...resolveEntry(target), "doctor"], payloadDir(dir));
1334
+ const result = (options.runEntry ?? defaultRunEntry)([...resolveEntry(target), "doctor"], payloadDir(dir), payloadEnv(dir));
1246
1335
  return result.code ?? (result.ok ? 0 : 1);
1247
1336
  }
1248
1337
  function doLauncherDoctor(config, dir, options, print) {
@@ -151,9 +151,10 @@ function createFace({ product, color = false, columns }) {
151
151
  const content = Math.min(width - 6, Math.max(...body.map(visibleWidth)));
152
152
  const rule = paint(identity.accent, GLYPH.rule.repeat(content + 4));
153
153
  const frame = (left, right) => `${paint(identity.accent, left)}${rule}${paint(identity.accent, right)}`;
154
+ const bodyRow = (line) => line.startsWith(GLYPH.check) ? `${paint(PALETTE.green, GLYPH.check)}${line.slice(1)}` : line;
154
155
  return [
155
156
  frame(GLYPH.boxTop, GLYPH.boxTopEnd),
156
- ...body.map((line) => `${paint(identity.accent, GLYPH.bar)} ${line}${" ".repeat(Math.max(0, content - visibleWidth(line)))} ${paint(identity.accent, GLYPH.bar)}`),
157
+ ...body.map((line) => `${paint(identity.accent, GLYPH.bar)} ${bodyRow(line)}${" ".repeat(Math.max(0, content - visibleWidth(line)))} ${paint(identity.accent, GLYPH.bar)}`),
157
158
  frame(GLYPH.boxBottom, GLYPH.boxBottomEnd)
158
159
  ];
159
160
  };
@@ -162,6 +163,8 @@ function createFace({ product, color = false, columns }) {
162
163
  return { identity, width, welcome, step, relay, receipt, signOff, refusal, paint };
163
164
  }
164
165
  var RELAY_INDENT = " ".repeat(TITLE_COLUMN - 1);
166
+ var FRAMES = ["\u25D2", "\u25D0", "\u25D3", "\u25D1"];
167
+ var SPINNER_FRAMES = Object.freeze([...FRAMES]);
165
168
  var ALLOWED = new Set(Object.values(GLYPH));
166
169
 
167
170
  // src/autoupdate.ts
@@ -891,7 +894,7 @@ function wipeProductDir(dir) {
891
894
  }
892
895
 
893
896
  // src/index.ts
894
- var LAUNCHER_VERSION = true ? "0.1.3" : readVersionFromPackage();
897
+ var LAUNCHER_VERSION = true ? "0.1.7" : readVersionFromPackage();
895
898
  function defaultPrint(message) {
896
899
  process.stdout.write(`${message}
897
900
  `);
@@ -1103,19 +1106,58 @@ function needsShell(command) {
1103
1106
  function quoteForShell(arg) {
1104
1107
  return /[\s"&|<>^%]/.test(arg) ? `"${arg.replace(/"/g, '\\"')}"` : arg;
1105
1108
  }
1106
- function defaultRunEntry(entry, cwd) {
1109
+ var SUPPORTED_PROGRESS_PROTOCOLS = /* @__PURE__ */ new Set([1]);
1110
+ function parseProgress(raw) {
1111
+ if (raw === null || raw === void 0) return [];
1112
+ const progress = [];
1113
+ for (const line of raw.toString().split(/\r?\n/)) {
1114
+ if (!line) continue;
1115
+ try {
1116
+ const message = JSON.parse(line);
1117
+ if (!SUPPORTED_PROGRESS_PROTOCOLS.has(message.v) || typeof message.step !== "string") continue;
1118
+ const state = message.state ?? "ok";
1119
+ if (state !== "ok" && state !== "fail" && state !== "note") continue;
1120
+ if (message.ms !== void 0 && typeof message.ms !== "number") continue;
1121
+ progress.push({
1122
+ step: message.step,
1123
+ state,
1124
+ ...typeof message.ms === "number" ? { ms: message.ms } : {}
1125
+ });
1126
+ } catch {
1127
+ }
1128
+ }
1129
+ return progress;
1130
+ }
1131
+ function defaultRunEntry(entry, cwd, env) {
1107
1132
  const [command, ...args] = entry;
1108
1133
  const shell = needsShell(command);
1109
1134
  const commandLine = shell ? [command, ...args].map(quoteForShell).join(" ") : command;
1110
- const result = (0, import_node_child_process3.spawnSync)(commandLine, shell ? [] : args, {
1135
+ const spawn2 = (progress2) => (0, import_node_child_process3.spawnSync)(commandLine, shell ? [] : args, {
1111
1136
  cwd,
1112
- stdio: "inherit",
1137
+ stdio: progress2 ? ["inherit", "inherit", "inherit", "pipe"] : "inherit",
1113
1138
  shell,
1139
+ env: progress2 ? { ...env ?? process.env, MM_PROGRESS_FD: "3", MM_PROGRESS_PROTOCOL: "1" } : env ?? process.env,
1114
1140
  windowsHide: true
1115
1141
  });
1142
+ let result;
1143
+ if (process.platform === "win32" && shell) {
1144
+ result = spawn2(false);
1145
+ } else {
1146
+ try {
1147
+ result = spawn2(true);
1148
+ } catch {
1149
+ result = spawn2(false);
1150
+ }
1151
+ }
1116
1152
  if (result.error) return { ok: false, error: result.error.message };
1117
1153
  const code = typeof result.status === "number" ? result.status : void 0;
1118
- return { ok: result.status === 0, error: result.status === 0 ? void 0 : `exit code ${result.status}`, code };
1154
+ const progress = parseProgress(result.output?.[3]);
1155
+ return {
1156
+ ok: result.status === 0,
1157
+ error: result.status === 0 ? void 0 : `exit code ${result.status}`,
1158
+ code,
1159
+ ...progress.length > 0 ? { progress } : {}
1160
+ };
1119
1161
  }
1120
1162
  function faceProduct(config) {
1121
1163
  const known = { mmi: "mmi-hub", jerv: "jerv-hub" };
@@ -1123,10 +1165,11 @@ function faceProduct(config) {
1123
1165
  }
1124
1166
  function faceFor(config, options) {
1125
1167
  const tty = options.tty ?? Boolean(process.stdout.isTTY);
1168
+ if (!tty) return null;
1126
1169
  try {
1127
1170
  return createFace({
1128
1171
  product: faceProduct(config),
1129
- color: tty && !process.env.NO_COLOR && process.env.TERM !== "dumb",
1172
+ color: !process.env.NO_COLOR && process.env.TERM !== "dumb",
1130
1173
  columns: process.stdout.columns
1131
1174
  });
1132
1175
  } catch {
@@ -1136,9 +1179,13 @@ function faceFor(config, options) {
1136
1179
  function since(start) {
1137
1180
  return (Date.now() - start) / 1e3;
1138
1181
  }
1182
+ function outerConsoleOwnsOutcome() {
1183
+ return process.env.MM_OUTER_CONSOLE === "1";
1184
+ }
1139
1185
  function printReceipt(face, config, print, ready, lines) {
1140
1186
  const name = face ? face.identity.name : config.product;
1141
1187
  const headline = ready ? `${name} is ready.` : `${name} is installed, but not ready yet.`;
1188
+ if (ready && outerConsoleOwnsOutcome()) return;
1142
1189
  if (!face) {
1143
1190
  print(headline);
1144
1191
  for (const line of lines) print(line.trim());
@@ -1151,6 +1198,11 @@ function printReceipt(face, config, print, ready, lines) {
1151
1198
  function printStep(face, print, title, seconds, kind = "ok") {
1152
1199
  print(face ? face.step(title, seconds, kind) : title);
1153
1200
  }
1201
+ function printProgress(face, print, progress) {
1202
+ for (const message of progress ?? []) {
1203
+ printStep(face, print, message.step, message.ms === void 0 ? null : message.ms / 1e3, message.state);
1204
+ }
1205
+ }
1154
1206
  async function doInstall(config, dir, options, print) {
1155
1207
  const fetchImpl = options.fetchImpl ?? fetch;
1156
1208
  const face = faceFor(config, options);
@@ -1159,13 +1211,13 @@ async function doInstall(config, dir, options, print) {
1159
1211
  const manifest = await fetchVerifiedManifest(config, accessToken, fetchImpl);
1160
1212
  await downloadAndUnpack(config, dir, manifest, accessToken, { fetchImpl });
1161
1213
  writeState(dir, { version: manifest.version, updatedAt: (/* @__PURE__ */ new Date()).toISOString() });
1162
- printStep(face, print, `installed ${config.product} ${manifest.version} into ${dir}`, since(started));
1214
+ printStep(face, print, `installed ${config.product} ${manifest.version}`, since(started));
1163
1215
  return finishLastMile(config, dir, options, print, face, manifest.version);
1164
1216
  }
1165
1217
  async function doUpdate(config, dir, options, print) {
1166
1218
  const fetchImpl = options.fetchImpl ?? fetch;
1167
1219
  const face = faceFor(config, options);
1168
- if (face) for (const line of face.welcome()) print(line);
1220
+ if (face && !outerConsoleOwnsOutcome()) for (const line of face.welcome()) print(line);
1169
1221
  const accessToken = await fetchAccessTokenOrLogin(config, dir, options, print);
1170
1222
  const started = Date.now();
1171
1223
  const manifest = await fetchVerifiedManifest(config, accessToken, fetchImpl);
@@ -1179,32 +1231,69 @@ async function doUpdate(config, dir, options, print) {
1179
1231
  printStep(face, print, `updated ${config.product} to ${manifest.version}`, since(started));
1180
1232
  return finishLastMile(config, dir, options, print, face, manifest.version);
1181
1233
  }
1234
+ function payloadEnv(dir) {
1235
+ const stored = readTokens(dir);
1236
+ if (!stored?.accessToken) return void 0;
1237
+ return { ...process.env, MM_INSTALLER_TOKEN: stored.accessToken };
1238
+ }
1239
+ var NEVER_A_PROGRAM = /\.(tgz|tar|gz|bz2|xz|zip|json|txt|md|lock)$/i;
1240
+ function payloadFileAsCommand(entry, payload) {
1241
+ const first = entry[0];
1242
+ if (!first || first === "$self") return null;
1243
+ if (first.includes("/") || first.includes("\\")) return null;
1244
+ const candidate = (0, import_node_path4.join)(payload, first);
1245
+ if (!(0, import_node_fs5.existsSync)(candidate)) return null;
1246
+ if (NEVER_A_PROGRAM.test(first)) return first;
1247
+ if (process.platform === "win32") return null;
1248
+ try {
1249
+ return ((0, import_node_fs5.statSync)(candidate).mode & 73) === 0 ? first : null;
1250
+ } catch {
1251
+ return null;
1252
+ }
1253
+ }
1182
1254
  function finishLastMile(config, dir, options, print, face, version) {
1183
1255
  const entry = readPayloadEntry(dir);
1184
1256
  const payload = payloadDir(dir);
1185
1257
  if (!entry) {
1186
1258
  printReceipt(face, config, print, true, [
1187
- ` Installed ${version} into ${dir}`,
1188
- ` next step: run ${(0, import_node_path4.join)(payload, config.binName)} to start ${config.product}.`
1259
+ `Installed ${version} into ${dir}`,
1260
+ `next step: run ${(0, import_node_path4.join)(payload, config.binName)} to start ${config.product}.`
1189
1261
  ]);
1190
1262
  return 0;
1191
1263
  }
1192
1264
  const command = resolveEntry(entry);
1265
+ const dataFile = payloadFileAsCommand(entry, payload);
1266
+ if (dataFile) {
1267
+ printStep(face, print, `The payload names ${dataFile} as its command, but that is a file, not a program`, null, "fail");
1268
+ printReceipt(face, config, print, false, [
1269
+ `Downloaded ${version} into ${dir}`,
1270
+ `This payload was built wrong: its entry must be a command, not one of its own files.`,
1271
+ `Nothing on this machine can finish it \u2014 report it to the ${config.product} maintainers.`
1272
+ ]);
1273
+ return 2;
1274
+ }
1193
1275
  const started = Date.now();
1194
- const result = (options.runEntry ?? defaultRunEntry)(command, payload);
1276
+ const result = (options.runEntry ?? defaultRunEntry)(command, payload, payloadEnv(dir));
1277
+ printProgress(face, print, result.progress);
1195
1278
  if (result.ok) {
1196
1279
  printStep(face, print, "Armed this machine", since(started));
1197
1280
  printReceipt(face, config, print, true, [
1198
- ` Installed ${version} into ${dir}`,
1199
- ` Check health any time: ${config.binName} doctor`
1281
+ `Installed ${version} into ${dir}`,
1282
+ `Check health any time: ${config.binName} doctor`
1200
1283
  ]);
1201
1284
  return 0;
1202
1285
  }
1203
1286
  const code = result.code ?? 1;
1204
- printStep(face, print, `Arming this machine did not finish (exit code ${code})`, null, "fail");
1287
+ printStep(
1288
+ face,
1289
+ print,
1290
+ result.code === void 0 ? `Arming this machine could not start: ${result.error ?? `could not run ${command[0]}`}` : `Arming this machine did not finish (exit code ${code})`,
1291
+ null,
1292
+ "fail"
1293
+ );
1205
1294
  printReceipt(face, config, print, false, [
1206
- ` Downloaded ${version} into ${dir}`,
1207
- ` Finish it with: (cd ${payload} && ${command.join(" ")})`
1295
+ `Downloaded ${version} into ${dir}`,
1296
+ `Finish it with: (cd ${payload} && ${command.join(" ")})`
1208
1297
  ]);
1209
1298
  return code;
1210
1299
  }
@@ -1222,7 +1311,7 @@ function doForward(config, dir, options, command, argv, print) {
1222
1311
  return 2;
1223
1312
  }
1224
1313
  const forwarded = [...resolveEntry(target), ...argv];
1225
- const result = (options.runEntry ?? defaultRunEntry)(forwarded, payloadDir(dir));
1314
+ const result = (options.runEntry ?? defaultRunEntry)(forwarded, payloadDir(dir), payloadEnv(dir));
1226
1315
  if (!result.ok && result.code === void 0) {
1227
1316
  print(`failed: ${result.error ?? `could not run ${forwarded[0]}`}`);
1228
1317
  return 1;
@@ -1273,7 +1362,7 @@ function doDoctor(config, dir, options, print) {
1273
1362
  const verbs = readPayloadVerbs(dir);
1274
1363
  const chains = target !== null && (verbs === "*" || Array.isArray(verbs) && verbs.includes("doctor"));
1275
1364
  if (!chains) return 0;
1276
- const result = (options.runEntry ?? defaultRunEntry)([...resolveEntry(target), "doctor"], payloadDir(dir));
1365
+ const result = (options.runEntry ?? defaultRunEntry)([...resolveEntry(target), "doctor"], payloadDir(dir), payloadEnv(dir));
1277
1366
  return result.code ?? (result.ok ? 0 : 1);
1278
1367
  }
1279
1368
  function doLauncherDoctor(config, dir, options, print) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mutmutco/installer-launcher",
3
- "version": "0.1.3",
3
+ "version": "0.1.7",
4
4
  "description": "Single-executable (SEA) launcher: sign-in, gated payload fetch, self-update. One copy ships per product.",
5
5
  "type": "module",
6
6
  "license": "UNLICENSED",
@@ -34,6 +34,6 @@
34
34
  "vitest": "^4.1.0"
35
35
  },
36
36
  "dependencies": {
37
- "@mutmutco/installer-face": "^0.1.0"
37
+ "@mutmutco/installer-face": "^0.2.1"
38
38
  }
39
39
  }