@moneysiren/app 0.1.0-alpha.11 → 0.1.0-alpha.13

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
@@ -2,29 +2,39 @@
2
2
 
3
3
  One-command alpha installer for MoneySiren.
4
4
 
5
- This package bundles the MoneySiren CLI entrypoints and, on global npm installs, runs `msiren install --all` to download the local web dashboard runtime and HUD desktop artifacts from the matching GitHub Release.
5
+ This is the recommended npm package for users who want all three local MoneySiren surfaces: CLI, web dashboard, and HUD. It bundles the MoneySiren CLI entrypoints and, on global npm installs, runs `msiren install --all` to download the local web dashboard runtime and HUD desktop artifacts from the matching GitHub Release.
6
6
 
7
7
  ## Install
8
8
 
9
9
  ```bash
10
10
  npm install -g @moneysiren/app@alpha
11
+ msiren --version
11
12
  msiren start
12
13
  msiren hud
13
14
  ```
14
15
 
15
- The package installs both commands:
16
+ During alpha, keep `@alpha` in the install command. Stable releases will use the unqualified package name.
17
+
18
+ The package creates both global command shims during postinstall:
16
19
 
17
20
  - `moneysiren`
18
21
  - `msiren`
19
22
 
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:
23
+ 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
24
 
22
25
  ```powershell
23
26
  npm uninstall -g @moneysiren/cli @moneysiren/app
24
27
  npm install -g @moneysiren/app@alpha --force
25
28
  ```
26
29
 
27
- The app package also removes stale MoneySiren-owned command shims during global install when npm exposes the global prefix.
30
+ 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.
31
+
32
+ If Web/HUD asset download fails during postinstall, fix network or release access and rerun:
33
+
34
+ ```bash
35
+ msiren install --all
36
+ msiren install --status
37
+ ```
28
38
 
29
39
  ## What It Installs
30
40
 
@@ -34,6 +44,8 @@ The app package also removes stale MoneySiren-owned command shims during global
34
44
 
35
45
  The Web/HUD artifacts are verified against published SHA256 checksums. Alpha Windows HUD artifacts may be unsigned until release signing is fully configured.
36
46
 
47
+ For CLI-only automation, install `@moneysiren/cli@alpha` instead.
48
+
37
49
  ## Opt Out
38
50
 
39
51
  To install only the package command and skip Web/HUD asset download:
@@ -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.13";
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.13";
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.13";
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.13";
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.13";
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.13",
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
- }