run402 3.7.14 → 3.8.1
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 +4 -2
- package/cli.mjs +6 -0
- package/core-dist/config.js +9 -0
- package/core-dist/keystore.js +103 -68
- package/core-dist/profile-state.js +137 -0
- package/lib/config.mjs +31 -20
- package/lib/credentials.mjs +273 -0
- package/lib/deploy-v2.mjs +11 -0
- package/lib/doctor.mjs +18 -2
- package/lib/domains.mjs +40 -20
- package/lib/next-actions.mjs +7 -0
- package/lib/projects.mjs +65 -27
- package/lib/sdk-errors.mjs +24 -3
- package/lib/sdk.mjs +3 -2
- package/lib/up.mjs +19 -0
- package/lib/up.test.mjs +29 -0
- package/lib/update-check.mjs +722 -0
- package/lib/update-check.test.mjs +438 -0
- package/package.json +1 -1
- package/sdk/core-dist/config.js +9 -0
- package/sdk/core-dist/keystore.js +103 -68
- package/sdk/core-dist/profile-state.js +137 -0
- package/sdk/dist/credentials.d.ts +24 -7
- package/sdk/dist/credentials.d.ts.map +1 -1
- package/sdk/dist/credentials.js +4 -4
- package/sdk/dist/errors.d.ts +27 -1
- package/sdk/dist/errors.d.ts.map +1 -1
- package/sdk/dist/errors.js +61 -3
- package/sdk/dist/errors.js.map +1 -1
- package/sdk/dist/index.d.ts +16 -2
- package/sdk/dist/index.d.ts.map +1 -1
- package/sdk/dist/index.js +9 -3
- package/sdk/dist/index.js.map +1 -1
- package/sdk/dist/kernel.d.ts +9 -0
- package/sdk/dist/kernel.d.ts.map +1 -1
- package/sdk/dist/kernel.js +40 -1
- package/sdk/dist/kernel.js.map +1 -1
- package/sdk/dist/namespaces/ai.d.ts.map +1 -1
- package/sdk/dist/namespaces/ai.js +5 -10
- package/sdk/dist/namespaces/ai.js.map +1 -1
- package/sdk/dist/namespaces/apps.d.ts.map +1 -1
- package/sdk/dist/namespaces/apps.js +5 -13
- package/sdk/dist/namespaces/apps.js.map +1 -1
- package/sdk/dist/namespaces/assets.d.ts.map +1 -1
- package/sdk/dist/namespaces/assets.js +9 -22
- package/sdk/dist/namespaces/assets.js.map +1 -1
- package/sdk/dist/namespaces/auth.d.ts.map +1 -1
- package/sdk/dist/namespaces/auth.js +16 -43
- package/sdk/dist/namespaces/auth.js.map +1 -1
- package/sdk/dist/namespaces/contracts.d.ts.map +1 -1
- package/sdk/dist/namespaces/contracts.js +12 -31
- package/sdk/dist/namespaces/contracts.js.map +1 -1
- package/sdk/dist/namespaces/credentials.d.ts +48 -0
- package/sdk/dist/namespaces/credentials.d.ts.map +1 -0
- package/sdk/dist/namespaces/credentials.js +115 -0
- package/sdk/dist/namespaces/credentials.js.map +1 -0
- package/sdk/dist/namespaces/domains.d.ts +8 -2
- package/sdk/dist/namespaces/domains.d.ts.map +1 -1
- package/sdk/dist/namespaces/domains.js +41 -23
- package/sdk/dist/namespaces/domains.js.map +1 -1
- package/sdk/dist/namespaces/email.d.ts.map +1 -1
- package/sdk/dist/namespaces/email.js +8 -19
- package/sdk/dist/namespaces/email.js.map +1 -1
- package/sdk/dist/namespaces/functions.d.ts.map +1 -1
- package/sdk/dist/namespaces/functions.js +14 -37
- package/sdk/dist/namespaces/functions.js.map +1 -1
- package/sdk/dist/namespaces/jobs.d.ts.map +1 -1
- package/sdk/dist/namespaces/jobs.js +8 -19
- package/sdk/dist/namespaces/jobs.js.map +1 -1
- package/sdk/dist/namespaces/projects.d.ts +4 -4
- package/sdk/dist/namespaces/projects.d.ts.map +1 -1
- package/sdk/dist/namespaces/projects.js +18 -42
- package/sdk/dist/namespaces/projects.js.map +1 -1
- package/sdk/dist/namespaces/secrets.d.ts.map +1 -1
- package/sdk/dist/namespaces/secrets.js +5 -10
- package/sdk/dist/namespaces/secrets.js.map +1 -1
- package/sdk/dist/namespaces/sender-domain.d.ts.map +1 -1
- package/sdk/dist/namespaces/sender-domain.js +6 -16
- package/sdk/dist/namespaces/sender-domain.js.map +1 -1
- package/sdk/dist/namespaces/subdomains.d.ts.map +1 -1
- package/sdk/dist/namespaces/subdomains.js +5 -10
- package/sdk/dist/namespaces/subdomains.js.map +1 -1
- package/sdk/dist/node/actions-node.d.ts.map +1 -1
- package/sdk/dist/node/actions-node.js +46 -15
- package/sdk/dist/node/actions-node.js.map +1 -1
- package/sdk/dist/node/credentials.d.ts +18 -4
- package/sdk/dist/node/credentials.d.ts.map +1 -1
- package/sdk/dist/node/credentials.js +37 -9
- package/sdk/dist/node/credentials.js.map +1 -1
- package/sdk/dist/node/index.d.ts +11 -2
- package/sdk/dist/node/index.d.ts.map +1 -1
- package/sdk/dist/node/index.js +26 -1
- package/sdk/dist/node/index.js.map +1 -1
- package/sdk/dist/node/target-profile.d.ts.map +1 -1
- package/sdk/dist/node/target-profile.js +6 -4
- package/sdk/dist/node/target-profile.js.map +1 -1
- package/sdk/dist/project-auth-classification.d.ts +125 -0
- package/sdk/dist/project-auth-classification.d.ts.map +1 -0
- package/sdk/dist/project-auth-classification.js +135 -0
- package/sdk/dist/project-auth-classification.js.map +1 -0
- package/sdk/dist/project-credentials.d.ts +4 -0
- package/sdk/dist/project-credentials.d.ts.map +1 -0
- package/sdk/dist/project-credentials.js +9 -0
- package/sdk/dist/project-credentials.js.map +1 -0
|
@@ -0,0 +1,438 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { describe, it } from "node:test";
|
|
3
|
+
import { chmodSync, mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import { tmpdir } from "node:os";
|
|
6
|
+
import {
|
|
7
|
+
compareSemver,
|
|
8
|
+
createUpdateCheckScheduler,
|
|
9
|
+
detectInstallContext,
|
|
10
|
+
doctorUpdateCheck,
|
|
11
|
+
emitUpdateNotice,
|
|
12
|
+
isCacheFresh,
|
|
13
|
+
readUpdateCache,
|
|
14
|
+
refreshUpdateCheck,
|
|
15
|
+
updateNoticeFromRecord,
|
|
16
|
+
upgradeNextActions,
|
|
17
|
+
writeUpdateCache,
|
|
18
|
+
} from "./update-check.mjs";
|
|
19
|
+
|
|
20
|
+
function tempDir() {
|
|
21
|
+
return mkdtempSync(join(tmpdir(), "run402-update-check-"));
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
async function withTemp(fn) {
|
|
25
|
+
const dir = tempDir();
|
|
26
|
+
try {
|
|
27
|
+
return await fn(dir);
|
|
28
|
+
} finally {
|
|
29
|
+
rmSync(dir, { recursive: true, force: true });
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function packageJson(dir, body) {
|
|
34
|
+
writeFileSync(join(dir, "package.json"), JSON.stringify(body, null, 2));
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function staleRecord(latest = "3.7.15") {
|
|
38
|
+
return {
|
|
39
|
+
current: "3.7.14",
|
|
40
|
+
latest,
|
|
41
|
+
checked_at: "2026-07-03T10:18:20.000Z",
|
|
42
|
+
source: "cache",
|
|
43
|
+
error: null,
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
describe("CLI update check semver and cache", () => {
|
|
48
|
+
it("compares normal, prerelease, current-newer, and invalid semver safely", () => {
|
|
49
|
+
assert.equal(compareSemver("3.7.14", "3.7.15"), -1);
|
|
50
|
+
assert.equal(compareSemver("3.7.15", "3.7.14"), 1);
|
|
51
|
+
assert.equal(compareSemver("3.7.15-beta.1", "3.7.15"), -1);
|
|
52
|
+
assert.equal(compareSemver("3.7.15", "3.7.15-beta.1"), 1);
|
|
53
|
+
assert.equal(compareSemver("not-semver", "3.7.15"), null);
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it("treats corrupt cache as absent and unwritable cache as fail-open", () => withTemp((dir) => {
|
|
57
|
+
const cachePath = join(dir, "cache.json");
|
|
58
|
+
writeFileSync(cachePath, "{");
|
|
59
|
+
assert.equal(readUpdateCache({ path: cachePath }), null);
|
|
60
|
+
|
|
61
|
+
mkdirSync(join(dir, "as-directory"));
|
|
62
|
+
assert.equal(writeUpdateCache(staleRecord(), { path: join(dir, "as-directory") }), false);
|
|
63
|
+
}));
|
|
64
|
+
|
|
65
|
+
it("tracks cache TTL from checked_at", () => {
|
|
66
|
+
const checked_at = "2026-07-03T10:00:00.000Z";
|
|
67
|
+
assert.equal(isCacheFresh({ checked_at }, { now: Date.parse("2026-07-03T11:00:00.000Z") }), true);
|
|
68
|
+
assert.equal(isCacheFresh({ checked_at }, { now: Date.parse("2026-07-05T11:00:00.000Z") }), false);
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it("records timeout and malformed latest checks as non-throwing failure cache", async () => withTemp(async (dir) => {
|
|
72
|
+
const timeout = await refreshUpdateCheck({
|
|
73
|
+
current: "3.7.14",
|
|
74
|
+
cachePath: join(dir, "timeout.json"),
|
|
75
|
+
timeoutMs: 1,
|
|
76
|
+
fetchImpl: async (_url, init) => new Promise((_resolve, reject) => {
|
|
77
|
+
init.signal.addEventListener("abort", () => {
|
|
78
|
+
const err = new Error("aborted");
|
|
79
|
+
err.name = "AbortError";
|
|
80
|
+
reject(err);
|
|
81
|
+
});
|
|
82
|
+
}),
|
|
83
|
+
});
|
|
84
|
+
assert.equal(timeout.ok, false);
|
|
85
|
+
assert.equal(timeout.error.code, "TIMEOUT");
|
|
86
|
+
|
|
87
|
+
const malformed = await refreshUpdateCheck({
|
|
88
|
+
current: "3.7.14",
|
|
89
|
+
cachePath: join(dir, "malformed.json"),
|
|
90
|
+
fetchImpl: async () => new Response(JSON.stringify({ version: "latest" }), {
|
|
91
|
+
status: 200,
|
|
92
|
+
headers: { "content-type": "application/json" },
|
|
93
|
+
}),
|
|
94
|
+
});
|
|
95
|
+
assert.equal(malformed.ok, false);
|
|
96
|
+
assert.equal(malformed.error.code, "INVALID_LATEST");
|
|
97
|
+
}));
|
|
98
|
+
|
|
99
|
+
it("uses custom npm registries and fails open when they are unavailable", async () => withTemp(async (dir) => {
|
|
100
|
+
let requested = "";
|
|
101
|
+
const result = await refreshUpdateCheck({
|
|
102
|
+
current: "3.7.14",
|
|
103
|
+
cachePath: join(dir, "custom-registry.json"),
|
|
104
|
+
env: { RUN402_NPM_REGISTRY: "https://registry.example.test/npm/" },
|
|
105
|
+
fetchImpl: async (url) => {
|
|
106
|
+
requested = String(url);
|
|
107
|
+
return new Response(JSON.stringify({ error: "down" }), {
|
|
108
|
+
status: 503,
|
|
109
|
+
headers: { "content-type": "application/json" },
|
|
110
|
+
});
|
|
111
|
+
},
|
|
112
|
+
});
|
|
113
|
+
assert.equal(result.ok, false);
|
|
114
|
+
assert.equal(result.error.code, "NETWORK_ERROR");
|
|
115
|
+
assert.equal(requested, "https://registry.example.test/npm/run402/latest");
|
|
116
|
+
assert.ok(readUpdateCache({ path: join(dir, "custom-registry.json") }));
|
|
117
|
+
}));
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
describe("CLI install-context detection", () => {
|
|
121
|
+
it("detects stale project-local npm installs and suggests a local mutation", () => withTemp((dir) => {
|
|
122
|
+
packageJson(dir, { devDependencies: { run402: "3.7.14" } });
|
|
123
|
+
mkdirSync(join(dir, "node_modules", ".bin"), { recursive: true });
|
|
124
|
+
const install = detectInstallContext({
|
|
125
|
+
cwd: dir,
|
|
126
|
+
execPath: join(dir, "node_modules", ".bin", "run402"),
|
|
127
|
+
env: { npm_config_user_agent: "npm/11.0.0 node/v22" },
|
|
128
|
+
});
|
|
129
|
+
assert.equal(install.kind, "local_project");
|
|
130
|
+
assert.equal(install.confidence, "high");
|
|
131
|
+
const [action] = upgradeNextActions({ install, cwd: dir });
|
|
132
|
+
assert.deepEqual(action.argv, ["npm", "install", "-D", "run402@latest"]);
|
|
133
|
+
assert.equal(action.mutates_project, true);
|
|
134
|
+
assert.equal(action.mutates_global_install, false);
|
|
135
|
+
}));
|
|
136
|
+
|
|
137
|
+
it("detects global npm installs and avoids mutating the project", () => withTemp((dir) => {
|
|
138
|
+
packageJson(dir, { devDependencies: { run402: "3.7.14" } });
|
|
139
|
+
const install = detectInstallContext({
|
|
140
|
+
cwd: dir,
|
|
141
|
+
execPath: "/usr/local/lib/node_modules/run402/cli.mjs",
|
|
142
|
+
env: { npm_config_prefix: "/usr/local" },
|
|
143
|
+
});
|
|
144
|
+
assert.equal(install.kind, "global_npm");
|
|
145
|
+
const [action] = upgradeNextActions({ install, cwd: dir });
|
|
146
|
+
assert.deepEqual(action.argv, ["npm", "install", "-g", "run402@latest"]);
|
|
147
|
+
assert.equal(action.mutates_project, false);
|
|
148
|
+
assert.equal(action.mutates_global_install, true);
|
|
149
|
+
}));
|
|
150
|
+
|
|
151
|
+
it("detects npx/npm exec and suggests an ephemeral latest rerun", () => withTemp((dir) => {
|
|
152
|
+
const install = detectInstallContext({
|
|
153
|
+
cwd: dir,
|
|
154
|
+
execPath: join(dir, ".npm", "_npx", "abc", "node_modules", "run402", "cli.mjs"),
|
|
155
|
+
argv: ["node", "run402", "up", "--json"],
|
|
156
|
+
env: { npm_command: "exec", npm_config_user_agent: "npm/11 node/v22" },
|
|
157
|
+
});
|
|
158
|
+
assert.equal(install.kind, "ephemeral_exec");
|
|
159
|
+
const [action] = upgradeNextActions({ install, cwd: dir, command: ["run402", "up", "--json"] });
|
|
160
|
+
assert.deepEqual(action.argv, ["npx", "-y", "run402@latest", "up", "--json"]);
|
|
161
|
+
assert.equal(action.mutates_project, false);
|
|
162
|
+
}));
|
|
163
|
+
|
|
164
|
+
it("handles pnpm symlink layouts, Yarn PnP, Bun local/bunx, and Windows shims", () => withTemp((dir) => {
|
|
165
|
+
packageJson(dir, { packageManager: "pnpm@10.0.0", devDependencies: { run402: "3.7.14" } });
|
|
166
|
+
let install = detectInstallContext({
|
|
167
|
+
cwd: dir,
|
|
168
|
+
execPath: join(dir, "node_modules", ".pnpm", "run402@3.7.14", "node_modules", "run402", "cli.mjs"),
|
|
169
|
+
env: {},
|
|
170
|
+
});
|
|
171
|
+
assert.equal(install.kind, "local_project");
|
|
172
|
+
assert.equal(upgradeNextActions({ install })[0].argv[0], "pnpm");
|
|
173
|
+
|
|
174
|
+
packageJson(dir, { packageManager: "yarn@4.0.0", devDependencies: { run402: "3.7.14" } });
|
|
175
|
+
install = detectInstallContext({
|
|
176
|
+
cwd: dir,
|
|
177
|
+
execPath: join(dir, ".yarn", "cache", "run402.zip", "node_modules", "run402", "cli.mjs"),
|
|
178
|
+
env: { NODE_OPTIONS: "--require ./.pnp.cjs" },
|
|
179
|
+
});
|
|
180
|
+
assert.equal(install.kind, "local_project");
|
|
181
|
+
assert.equal(install.package_manager, "yarn");
|
|
182
|
+
|
|
183
|
+
packageJson(dir, { packageManager: "bun@1.2.0", devDependencies: { run402: "3.7.14" } });
|
|
184
|
+
install = detectInstallContext({
|
|
185
|
+
cwd: dir,
|
|
186
|
+
execPath: join(dir, "node_modules", ".bin", "run402"),
|
|
187
|
+
env: { npm_config_user_agent: "bun/1.2.0" },
|
|
188
|
+
});
|
|
189
|
+
assert.equal(install.kind, "local_project");
|
|
190
|
+
assert.equal(upgradeNextActions({ install })[0].argv[0], "bun");
|
|
191
|
+
|
|
192
|
+
install = detectInstallContext({
|
|
193
|
+
cwd: dir,
|
|
194
|
+
execPath: "C:\\Users\\me\\AppData\\Local\\npm-cache\\_npx\\abc\\node_modules\\run402\\cli.mjs",
|
|
195
|
+
argv: ["node", "run402"],
|
|
196
|
+
env: { npm_config_npx: "true" },
|
|
197
|
+
platform: "win32",
|
|
198
|
+
});
|
|
199
|
+
assert.equal(install.kind, "ephemeral_exec");
|
|
200
|
+
}));
|
|
201
|
+
|
|
202
|
+
it("detects workspace-root local installs from package subdirectories", () => withTemp((dir) => {
|
|
203
|
+
packageJson(dir, {
|
|
204
|
+
packageManager: "pnpm@10.0.0",
|
|
205
|
+
workspaces: ["packages/*"],
|
|
206
|
+
devDependencies: { run402: "3.7.14" },
|
|
207
|
+
});
|
|
208
|
+
const appDir = join(dir, "packages", "app");
|
|
209
|
+
mkdirSync(appDir, { recursive: true });
|
|
210
|
+
packageJson(appDir, { name: "app", private: true });
|
|
211
|
+
const install = detectInstallContext({
|
|
212
|
+
cwd: appDir,
|
|
213
|
+
execPath: join(dir, "node_modules", ".bin", "run402"),
|
|
214
|
+
env: {},
|
|
215
|
+
});
|
|
216
|
+
assert.equal(install.kind, "local_project");
|
|
217
|
+
assert.equal(install.cwd, dir);
|
|
218
|
+
assert.equal(install.package_root, dir);
|
|
219
|
+
const [action] = upgradeNextActions({ install, cwd: appDir });
|
|
220
|
+
assert.deepEqual(action.argv, ["pnpm", "add", "-D", "run402@latest"]);
|
|
221
|
+
assert.equal(action.cwd, dir);
|
|
222
|
+
assert.equal(action.mutates_project, true);
|
|
223
|
+
}));
|
|
224
|
+
|
|
225
|
+
it("uses packageManager over conflicting lockfiles and gives custom paths low-confidence doctor guidance", () => withTemp((dir) => {
|
|
226
|
+
packageJson(dir, { packageManager: "pnpm@10.0.0" });
|
|
227
|
+
writeFileSync(join(dir, "package-lock.json"), "{}");
|
|
228
|
+
writeFileSync(join(dir, "yarn.lock"), "");
|
|
229
|
+
const install = detectInstallContext({
|
|
230
|
+
cwd: dir,
|
|
231
|
+
execPath: join(dir, "dist", "run402-dev.mjs"),
|
|
232
|
+
env: {},
|
|
233
|
+
});
|
|
234
|
+
assert.equal(install.kind, "custom_path");
|
|
235
|
+
assert.equal(install.package_manager, "pnpm");
|
|
236
|
+
const [action] = upgradeNextActions({ install, cwd: dir });
|
|
237
|
+
assert.deepEqual(action.argv, ["run402", "doctor", "--refresh"]);
|
|
238
|
+
assert.equal(action.confidence, "low");
|
|
239
|
+
}));
|
|
240
|
+
|
|
241
|
+
it("keeps nvm/asdf global installs separate from Volta/Corepack-style ambiguous shims", () => withTemp((dir) => {
|
|
242
|
+
let install = detectInstallContext({
|
|
243
|
+
cwd: dir,
|
|
244
|
+
execPath: "/Users/me/.volta/tools/image/packages/run402/bin/run402",
|
|
245
|
+
env: {},
|
|
246
|
+
});
|
|
247
|
+
assert.equal(install.kind, "package_manager_shim");
|
|
248
|
+
assert.equal(install.confidence, "low");
|
|
249
|
+
assert.deepEqual(upgradeNextActions({ install })[0].argv, ["run402", "doctor", "--refresh"]);
|
|
250
|
+
|
|
251
|
+
install = detectInstallContext({
|
|
252
|
+
cwd: dir,
|
|
253
|
+
execPath: "/Users/me/.nvm/versions/node/v24.0.0/lib/node_modules/run402/cli.mjs",
|
|
254
|
+
env: {},
|
|
255
|
+
});
|
|
256
|
+
assert.equal(install.kind, "global_npm");
|
|
257
|
+
assert.deepEqual(upgradeNextActions({ install })[0].argv, ["npm", "install", "-g", "run402@latest"]);
|
|
258
|
+
|
|
259
|
+
install = detectInstallContext({
|
|
260
|
+
cwd: dir,
|
|
261
|
+
execPath: "/Users/me/.asdf/installs/nodejs/24.0.0/lib/node_modules/run402/cli.mjs",
|
|
262
|
+
env: {},
|
|
263
|
+
});
|
|
264
|
+
assert.equal(install.kind, "global_npm");
|
|
265
|
+
|
|
266
|
+
install = detectInstallContext({
|
|
267
|
+
cwd: dir,
|
|
268
|
+
execPath: "/Users/me/.cache/corepack/run402",
|
|
269
|
+
env: { npm_config_user_agent: "yarn/4.0.0 node/v24" },
|
|
270
|
+
});
|
|
271
|
+
assert.equal(install.kind, "custom_path");
|
|
272
|
+
assert.equal(install.confidence, "low");
|
|
273
|
+
assert.equal(upgradeNextActions({ install })[0].mutates_project, false);
|
|
274
|
+
}));
|
|
275
|
+
|
|
276
|
+
it("distinguishes stale local shadowing from explicit latest npx reruns", () => withTemp((dir) => {
|
|
277
|
+
packageJson(dir, { devDependencies: { run402: "3.7.14" } });
|
|
278
|
+
let install = detectInstallContext({
|
|
279
|
+
cwd: dir,
|
|
280
|
+
execPath: join(dir, ".npm", "_npx", "shadow", "node_modules", "run402", "cli.mjs"),
|
|
281
|
+
argv: ["npx", "run402", "up"],
|
|
282
|
+
env: { npm_config_npx: "true" },
|
|
283
|
+
});
|
|
284
|
+
assert.equal(install.kind, "ephemeral_exec");
|
|
285
|
+
assert.deepEqual(
|
|
286
|
+
upgradeNextActions({ install, cwd: dir, command: ["run402", "up"] })[0].argv,
|
|
287
|
+
["npx", "-y", "run402@latest", "up"],
|
|
288
|
+
);
|
|
289
|
+
|
|
290
|
+
install = detectInstallContext({
|
|
291
|
+
cwd: dir,
|
|
292
|
+
execPath: join(dir, ".npm", "_npx", "latest", "node_modules", "run402", "cli.mjs"),
|
|
293
|
+
argv: ["npx", "-y", "run402@latest", "up"],
|
|
294
|
+
env: { npm_command: "exec" },
|
|
295
|
+
});
|
|
296
|
+
assert.equal(install.kind, "ephemeral_exec");
|
|
297
|
+
assert.deepEqual(
|
|
298
|
+
upgradeNextActions({ install, cwd: dir, command: ["run402", "up"] })[0].argv,
|
|
299
|
+
["npx", "-y", "run402@latest", "up"],
|
|
300
|
+
);
|
|
301
|
+
}));
|
|
302
|
+
});
|
|
303
|
+
|
|
304
|
+
describe("CLI update notices and scheduler", () => {
|
|
305
|
+
it("builds structured stale notices with argv/cwd/confidence/mutation flags", () => withTemp((dir) => {
|
|
306
|
+
packageJson(dir, { devDependencies: { run402: "3.7.14" } });
|
|
307
|
+
const notice = updateNoticeFromRecord(staleRecord(), {
|
|
308
|
+
cwd: dir,
|
|
309
|
+
execPath: join(dir, "node_modules", ".bin", "run402"),
|
|
310
|
+
current: "3.7.14",
|
|
311
|
+
command: ["run402", "up", "--json"],
|
|
312
|
+
});
|
|
313
|
+
assert.equal(notice.type, "cli.update_available");
|
|
314
|
+
assert.equal(notice.source, "cache");
|
|
315
|
+
assert.equal(notice.install_context, "local_project");
|
|
316
|
+
assert.deepEqual(notice.next_actions[0].argv, ["npm", "install", "-D", "run402@latest"]);
|
|
317
|
+
assert.equal(notice.next_actions[0].cwd, dir);
|
|
318
|
+
assert.equal(notice.next_actions[0].confidence, "high");
|
|
319
|
+
assert.equal(notice.next_actions[0].mutates_project, true);
|
|
320
|
+
}));
|
|
321
|
+
|
|
322
|
+
it("does not emit for invalid latest, prerelease newer current, current newer, or opt-out", () => withTemp((dir) => {
|
|
323
|
+
assert.equal(updateNoticeFromRecord(staleRecord("wat"), { cwd: dir, current: "3.7.14" }), null);
|
|
324
|
+
assert.equal(updateNoticeFromRecord(staleRecord("3.7.15-beta.1"), { cwd: dir, current: "3.7.15" }), null);
|
|
325
|
+
assert.equal(updateNoticeFromRecord(staleRecord("3.7.14"), { cwd: dir, current: "3.7.15" }), null);
|
|
326
|
+
assert.equal(updateNoticeFromRecord(staleRecord("3.7.15"), {
|
|
327
|
+
cwd: dir,
|
|
328
|
+
current: "3.7.14",
|
|
329
|
+
env: { RUN402_NO_UPDATE_CHECK: "1" },
|
|
330
|
+
}), null);
|
|
331
|
+
}));
|
|
332
|
+
|
|
333
|
+
it("keeps notices off success stdout except json-stream structured events", () => {
|
|
334
|
+
const notice = { type: "cli.update_available", schema_version: 1 };
|
|
335
|
+
const stdout = [];
|
|
336
|
+
const stderr = [];
|
|
337
|
+
assert.equal(emitUpdateNotice(notice, {
|
|
338
|
+
stdout: (line) => stdout.push(line),
|
|
339
|
+
stderr: (line) => stderr.push(line),
|
|
340
|
+
}), true);
|
|
341
|
+
assert.deepEqual(stdout, []);
|
|
342
|
+
assert.equal(JSON.parse(stderr[0]).type, "cli.update_available");
|
|
343
|
+
|
|
344
|
+
stdout.length = 0;
|
|
345
|
+
stderr.length = 0;
|
|
346
|
+
emitUpdateNotice(notice, {
|
|
347
|
+
jsonStream: true,
|
|
348
|
+
quiet: true,
|
|
349
|
+
stdout: (line) => stdout.push(line),
|
|
350
|
+
stderr: (line) => stderr.push(line),
|
|
351
|
+
});
|
|
352
|
+
assert.equal(JSON.parse(stdout[0]).type, "cli.update_available");
|
|
353
|
+
assert.deepEqual(stderr, []);
|
|
354
|
+
});
|
|
355
|
+
|
|
356
|
+
it("normal commands use cache immediately, skip live checks in CI/non-TTY, and honor explicit opt-in", async () => withTemp(async (dir) => {
|
|
357
|
+
const cachePath = join(dir, "cache.json");
|
|
358
|
+
writeUpdateCache(staleRecord(), { path: cachePath });
|
|
359
|
+
let fetchCalls = 0;
|
|
360
|
+
let scheduler = createUpdateCheckScheduler({
|
|
361
|
+
cwd: dir,
|
|
362
|
+
cachePath,
|
|
363
|
+
current: "3.7.14",
|
|
364
|
+
env: { CI: "true" },
|
|
365
|
+
stderrIsTTY: false,
|
|
366
|
+
stdoutIsTTY: false,
|
|
367
|
+
fetchImpl: async () => {
|
|
368
|
+
fetchCalls += 1;
|
|
369
|
+
return new Response(JSON.stringify({ version: "3.7.16" }), {
|
|
370
|
+
status: 200,
|
|
371
|
+
headers: { "content-type": "application/json" },
|
|
372
|
+
});
|
|
373
|
+
},
|
|
374
|
+
});
|
|
375
|
+
assert.equal(scheduler.cachedNotice.type, "cli.update_available");
|
|
376
|
+
assert.equal(scheduler.livePromise, null);
|
|
377
|
+
assert.equal(fetchCalls, 0);
|
|
378
|
+
|
|
379
|
+
chmodSync(cachePath, 0o600);
|
|
380
|
+
scheduler = createUpdateCheckScheduler({
|
|
381
|
+
cwd: dir,
|
|
382
|
+
cachePath,
|
|
383
|
+
current: "3.7.14",
|
|
384
|
+
env: { CI: "true", RUN402_UPDATE_CHECK: "1" },
|
|
385
|
+
stderrIsTTY: false,
|
|
386
|
+
stdoutIsTTY: false,
|
|
387
|
+
fetchImpl: async () => {
|
|
388
|
+
fetchCalls += 1;
|
|
389
|
+
return new Response(JSON.stringify({ version: "3.7.16" }), {
|
|
390
|
+
status: 200,
|
|
391
|
+
headers: { "content-type": "application/json" },
|
|
392
|
+
});
|
|
393
|
+
},
|
|
394
|
+
now: Date.parse("2026-07-05T10:18:20.000Z"),
|
|
395
|
+
});
|
|
396
|
+
assert.ok(scheduler.livePromise);
|
|
397
|
+
await scheduler.livePromise;
|
|
398
|
+
assert.equal(fetchCalls, 1);
|
|
399
|
+
assert.equal(scheduler.getCompletedLiveNotice().latest, "3.7.16");
|
|
400
|
+
}));
|
|
401
|
+
|
|
402
|
+
it("doctor reports stale, unknown, skipped, and refresh states without failing other checks", async () => withTemp(async (dir) => {
|
|
403
|
+
packageJson(dir, { devDependencies: { run402: "3.7.14" } });
|
|
404
|
+
const cachePath = join(dir, "cache.json");
|
|
405
|
+
writeUpdateCache(staleRecord(), { path: cachePath });
|
|
406
|
+
let check = await doctorUpdateCheck({
|
|
407
|
+
cwd: dir,
|
|
408
|
+
execPath: join(dir, "node_modules", ".bin", "run402"),
|
|
409
|
+
current: "3.7.14",
|
|
410
|
+
cachePath,
|
|
411
|
+
});
|
|
412
|
+
assert.equal(check.status, "warning");
|
|
413
|
+
assert.equal(check.value.next_actions[0].mutates_project, true);
|
|
414
|
+
|
|
415
|
+
check = await doctorUpdateCheck({ cwd: dir, current: "3.7.14", cachePath: join(dir, "missing.json") });
|
|
416
|
+
assert.equal(check.status, "unknown");
|
|
417
|
+
|
|
418
|
+
check = await doctorUpdateCheck({
|
|
419
|
+
cwd: dir,
|
|
420
|
+
current: "3.7.14",
|
|
421
|
+
cachePath,
|
|
422
|
+
env: { RUN402_NO_UPDATE_CHECK: "1" },
|
|
423
|
+
});
|
|
424
|
+
assert.equal(check.status, "skipped");
|
|
425
|
+
|
|
426
|
+
check = await doctorUpdateCheck({
|
|
427
|
+
cwd: dir,
|
|
428
|
+
current: "3.7.14",
|
|
429
|
+
cachePath,
|
|
430
|
+
refresh: true,
|
|
431
|
+
fetchImpl: async () => new Response(JSON.stringify({ version: "3.7.14" }), {
|
|
432
|
+
status: 200,
|
|
433
|
+
headers: { "content-type": "application/json" },
|
|
434
|
+
}),
|
|
435
|
+
});
|
|
436
|
+
assert.equal(check.status, "ok");
|
|
437
|
+
}));
|
|
438
|
+
});
|
package/package.json
CHANGED
package/sdk/core-dist/config.js
CHANGED
|
@@ -119,8 +119,17 @@ export function getConfigDir() {
|
|
|
119
119
|
return profile === DEFAULT_PROFILE ? base : join(base, "profiles", profile);
|
|
120
120
|
}
|
|
121
121
|
export function getKeystorePath() {
|
|
122
|
+
return getProjectCredentialsPath();
|
|
123
|
+
}
|
|
124
|
+
export function getLegacyProjectsPath() {
|
|
122
125
|
return join(getConfigDir(), "projects.json");
|
|
123
126
|
}
|
|
127
|
+
export function getProjectCredentialsPath() {
|
|
128
|
+
return join(getConfigDir(), "credentials", "project-keys.v1.json");
|
|
129
|
+
}
|
|
130
|
+
export function getProfileStatePath() {
|
|
131
|
+
return join(getConfigDir(), "state.json");
|
|
132
|
+
}
|
|
124
133
|
export function getApiTargetConfigPath() {
|
|
125
134
|
return join(getConfigDir(), "target.json");
|
|
126
135
|
}
|