@moneysiren/app 0.1.0-alpha.11 → 0.1.0-alpha.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/README.md CHANGED
@@ -12,19 +12,19 @@ msiren start
12
12
  msiren hud
13
13
  ```
14
14
 
15
- The package installs both commands:
15
+ The package creates both global command shims during postinstall:
16
16
 
17
17
  - `moneysiren`
18
18
  - `msiren`
19
19
 
20
- If npm reports `EEXIST` for `moneysiren` or `msiren`, an older global MoneySiren install left command shims behind. Remove the old global packages and reinstall:
20
+ If npm reports `EEXIST` for `moneysiren` or `msiren`, an older alpha app package may still be installed. Remove the old global packages and reinstall:
21
21
 
22
22
  ```powershell
23
23
  npm uninstall -g @moneysiren/cli @moneysiren/app
24
24
  npm install -g @moneysiren/app@alpha --force
25
25
  ```
26
26
 
27
- The app package also removes stale MoneySiren-owned command shims during global install when npm exposes the global prefix.
27
+ Current app packages do not use npm's `bin` field for these aliases, so stale MoneySiren-owned command shims can be replaced during postinstall without tripping npm's bin conflict check.
28
28
 
29
29
  ## What It Installs
30
30
 
@@ -1,6 +1,6 @@
1
1
  import type { InstallSurface } from "./install-profile.js";
2
2
  export declare const DEFAULT_RELEASE_REPOSITORY = "ztwz11/moneysiren";
3
- export declare const DEFAULT_RELEASE_TAG = "v0.1.0-alpha.11";
3
+ export declare const DEFAULT_RELEASE_TAG = "v0.1.0-alpha.12";
4
4
  export interface ReleaseInstallOptions {
5
5
  env?: Record<string, string | undefined>;
6
6
  fetchImpl: typeof fetch;
@@ -7,7 +7,7 @@ import { promisify } from "node:util";
7
7
  const execFileAsync = promisify(execFile);
8
8
  export const DEFAULT_RELEASE_REPOSITORY = "ztwz11/moneysiren";
9
9
  // Keep the source-free installer pinned to the latest published desktop/web release tag.
10
- export const DEFAULT_RELEASE_TAG = "v0.1.0-alpha.11";
10
+ export const DEFAULT_RELEASE_TAG = "v0.1.0-alpha.12";
11
11
  const RELEASE_REPOSITORY_ENV_KEY = "MONEYSIREN_RELEASE_REPOSITORY";
12
12
  const RELEASE_TAG_ENV_KEY = "MONEYSIREN_RELEASE_TAG";
13
13
  const RELEASE_INSTALL_DIR_ENV_KEY = "MONEYSIREN_RELEASE_INSTALL_DIR";
@@ -1,2 +1,2 @@
1
- export declare const CLI_VERSION = "0.1.0-alpha.11";
1
+ export declare const CLI_VERSION = "0.1.0-alpha.12";
2
2
  //# sourceMappingURL=version.d.ts.map
@@ -1,2 +1,2 @@
1
- export const CLI_VERSION = "0.1.0-alpha.11";
1
+ export const CLI_VERSION = "0.1.0-alpha.12";
2
2
  //# sourceMappingURL=version.js.map
@@ -3,7 +3,7 @@ import { parseNotificationPreferences, readNotificationDigest, readNotificationP
3
3
  import { assertLoopbackHost, isLoopbackHost, removeRuntimeLock, writeRuntimeLock, } from "../../runtime/src/index.js";
4
4
  const DEFAULT_HOST = "127.0.0.1";
5
5
  const DEFAULT_PORT = 47831;
6
- const DEFAULT_VERSION = "0.1.0-alpha.11";
6
+ const DEFAULT_VERSION = "0.1.0-alpha.12";
7
7
  export async function startLocalApiServer(options = {}) {
8
8
  const host = options.host ?? DEFAULT_HOST;
9
9
  const requestedPort = options.port ?? DEFAULT_PORT;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@moneysiren/app",
3
- "version": "0.1.0-alpha.11",
3
+ "version": "0.1.0-alpha.12",
4
4
  "description": "One-command installer for the MoneySiren CLI, local web dashboard, and HUD.",
5
5
  "private": false,
6
6
  "license": "MIT",
@@ -9,10 +9,6 @@
9
9
  "engines": {
10
10
  "node": ">=20.11.0"
11
11
  },
12
- "bin": {
13
- "moneysiren": "dist/apps/cli/src/index.js",
14
- "msiren": "dist/apps/cli/src/index.js"
15
- },
16
12
  "dependencies": {
17
13
  "@aws-sdk/client-cost-explorer": "^3.1061.0"
18
14
  },
@@ -21,7 +17,6 @@
21
17
  "dist/apps/cli/src/**/*.d.ts",
22
18
  "dist/packages/**/*.js",
23
19
  "dist/packages/**/*.d.ts",
24
- "scripts/preinstall.mjs",
25
20
  "scripts/postinstall.mjs",
26
21
  "README.md",
27
22
  "LICENSE"
@@ -32,11 +27,10 @@
32
27
  "scripts": {
33
28
  "build": "node ../../tools/scripts/build-app-package.mjs",
34
29
  "prepack": "node ../../tools/scripts/build-app-package.mjs",
35
- "preinstall": "node scripts/preinstall.mjs",
36
30
  "postinstall": "node scripts/postinstall.mjs",
37
31
  "pack:dry-run": "npm pack --dry-run",
38
- "test": "node --check scripts/preinstall.mjs && node --check scripts/postinstall.mjs",
39
- "typecheck": "node --check scripts/preinstall.mjs && node --check scripts/postinstall.mjs",
40
- "lint": "node --check scripts/preinstall.mjs && node --check scripts/postinstall.mjs"
32
+ "test": "node --check scripts/postinstall.mjs",
33
+ "typecheck": "node --check scripts/postinstall.mjs",
34
+ "lint": "node --check scripts/postinstall.mjs"
41
35
  }
42
36
  }
@@ -1,13 +1,14 @@
1
1
  #!/usr/bin/env node
2
2
 
3
- import { existsSync } from "node:fs";
4
- import { dirname, resolve } from "node:path";
3
+ import { chmodSync, existsSync, mkdirSync, readFileSync, realpathSync, writeFileSync } from "node:fs";
4
+ import { basename, dirname, resolve } from "node:path";
5
5
  import { spawnSync } from "node:child_process";
6
6
  import { fileURLToPath } from "node:url";
7
7
 
8
8
  const scriptDir = dirname(fileURLToPath(import.meta.url));
9
9
  const packageRoot = resolve(scriptDir, "..");
10
10
  const cliEntry = resolve(packageRoot, "dist", "apps", "cli", "src", "index.js");
11
+ const isGlobal = isGlobalInstall();
11
12
 
12
13
  if (isTruthy(process.env.MONEYSIREN_SKIP_APP_POSTINSTALL)) {
13
14
  console.log("MoneySiren app asset installation skipped by MONEYSIREN_SKIP_APP_POSTINSTALL.");
@@ -20,6 +21,10 @@ if (!existsSync(cliEntry)) {
20
21
  process.exit(0);
21
22
  }
22
23
 
24
+ if (isGlobal || isTruthy(process.env.MONEYSIREN_APP_INSTALL_GLOBAL_SHIMS)) {
25
+ installGlobalCommandShims(cliEntry);
26
+ }
27
+
23
28
  if (!shouldInstallReleaseAssets()) {
24
29
  console.log("MoneySiren app package installed.");
25
30
  console.log("Run `msiren install --all` to download the local web dashboard and HUD artifacts.");
@@ -41,20 +46,199 @@ const result = spawnSync(process.execPath, [cliEntry, "install", "--all"], {
41
46
  });
42
47
 
43
48
  if (result.error !== undefined) {
44
- console.error(`MoneySiren app asset installation failed: ${result.error.message}`);
45
- console.error("Retry with `msiren install --all` after npm finishes.");
46
- process.exit(1);
49
+ handleAssetInstallFailure(`MoneySiren app asset installation failed: ${result.error.message}`);
47
50
  }
48
51
 
49
52
  if (result.status !== 0) {
50
- console.error("MoneySiren app asset installation failed.");
51
- console.error("Retry with `msiren install --all` after npm finishes.");
52
- process.exit(result.status ?? 1);
53
+ handleAssetInstallFailure("MoneySiren app asset installation failed.");
54
+ }
55
+
56
+ function installGlobalCommandShims(entrypoint) {
57
+ const binDirs = getGlobalBinDirs();
58
+ const installed = [];
59
+
60
+ for (const binDir of binDirs) {
61
+ try {
62
+ mkdirSync(binDir, {
63
+ recursive: true,
64
+ });
65
+
66
+ for (const command of ["moneysiren", "msiren"]) {
67
+ installed.push(...writeCommandShim(binDir, command, entrypoint));
68
+ }
69
+ } catch (error) {
70
+ const message = error instanceof Error ? error.message : String(error);
71
+ console.warn(`MoneySiren app command shim setup skipped for ${binDir}: ${message}`);
72
+ }
73
+ }
74
+
75
+ if (installed.length > 0) {
76
+ console.log(`MoneySiren command shim(s) ready: ${Array.from(new Set(installed)).join(", ")}`);
77
+ }
78
+ }
79
+
80
+ function getGlobalBinDirs() {
81
+ const candidates = [process.env.npm_config_prefix ?? dirname(process.execPath)];
82
+ const dirs = [];
83
+
84
+ for (const candidate of candidates) {
85
+ if (!candidate) {
86
+ continue;
87
+ }
88
+
89
+ addBinDir(dirs, candidate);
90
+
91
+ try {
92
+ addBinDir(dirs, realpathSync(candidate));
93
+ } catch {
94
+ // Best effort only. The original candidate is still useful.
95
+ }
96
+ }
97
+
98
+ return dirs;
99
+ }
100
+
101
+ function addBinDir(dirs, candidate) {
102
+ const binDir = process.platform === "win32"
103
+ ? resolve(candidate)
104
+ : basename(candidate) === "bin"
105
+ ? resolve(candidate)
106
+ : resolve(candidate, "bin");
107
+ const normalized = binDir.toLowerCase();
108
+
109
+ if (!dirs.some((dir) => dir.toLowerCase() === normalized)) {
110
+ dirs.push(binDir);
111
+ }
112
+ }
113
+
114
+ function writeCommandShim(binDir, command, entrypoint) {
115
+ if (process.platform === "win32") {
116
+ return [
117
+ writeShimFile(resolve(binDir, command), createPosixShim(entrypoint), true),
118
+ writeShimFile(resolve(binDir, `${command}.cmd`), createCmdShim(entrypoint), false),
119
+ writeShimFile(resolve(binDir, `${command}.ps1`), createPowerShellShim(entrypoint), false),
120
+ ].filter(Boolean);
121
+ }
122
+
123
+ return [
124
+ writeShimFile(resolve(binDir, command), createPosixShim(entrypoint), true),
125
+ ].filter(Boolean);
126
+ }
127
+
128
+ function writeShimFile(filePath, content, executable) {
129
+ if (existsSync(filePath) && !isMoneySirenShim(filePath)) {
130
+ console.warn(`MoneySiren app command shim not replaced because it is not MoneySiren-owned: ${filePath}`);
131
+ return null;
132
+ }
133
+
134
+ writeFileSync(filePath, content, "utf8");
135
+
136
+ if (executable) {
137
+ try {
138
+ chmodSync(filePath, 0o755);
139
+ } catch {
140
+ // Windows may ignore POSIX executable bits.
141
+ }
142
+ }
143
+
144
+ return filePath;
145
+ }
146
+
147
+ function handleAssetInstallFailure(message) {
148
+ console.warn(message);
149
+ console.warn("MoneySiren commands were installed. Retry release asset installation with `msiren install --all` after npm finishes.");
150
+
151
+ if (isTruthy(process.env.MONEYSIREN_APP_STRICT_POSTINSTALL)) {
152
+ process.exit(1);
153
+ }
154
+
155
+ process.exit(0);
156
+ }
157
+
158
+ function isMoneySirenShim(filePath) {
159
+ try {
160
+ const source = readFileSync(filePath, "utf8");
161
+
162
+ return /@moneysiren[\\/]app|@moneysiren[\\/]cli|moneysiren-app|moneysiren-cli|MoneySiren app command shim/i.test(source);
163
+ } catch {
164
+ return false;
165
+ }
166
+ }
167
+
168
+ function createPosixShim(entrypoint) {
169
+ return [
170
+ "#!/bin/sh",
171
+ "basedir=$(dirname \"$(echo \"$0\" | sed -e 's,\\\\,/,g')\")",
172
+ "",
173
+ "case `uname` in",
174
+ " *CYGWIN*|*MINGW*|*MSYS*)",
175
+ " if command -v cygpath > /dev/null 2>&1; then",
176
+ " basedir=`cygpath -w \"$basedir\"`",
177
+ " fi",
178
+ " ;;",
179
+ "esac",
180
+ "",
181
+ "if [ -x \"$basedir/node\" ]; then",
182
+ ` exec "$basedir/node" ${shellQuote(toPosixPath(entrypoint))} "$@"`,
183
+ "else",
184
+ ` exec node ${shellQuote(toPosixPath(entrypoint))} "$@"`,
185
+ "fi",
186
+ "",
187
+ ].join("\n");
188
+ }
189
+
190
+ function createCmdShim(entrypoint) {
191
+ return [
192
+ "@ECHO off",
193
+ "SETLOCAL",
194
+ "IF EXIST \"%~dp0\\node.exe\" (",
195
+ " SET \"_prog=%~dp0\\node.exe\"",
196
+ ") ELSE (",
197
+ " SET \"_prog=node\"",
198
+ " SET PATHEXT=%PATHEXT:;.JS;=;%",
199
+ ")",
200
+ `"%_prog%" "${entrypoint}" %*`,
201
+ "",
202
+ ].join("\r\n");
203
+ }
204
+
205
+ function createPowerShellShim(entrypoint) {
206
+ const escapedEntrypoint = entrypoint.replace(/'/g, "''");
207
+
208
+ return [
209
+ "#!/usr/bin/env pwsh",
210
+ "$basedir = Split-Path $MyInvocation.MyCommand.Definition -Parent",
211
+ "$exe = \"\"",
212
+ "if ($PSVersionTable.PSVersion -lt \"6.0\" -or $IsWindows) {",
213
+ " $exe = \".exe\"",
214
+ "}",
215
+ "$node = if (Test-Path \"$basedir/node$exe\") { \"$basedir/node$exe\" } else { \"node\" }",
216
+ `$entry = '${escapedEntrypoint}'`,
217
+ "if ($MyInvocation.ExpectingInput) {",
218
+ " $input | & $node $entry $args",
219
+ "} else {",
220
+ " & $node $entry $args",
221
+ "}",
222
+ "exit $LASTEXITCODE",
223
+ "",
224
+ ].join("\n");
225
+ }
226
+
227
+ function toPosixPath(value) {
228
+ return value.replace(/\\/g, "/");
229
+ }
230
+
231
+ function shellQuote(value) {
232
+ return `'${value.replace(/'/g, "'\\''")}'`;
53
233
  }
54
234
 
55
235
  function shouldInstallReleaseAssets() {
56
236
  return isTruthy(process.env.MONEYSIREN_APP_INSTALL_ALL) ||
57
- process.env.npm_config_global === "true" ||
237
+ isGlobal;
238
+ }
239
+
240
+ function isGlobalInstall() {
241
+ return process.env.npm_config_global === "true" ||
58
242
  process.env.npm_config_location === "global";
59
243
  }
60
244
 
@@ -1,87 +0,0 @@
1
- #!/usr/bin/env node
2
-
3
- import { existsSync, lstatSync, readFileSync, readlinkSync, rmSync } from "node:fs";
4
- import { join, resolve } from "node:path";
5
-
6
- if (isTruthy(process.env.MONEYSIREN_SKIP_APP_PREINSTALL)) {
7
- process.exit(0);
8
- }
9
-
10
- if (!isGlobalInstall()) {
11
- process.exit(0);
12
- }
13
-
14
- const prefix = process.env.npm_config_prefix;
15
-
16
- if (!prefix) {
17
- console.warn("MoneySiren app preinstall skipped: npm global prefix was not provided.");
18
- process.exit(0);
19
- }
20
-
21
- const binDir = process.platform === "win32" ? prefix : join(prefix, "bin");
22
- const shimNames = process.platform === "win32"
23
- ? ["moneysiren", "moneysiren.cmd", "moneysiren.ps1", "msiren", "msiren.cmd", "msiren.ps1"]
24
- : ["moneysiren", "msiren"];
25
- const removed = [];
26
-
27
- for (const shimName of shimNames) {
28
- const shimPath = resolve(binDir, shimName);
29
-
30
- if (!isInside(resolve(binDir), shimPath) || !existsSync(shimPath)) {
31
- continue;
32
- }
33
-
34
- if (!isMoneySirenShim(shimPath)) {
35
- continue;
36
- }
37
-
38
- try {
39
- rmSync(shimPath, {
40
- force: true,
41
- });
42
- removed.push(shimName);
43
- } catch (error) {
44
- const message = error instanceof Error ? error.message : String(error);
45
- console.warn(`MoneySiren app preinstall could not remove stale global command ${shimName}: ${message}`);
46
- }
47
- }
48
-
49
- if (removed.length > 0) {
50
- console.log(`MoneySiren app preinstall removed stale global command shim(s): ${removed.join(", ")}`);
51
- }
52
-
53
- function isGlobalInstall() {
54
- return process.env.npm_config_global === "true" ||
55
- process.env.npm_config_location === "global" ||
56
- isTruthy(process.env.MONEYSIREN_APP_GLOBAL_PREINSTALL);
57
- }
58
-
59
- function isMoneySirenShim(filePath) {
60
- try {
61
- const stat = lstatSync(filePath);
62
- const source = stat.isSymbolicLink()
63
- ? readlinkSync(filePath)
64
- : stat.isFile()
65
- ? readFileSync(filePath, "utf8")
66
- : "";
67
-
68
- return /@moneysiren[\\/]app|@moneysiren[\\/]cli|moneysiren-app|moneysiren-cli/i.test(source);
69
- } catch {
70
- return false;
71
- }
72
- }
73
-
74
- function isInside(parent, child) {
75
- const relative = child.slice(parent.length);
76
- return child === parent || (child.startsWith(parent) && /^[/\\]/.test(relative));
77
- }
78
-
79
- function isTruthy(value) {
80
- if (value === undefined) {
81
- return false;
82
- }
83
-
84
- const normalized = value.trim().toLowerCase();
85
-
86
- return normalized.length > 0 && normalized !== "0" && normalized !== "false" && normalized !== "no";
87
- }