impel-cli 0.20.63-canary.5 → 0.20.63-canary.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/package.json +1 -1
- package/scripts/npm-dist-tags.mjs +46 -3
- package/src/managedApps.js +18 -5
- package/src/remote/transfer.js +10 -8
- package/src/windowsGit.js +2 -1
package/package.json
CHANGED
|
@@ -5,6 +5,15 @@ import { fileURLToPath } from "node:url";
|
|
|
5
5
|
|
|
6
6
|
const DIST_TAGS_URL = "https://registry.npmjs.org/-/package/impel-cli/dist-tags";
|
|
7
7
|
|
|
8
|
+
// npm accepts a version and returns success before every registry edge exposes
|
|
9
|
+
// its new dist-tag. The post-publish verifier therefore treats a stale canary
|
|
10
|
+
// tag as propagation, not proof that npm publish failed. Five minutes stays
|
|
11
|
+
// comfortably inside the publish job's 15-minute timeout.
|
|
12
|
+
const CANARY_TAG_VERIFY_ATTEMPTS = 20;
|
|
13
|
+
const CANARY_TAG_VERIFY_DELAY_MS = 15_000;
|
|
14
|
+
|
|
15
|
+
class CanaryDistTagMismatchError extends Error {}
|
|
16
|
+
|
|
8
17
|
export function assertCanaryDistTags({ beforeLatest, expectedCanary, tags }) {
|
|
9
18
|
const latest = tags.latest ?? "";
|
|
10
19
|
const canary = tags.canary ?? "";
|
|
@@ -15,11 +24,46 @@ export function assertCanaryDistTags({ beforeLatest, expectedCanary, tags }) {
|
|
|
15
24
|
);
|
|
16
25
|
}
|
|
17
26
|
if (canary !== expectedCanary) {
|
|
18
|
-
throw new
|
|
27
|
+
throw new CanaryDistTagMismatchError(
|
|
28
|
+
`dist-tags.canary is ${canary || "<none>"}, expected ${expectedCanary}.`,
|
|
29
|
+
);
|
|
19
30
|
}
|
|
20
31
|
return { latest, canary };
|
|
21
32
|
}
|
|
22
33
|
|
|
34
|
+
/**
|
|
35
|
+
* Verify npm's public dist-tags after publish while allowing the canary tag to
|
|
36
|
+
* propagate. A moved `latest` remains an immediate failure: that is a release
|
|
37
|
+
* invariant violation, not ordinary registry propagation.
|
|
38
|
+
*/
|
|
39
|
+
export async function verifyCanaryDistTags({
|
|
40
|
+
beforeLatest,
|
|
41
|
+
expectedCanary,
|
|
42
|
+
readTags = readDistTags,
|
|
43
|
+
attempts = CANARY_TAG_VERIFY_ATTEMPTS,
|
|
44
|
+
delayMs = CANARY_TAG_VERIFY_DELAY_MS,
|
|
45
|
+
sleep = (milliseconds) => new Promise((resolve) => setTimeout(resolve, milliseconds)),
|
|
46
|
+
}) {
|
|
47
|
+
let lastObservedCanary = "";
|
|
48
|
+
let lastError;
|
|
49
|
+
for (let attempt = 1; attempt <= attempts; attempt += 1) {
|
|
50
|
+
const tags = await readTags({ bustCache: true });
|
|
51
|
+
try {
|
|
52
|
+
return assertCanaryDistTags({ beforeLatest, expectedCanary, tags });
|
|
53
|
+
} catch (error) {
|
|
54
|
+
if (!(error instanceof CanaryDistTagMismatchError)) throw error;
|
|
55
|
+
lastObservedCanary = tags.canary ?? "";
|
|
56
|
+
lastError = error;
|
|
57
|
+
if (attempt < attempts) await sleep(delayMs);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
throw new Error(
|
|
61
|
+
`npm accepted the canary publish but did not expose it after ${attempts} dist-tag checks: `
|
|
62
|
+
+ `${lastError?.message || `last dist-tags.canary was ${lastObservedCanary || "<none>"}.`} `
|
|
63
|
+
+ "This is registry propagation lag, not an unpublished tarball.",
|
|
64
|
+
);
|
|
65
|
+
}
|
|
66
|
+
|
|
23
67
|
async function readDistTags({ allowMissing, bustCache = false } = {}) {
|
|
24
68
|
const url = bustCache ? `${DIST_TAGS_URL}?t=${Date.now()}` : DIST_TAGS_URL;
|
|
25
69
|
let lastError;
|
|
@@ -67,8 +111,7 @@ async function main([command, ...args]) {
|
|
|
67
111
|
}
|
|
68
112
|
if (command === "assert-canary" && args.length === 2) {
|
|
69
113
|
const [expectedCanary, beforeLatest] = args;
|
|
70
|
-
const
|
|
71
|
-
const { latest } = assertCanaryDistTags({ expectedCanary, beforeLatest, tags });
|
|
114
|
+
const { latest } = await verifyCanaryDistTags({ expectedCanary, beforeLatest });
|
|
72
115
|
appendSummary([
|
|
73
116
|
"## Canary published",
|
|
74
117
|
"",
|
package/src/managedApps.js
CHANGED
|
@@ -2,14 +2,27 @@ import fs from "node:fs";
|
|
|
2
2
|
import os from "node:os";
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import { spawn } from "node:child_process";
|
|
5
|
+
import { RUNTIME_BRAND } from "./runtimeBrand.js";
|
|
5
6
|
|
|
6
7
|
const TENANT = /^[a-z0-9](?:[a-z0-9._-]{0,62}[a-z0-9])?$/u;
|
|
7
8
|
const APPLICATIONS_BY_TOOL = Object.freeze({
|
|
8
9
|
claude: ["claude"],
|
|
9
10
|
codex: ["codex", "chatgpt"],
|
|
10
11
|
});
|
|
11
|
-
|
|
12
|
-
|
|
12
|
+
// Managed-app discovery is channel scoped. The canary installer may replace
|
|
13
|
+
// the machine-global runner, but its descriptors and credentials must remain
|
|
14
|
+
// disjoint from stable state so reinstall/recovery never crosses channels.
|
|
15
|
+
const BRAND_DISPLAY_PREFIX = RUNTIME_BRAND.channel === "canary"
|
|
16
|
+
&& RUNTIME_BRAND.apps.displayPrefix === "Impel"
|
|
17
|
+
? "Impel Canary"
|
|
18
|
+
: RUNTIME_BRAND.apps.displayPrefix;
|
|
19
|
+
const MAC_RUNNER = `/Applications/${BRAND_DISPLAY_PREFIX} Managed App Runner.app/Contents/Library/LaunchServices/impel-managed-runner`;
|
|
20
|
+
const MAC_DESCRIPTOR_ROOT = `/Library/Application Support/${BRAND_DISPLAY_PREFIX}/Managed App Runner/descriptors`;
|
|
21
|
+
const CONFIG_ROOT_SEGMENT = RUNTIME_BRAND.cli.configNamespace;
|
|
22
|
+
|
|
23
|
+
function credentialPath(home, tenantId) {
|
|
24
|
+
return path.join(home, ".config", CONFIG_ROOT_SEGMENT, "apps", "credentials", `${tenantId}.json`);
|
|
25
|
+
}
|
|
13
26
|
|
|
14
27
|
function regularFile(file) {
|
|
15
28
|
try {
|
|
@@ -44,7 +57,7 @@ function parseCandidate(manifestPath, launcher, applicationId, platform, home) {
|
|
|
44
57
|
|| String(tenantId).includes("..")
|
|
45
58
|
|| manifest?.logicalApplicationId !== `${tenantId}.${applicationId}`
|
|
46
59
|
) return null;
|
|
47
|
-
const credential =
|
|
60
|
+
const credential = credentialPath(home, tenantId);
|
|
48
61
|
if (!regularFile(credential)) return null;
|
|
49
62
|
return {
|
|
50
63
|
tenantId,
|
|
@@ -101,7 +114,7 @@ function runnerEnvelopeCandidate(envelopePath, launcher, applicationId, home) {
|
|
|
101
114
|
|| String(tenantId).includes("..")
|
|
102
115
|
|| payload?.logicalApplicationId !== `${tenantId}.${applicationId}`
|
|
103
116
|
) return null;
|
|
104
|
-
const credential =
|
|
117
|
+
const credential = credentialPath(home, tenantId);
|
|
105
118
|
if (!regularFile(credential)) return null;
|
|
106
119
|
return {
|
|
107
120
|
tenantId,
|
|
@@ -185,7 +198,7 @@ export function installedManagedAppCandidates(tool, {
|
|
|
185
198
|
platform = process.platform,
|
|
186
199
|
home = os.homedir(),
|
|
187
200
|
applicationsRoot = platform === "win32"
|
|
188
|
-
? path.join(process.env.ProgramFiles || "C:\\Program Files",
|
|
201
|
+
? path.join(process.env.ProgramFiles || "C:\\Program Files", BRAND_DISPLAY_PREFIX, "Apps")
|
|
189
202
|
: "/Applications",
|
|
190
203
|
descriptorRoot = MAC_DESCRIPTOR_ROOT,
|
|
191
204
|
runnerLauncher = MAC_RUNNER,
|
package/src/remote/transfer.js
CHANGED
|
@@ -5,6 +5,7 @@ import { tenantCliProfilePaths } from "../cliProfiles.js";
|
|
|
5
5
|
import { redactSecretText } from "../config.js";
|
|
6
6
|
import { runBuffer, runCapture, runInteractive, sleep } from "./process.js";
|
|
7
7
|
import { runPaths } from "./state.js";
|
|
8
|
+
import { RUNTIME_BRAND } from "../runtimeBrand.js";
|
|
8
9
|
|
|
9
10
|
// LEGACY ONLY: this module's SSH upload and PAT-bearing remote configuration
|
|
10
11
|
// are isolated to portable-legacy. Broker-backed modes use immutable workspace
|
|
@@ -15,6 +16,7 @@ const REMOTE_REPO = "/workspace/repo";
|
|
|
15
16
|
const REMOTE_BUNDLE = "/tmp/impel-remote-repo.bundle";
|
|
16
17
|
const SESSION_ID_RE = /^[A-Za-z0-9][A-Za-z0-9._:-]{0,255}$/u;
|
|
17
18
|
const ENV_NAME_RE = /^[A-Za-z_][A-Za-z0-9_]*$/u;
|
|
19
|
+
export const REMOTE_CONFIG_ROOT = `/home/agent/.config/${RUNTIME_BRAND.cli.configNamespace}`;
|
|
18
20
|
|
|
19
21
|
function git(args, options = {}) {
|
|
20
22
|
return runCapture(process.env.IMPEL_REMOTE_GIT_BIN || "git", args, options).stdout.trim();
|
|
@@ -190,16 +192,16 @@ export function uploadRemoteConfiguration(state, {
|
|
|
190
192
|
const environmentFile = Object.entries(selectedEnvironment)
|
|
191
193
|
.map(([name, value]) => `export ${name}=${shellQuote(value)}`)
|
|
192
194
|
.join("\n") + "\n";
|
|
193
|
-
const profileRoot =
|
|
195
|
+
const profileRoot = `${REMOTE_CONFIG_ROOT}/cli/tenants/${tenant.tenantId}`;
|
|
194
196
|
const claudeSettings = {
|
|
195
197
|
permissions: { defaultMode: "bypassPermissions" },
|
|
196
198
|
sandbox: { enabled: false },
|
|
197
199
|
};
|
|
198
200
|
const files = [
|
|
199
|
-
{ path:
|
|
200
|
-
{ path:
|
|
201
|
+
{ path: `${REMOTE_CONFIG_ROOT}/config.json`, contents: `${JSON.stringify(remoteConfig, null, 2)}\n` },
|
|
202
|
+
{ path: `${REMOTE_CONFIG_ROOT}/remote/env.sh`, contents: environmentFile },
|
|
201
203
|
{
|
|
202
|
-
path:
|
|
204
|
+
path: `${REMOTE_CONFIG_ROOT}/remote/run.json`,
|
|
203
205
|
contents: `${JSON.stringify({
|
|
204
206
|
runId: state.runId,
|
|
205
207
|
credentialTokenId: credential.tokenId,
|
|
@@ -214,12 +216,12 @@ const path = require("node:path");
|
|
|
214
216
|
const payload = JSON.parse(fs.readFileSync(0, "utf8"));
|
|
215
217
|
for (const file of payload.files) {
|
|
216
218
|
const target = path.resolve(file.path);
|
|
217
|
-
if (!target.startsWith("/
|
|
219
|
+
if (!target.startsWith(payload.root + "/")) throw new Error("refusing remote path");
|
|
218
220
|
fs.mkdirSync(path.dirname(target), { recursive: true, mode: 0o700 });
|
|
219
221
|
fs.writeFileSync(target, file.contents, { mode: 0o600 });
|
|
220
222
|
fs.chmodSync(target, 0o600);
|
|
221
223
|
}
|
|
222
|
-
`, { files });
|
|
224
|
+
`, { files, root: REMOTE_CONFIG_ROOT });
|
|
223
225
|
}
|
|
224
226
|
|
|
225
227
|
export function uploadRepository(state, repository) {
|
|
@@ -294,8 +296,8 @@ export function transferSessionCheckpoint(state, { provider, sessionId, tenantId
|
|
|
294
296
|
const profiles = tenantCliProfilePaths(tenantId);
|
|
295
297
|
const localRoot = provider === "claude" ? profiles.claudeConfigDir : profiles.codexHome;
|
|
296
298
|
const remoteRoot = provider === "claude"
|
|
297
|
-
?
|
|
298
|
-
:
|
|
299
|
+
? `${REMOTE_CONFIG_ROOT}/cli/tenants/${tenantId}/claude`
|
|
300
|
+
: `${REMOTE_CONFIG_ROOT}/cli/tenants/${tenantId}/codex`;
|
|
299
301
|
const files = collectSessionFiles(localRoot, sessionId);
|
|
300
302
|
if (files.length === 0) {
|
|
301
303
|
throw new Error(`could not find ${provider} session ${sessionId} in the selected Impel CLI profile`);
|
package/src/windowsGit.js
CHANGED
|
@@ -15,6 +15,7 @@ import path from "node:path";
|
|
|
15
15
|
|
|
16
16
|
import { nativeCommandInvocation, nativeSpawnInvocation } from "./nativeProcess.js";
|
|
17
17
|
import { renameWithWindowsRetry } from "./windowsFs.js";
|
|
18
|
+
import { RUNTIME_BRAND } from "./runtimeBrand.js";
|
|
18
19
|
|
|
19
20
|
/**
|
|
20
21
|
* The exact MinGit release the CLI may install. Never use `latest` or an
|
|
@@ -52,7 +53,7 @@ const POWERSHELL_ARGS = [
|
|
|
52
53
|
|
|
53
54
|
/** Root of the Impel-managed MinGit install. */
|
|
54
55
|
export function managedGitRoot(homeDir = os.homedir()) {
|
|
55
|
-
return path.join(homeDir, ".config",
|
|
56
|
+
return path.join(homeDir, ".config", RUNTIME_BRAND.cli.configNamespace, "tools", "git");
|
|
56
57
|
}
|
|
57
58
|
|
|
58
59
|
/** The PATH-safe git.exe inside a MinGit root (MinGit unpacks `cmd/` at top level). */
|