impel-cli 0.20.38 → 0.20.40
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 -3
- package/RELEASE_NOTES.md +36 -0
- package/bin/impel.js +10 -1
- package/docs/native-agent-host-capability-matrix.md +4 -3
- package/package.json +1 -1
- package/scripts/capture-release-fixtures.mjs +56 -0
- package/scripts/clean-machine/README.md +51 -0
- package/scripts/clean-machine/run-clean-windows.sh +115 -0
- package/scripts/clean-machine/userdata.ps1.template +102 -0
- package/scripts/regen-managed-artifacts.mjs +475 -0
- package/scripts/verify-vendor-pins.mjs +207 -0
- package/src/agents.js +47 -15
- package/src/apps.js +270 -82
- package/src/cliProfiles.js +1 -1
- package/src/codexSecurity.js +21 -0
- package/src/commands/apps.js +25 -2
- package/src/commands/converge.js +17 -1
- package/src/commands/mcp.js +1 -1
- package/src/commands/sessions.js +44 -11
- package/src/commands/setup.js +17 -1
- package/src/commands/token.js +23 -3
- package/src/commands/update.js +144 -16
- package/src/installRecovery/engine.js +45 -0
- package/src/macSetup.js +26 -8
- package/src/modelCatalog.js +103 -0
- package/src/nativeAgentTransport.js +13 -4
- package/src/platformSetup.js +9 -3
- package/src/vendorCliBinaries.js +36 -7
- package/src/verbatimRelay.js +9 -1
- package/src/windowsSetup.js +20 -0
- package/src/codexCliCatalog.js +0 -56
package/README.md
CHANGED
|
@@ -203,9 +203,10 @@ Claude selection.
|
|
|
203
203
|
Codex loads a generated, tenant-bound profile with the ultra-fast Codex Spark
|
|
204
204
|
model at low reasoning for the thin adapter turns, a read-only local sandbox,
|
|
205
205
|
the exact fixed MCP binding, and only that binding's answer/run, resume, and
|
|
206
|
-
recovery tools. It
|
|
207
|
-
permission, environment, and
|
|
208
|
-
|
|
206
|
+
recovery tools. It disables host plugin and remote-plugin loading and omits
|
|
207
|
+
unrelated local repository, app, collaboration, permission, environment, and
|
|
208
|
+
skill-catalog prompt context; the selected Eve agent still performs the
|
|
209
|
+
substantive work. Model, profile,
|
|
209
210
|
developer-instruction, MCP, approval, sandbox, and feature overrides are
|
|
210
211
|
rejected for its managed path. Ordinary `impel claude`, user-authored Claude
|
|
211
212
|
agent, and `impel codex` passthrough are unchanged.
|
package/RELEASE_NOTES.md
CHANGED
|
@@ -1,5 +1,41 @@
|
|
|
1
1
|
# Release notes
|
|
2
2
|
|
|
3
|
+
## 0.20.40 — Field-report fixes: launcher drift, sync ownership, Codex capability, one-run update
|
|
4
|
+
|
|
5
|
+
- Survives Claude auto-update drift: `impel claude` resolves the exact reviewed
|
|
6
|
+
release from the installer's side-by-side layout instead of exiting 127 when
|
|
7
|
+
the vendor updater moves the shared symlink; setup/update detection shares
|
|
8
|
+
the same resolution, ending the reinstall loop.
|
|
9
|
+
- Makes `impel agents sync` ownership symmetric across Claude and Codex with
|
|
10
|
+
digest-proven reclaim: manifest scope drift self-heals, operator-edited
|
|
11
|
+
files still refuse fail-closed, non-owning manifests can no longer delete
|
|
12
|
+
another tenant's files, and shared roots converge on tenant switch.
|
|
13
|
+
- Unstrands answer-mode adapters: retrying the answer call is permitted only
|
|
14
|
+
before a continuation exists, continuation ids tolerate case drift, and the
|
|
15
|
+
invalid-handle error now says what to pass instead.
|
|
16
|
+
- Restores Codex capability in the managed app and CLI: the generated desktop
|
|
17
|
+
profile enables the Code Mode host (pinned Codex fails closed on
|
|
18
|
+
code_mode_only models without it, hiding the 5.6 family with its max/ultra
|
|
19
|
+
efforts and Fast tier), the CLI model catalog is served from the same
|
|
20
|
+
registry as the desktop (max/ultra/Fast restored), and a dropped
|
|
21
|
+
code_mode_host key now self-heals as drift on current installs.
|
|
22
|
+
- Sizes the Codex verbatim-relay parent wait from the child's attachment
|
|
23
|
+
window so visible working time is no longer truncated, and direct answers
|
|
24
|
+
emit progress notifications.
|
|
25
|
+
- Converges `impel update` in one run: npm-prefix skew hands convergence to
|
|
26
|
+
the verified freshly installed entrypoint instead of aborting after a
|
|
27
|
+
partial update, and install-recovery re-probes live state so completed
|
|
28
|
+
steps are never re-issued.
|
|
29
|
+
|
|
30
|
+
## 0.20.39 — Isolate fixed Codex agent adapters
|
|
31
|
+
|
|
32
|
+
- Disables local and remote plugin loading, locally built plugin sharing, and
|
|
33
|
+
recommended-plugin discovery only in qualified fixed-binding Codex agent
|
|
34
|
+
profiles while preserving the direct Code Mode namespace and ordinary
|
|
35
|
+
`impel codex` plugin behavior.
|
|
36
|
+
- Advances the managed-agent manifest and generated-config versions so existing
|
|
37
|
+
managed installs receive the isolated adapter profile on update.
|
|
38
|
+
|
|
3
39
|
## 0.20.38 — Restore managed Claude agent permissions
|
|
4
40
|
|
|
5
41
|
- Verifies an exact synchronized Claude agent and its generated file digest
|
package/bin/impel.js
CHANGED
|
@@ -13,6 +13,15 @@ if (process.platform === "win32" && process.env.IMPEL_SKIP_ENTRYPOINT_REFRESH !=
|
|
|
13
13
|
}
|
|
14
14
|
|
|
15
15
|
main(process.argv.slice(2)).catch((err) => {
|
|
16
|
-
|
|
16
|
+
// Intentional user-facing errors are thrown as bare `Error` with guidance in
|
|
17
|
+
// the message ("not authenticated; run `impel setup`…"); printing their stack
|
|
18
|
+
// buries the guidance in noise on end-user machines. Programming errors
|
|
19
|
+
// (TypeError, system errors with a code) keep the full stack, and
|
|
20
|
+
// IMPEL_DEBUG=1 restores it for everything.
|
|
21
|
+
const expected = err instanceof Error && err.constructor === Error && !err.code;
|
|
22
|
+
const detail = expected && process.env.IMPEL_DEBUG !== "1"
|
|
23
|
+
? err.message
|
|
24
|
+
: (err?.stack || err?.message || err);
|
|
25
|
+
console.error(`impel: ${detail}`);
|
|
17
26
|
process.exitCode = 1;
|
|
18
27
|
});
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Native-agent host capability matrix
|
|
2
2
|
|
|
3
|
-
Validated on 2026-08-
|
|
3
|
+
Validated on 2026-08-09 with isolated temporary homes and tenant profiles. The
|
|
4
4
|
matrix is intentionally pinned: a client upgrade must be re-qualified before it
|
|
5
5
|
is treated as an eager host.
|
|
6
6
|
|
|
@@ -50,8 +50,9 @@ is treated as an eager host.
|
|
|
50
50
|
`impel_agent` is not equivalent and does not qualify as direct exposure.
|
|
51
51
|
- Generated Codex fixed-binding adapters use the ultra-fast Codex Spark model
|
|
52
52
|
at low reasoning for the thin tool-selection and terminal-relay turns, with
|
|
53
|
-
unrelated local host and skill-catalog context
|
|
54
|
-
agent, not this wrapper model, still owns the
|
|
53
|
+
unrelated local host, plugin, remote-plugin, and skill-catalog context
|
|
54
|
+
disabled. The selected Eve agent, not this wrapper model, still owns the
|
|
55
|
+
substantive task.
|
|
55
56
|
|
|
56
57
|
## Requalification
|
|
57
58
|
|
package/package.json
CHANGED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
// Capture the released-state fixture corpus (docs/testing-protocol.md §5
|
|
2
|
+
// P0-2, Law 3: fixtures are captured from released artifacts, never
|
|
3
|
+
// fabricated from HEAD after the fact).
|
|
4
|
+
//
|
|
5
|
+
// Run this AT RELEASE TIME, on the exact tree being tagged, so
|
|
6
|
+
// test/fixtures/released/<version>/ honestly represents what shipped:
|
|
7
|
+
//
|
|
8
|
+
// node scripts/capture-release-fixtures.mjs [--force]
|
|
9
|
+
//
|
|
10
|
+
// It regenerates the managed on-disk artifacts deterministically and offline
|
|
11
|
+
// (see scripts/regen-managed-artifacts.mjs: fixed inputs, temp HOMEs, fake
|
|
12
|
+
// impel_pat_TEST credential, double-generation byte comparison) into
|
|
13
|
+
// test/fixtures/released/<package.json version>/. An existing version
|
|
14
|
+
// directory is never overwritten unless --force is passed.
|
|
15
|
+
|
|
16
|
+
import fs from "node:fs";
|
|
17
|
+
import path from "node:path";
|
|
18
|
+
import { fileURLToPath } from "node:url";
|
|
19
|
+
|
|
20
|
+
import { REPOSITORY_ROOT, regenerateManagedArtifacts } from "./regen-managed-artifacts.mjs";
|
|
21
|
+
|
|
22
|
+
export const RELEASED_FIXTURES_ROOT = path.join(REPOSITORY_ROOT, "test", "fixtures", "released");
|
|
23
|
+
|
|
24
|
+
export async function captureReleaseFixtures({ force = false } = {}) {
|
|
25
|
+
const packageJson = JSON.parse(fs.readFileSync(path.join(REPOSITORY_ROOT, "package.json"), "utf8"));
|
|
26
|
+
const version = packageJson.version;
|
|
27
|
+
if (typeof version !== "string" || !/^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?$/u.test(version)) {
|
|
28
|
+
throw new Error(`package.json version "${version}" is not a valid release version`);
|
|
29
|
+
}
|
|
30
|
+
const outputDir = path.join(RELEASED_FIXTURES_ROOT, version);
|
|
31
|
+
if (fs.existsSync(outputDir) && !force) {
|
|
32
|
+
throw new Error(
|
|
33
|
+
`${outputDir} already exists. A released fixture is immutable once captured; `
|
|
34
|
+
+ "pass --force only to re-capture a fixture that never shipped.",
|
|
35
|
+
);
|
|
36
|
+
}
|
|
37
|
+
const metadata = await regenerateManagedArtifacts(outputDir, { force });
|
|
38
|
+
return { version, outputDir, metadata };
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const isMain = process.argv[1]
|
|
42
|
+
&& path.resolve(process.argv[1]) === fileURLToPath(import.meta.url);
|
|
43
|
+
|
|
44
|
+
if (isMain) {
|
|
45
|
+
const { version, outputDir, metadata } = await captureReleaseFixtures({
|
|
46
|
+
force: process.argv.includes("--force"),
|
|
47
|
+
});
|
|
48
|
+
console.log(
|
|
49
|
+
`captured released-state fixtures for v${version}: ${metadata.artifacts.length} artifacts `
|
|
50
|
+
+ `(configVersion ${metadata.configVersion}, ${metadata.bundleFingerprint})`,
|
|
51
|
+
);
|
|
52
|
+
console.log(` ${outputDir}`);
|
|
53
|
+
for (const excluded of metadata.excluded) {
|
|
54
|
+
console.log(` excluded: ${excluded.path}`);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
# Clean-machine protocol (P0-4)
|
|
2
|
+
|
|
3
|
+
Runs the CLI the way a brand-new customer machine does — no git, no winget, no
|
|
4
|
+
dev tooling, stock PATH — because that machine state is structurally
|
|
5
|
+
unreachable from CI runners, and it is where an entire class of first-run
|
|
6
|
+
failures shipped (see `docs/testing-protocol.md`, RC2).
|
|
7
|
+
|
|
8
|
+
## Windows leg
|
|
9
|
+
|
|
10
|
+
```sh
|
|
11
|
+
AWS_PROFILE=useimpel scripts/clean-machine/run-clean-windows.sh
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
Launches a disposable, self-terminating Windows Server EC2 instance (no
|
|
15
|
+
inbound access, no SSH key, S3-scoped instance role) that:
|
|
16
|
+
|
|
17
|
+
1. asserts the machine is actually clean (git absent; winget state recorded);
|
|
18
|
+
2. installs Node headless from the official MSI;
|
|
19
|
+
3. global-installs the packed tarball with the bundled npm and asserts the
|
|
20
|
+
install is a real directory (the npm prepare-script symlink class);
|
|
21
|
+
4. runs `impel --version`, `impel doctor`, and non-interactive `impel setup`
|
|
22
|
+
with hang guards, asserting actionable single-line failures — never a
|
|
23
|
+
stack trace, never a hang, never a silent success;
|
|
24
|
+
5. runs the full ungated test suite on the git-less machine — the leg that
|
|
25
|
+
catches tests silently depending on runner tooling;
|
|
26
|
+
6. uploads `results.json` + full logs to S3 and terminates itself.
|
|
27
|
+
|
|
28
|
+
The runner exits 0 only when every post-condition holds and the suite has no
|
|
29
|
+
failing test. First run of this harness found 18 environment-sensitive tests
|
|
30
|
+
and two CLI output defects; expect it to keep earning its keep.
|
|
31
|
+
|
|
32
|
+
## When to run
|
|
33
|
+
|
|
34
|
+
- Before any stable release that touches install/update/setup paths
|
|
35
|
+
(protocol §4.3), and after changing anything under `src/installRecovery/`,
|
|
36
|
+
`src/selfInvocation.js`, or the skills/git provisioning chain.
|
|
37
|
+
- Cost: one t3.large for ~15 minutes (well under $0.10) plus a few MB of S3.
|
|
38
|
+
|
|
39
|
+
## Known limitations
|
|
40
|
+
|
|
41
|
+
- Windows **Server** AMIs never ship winget, so the winget install path can't
|
|
42
|
+
be exercised here (EC2 offers no consumer Windows 11 images). That leg stays
|
|
43
|
+
covered by `test/launch.test.js` (cmd-escaping contract) and the MSIX/Store
|
|
44
|
+
contract workflow. The results record winget absence so the gap is visible.
|
|
45
|
+
- The instance runs as SYSTEM at first boot, not as an interactive standard
|
|
46
|
+
user; per-user ACL behavior is covered by the `windows-standard-user-contract`
|
|
47
|
+
CI job instead.
|
|
48
|
+
- macOS leg: EC2 Mac requires a 24-hour dedicated-host allocation, so the
|
|
49
|
+
macOS clean-machine equivalent (running vendor apps, name-colliding consumer
|
|
50
|
+
apps, stale Homebrew binaries) is not automated here yet; the launch-smoke
|
|
51
|
+
CI job covers the bundle/egress side on ephemeral macOS runners.
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Clean-machine protocol (docs/testing-protocol.md P0-4), Windows leg runner.
|
|
3
|
+
#
|
|
4
|
+
# Launches a disposable, self-terminating Windows Server EC2 instance that has
|
|
5
|
+
# no git and no dev tooling, installs Node headless, global-installs the packed
|
|
6
|
+
# CLI tarball, asserts post-conditions (version/doctor/setup actionable, real
|
|
7
|
+
# install dir), runs the full ungated test suite, and uploads results to S3.
|
|
8
|
+
# The instance needs no inbound access and no SSH key; results come back via a
|
|
9
|
+
# bucket-scoped instance role.
|
|
10
|
+
#
|
|
11
|
+
# Usage:
|
|
12
|
+
# AWS_PROFILE=useimpel scripts/clean-machine/run-clean-windows.sh
|
|
13
|
+
#
|
|
14
|
+
# Tunables (env): AWS_PROFILE, CLEAN_MACHINE_REGION (default eu-west-2),
|
|
15
|
+
# CLEAN_MACHINE_BUCKET, CLEAN_MACHINE_INSTANCE_TYPE (default t3.large),
|
|
16
|
+
# CLEAN_MACHINE_NODE_VERSION (default v24.19.0).
|
|
17
|
+
#
|
|
18
|
+
# One-time account setup (idempotent, created automatically if missing):
|
|
19
|
+
# bucket, IAM role/instance-profile `clean-machine-test-role` (S3 Get/Put on
|
|
20
|
+
# the bucket only), and a no-inbound security group.
|
|
21
|
+
set -euo pipefail
|
|
22
|
+
|
|
23
|
+
REGION="${CLEAN_MACHINE_REGION:-eu-west-2}"
|
|
24
|
+
ACCOUNT="$(aws sts get-caller-identity --query Account --output text)"
|
|
25
|
+
BUCKET="${CLEAN_MACHINE_BUCKET:-impel-clean-machine-tests-${ACCOUNT}}"
|
|
26
|
+
INSTANCE_TYPE="${CLEAN_MACHINE_INSTANCE_TYPE:-t3.large}"
|
|
27
|
+
NODE_VERSION="${CLEAN_MACHINE_NODE_VERSION:-v24.19.0}"
|
|
28
|
+
ROLE="clean-machine-test-role"
|
|
29
|
+
SG_NAME="impel-clean-machine-no-inbound"
|
|
30
|
+
HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
31
|
+
REPO_ROOT="$(cd "$HERE/../.." && pwd)"
|
|
32
|
+
WORK="$(mktemp -d)"
|
|
33
|
+
trap 'rm -rf "$WORK"' EXIT
|
|
34
|
+
export AWS_DEFAULT_REGION="$REGION"
|
|
35
|
+
|
|
36
|
+
echo "== packing CLI + source from $REPO_ROOT"
|
|
37
|
+
(cd "$REPO_ROOT" && pnpm pack --pack-destination "$WORK" >/dev/null)
|
|
38
|
+
TARBALL="$(basename "$(find "$WORK" -maxdepth 1 -name 'impel-cli-*.tgz' -print -quit)")"
|
|
39
|
+
test -n "$TARBALL"
|
|
40
|
+
(cd "$REPO_ROOT" && git archive --format=tar.gz -o "$WORK/impel-cli-source.tgz" HEAD)
|
|
41
|
+
|
|
42
|
+
echo "== ensuring bucket/role/security group"
|
|
43
|
+
aws s3api head-bucket --bucket "$BUCKET" 2>/dev/null \
|
|
44
|
+
|| aws s3api create-bucket --bucket "$BUCKET" --create-bucket-configuration "LocationConstraint=$REGION" >/dev/null
|
|
45
|
+
aws iam get-role --role-name "$ROLE" >/dev/null 2>&1 || {
|
|
46
|
+
aws iam create-role --role-name "$ROLE" --assume-role-policy-document \
|
|
47
|
+
'{"Version":"2012-10-17","Statement":[{"Effect":"Allow","Principal":{"Service":"ec2.amazonaws.com"},"Action":"sts:AssumeRole"}]}' >/dev/null
|
|
48
|
+
aws iam create-instance-profile --instance-profile-name "$ROLE" >/dev/null
|
|
49
|
+
aws iam add-role-to-instance-profile --instance-profile-name "$ROLE" --role-name "$ROLE"
|
|
50
|
+
sleep 10 # instance-profile eventual consistency
|
|
51
|
+
}
|
|
52
|
+
aws iam put-role-policy --role-name "$ROLE" --policy-name s3-scratch --policy-document \
|
|
53
|
+
"{\"Version\":\"2012-10-17\",\"Statement\":[{\"Effect\":\"Allow\",\"Action\":[\"s3:GetObject\",\"s3:PutObject\"],\"Resource\":\"arn:aws:s3:::$BUCKET/*\"}]}"
|
|
54
|
+
VPC="$(aws ec2 describe-vpcs --filters Name=is-default,Values=true --query 'Vpcs[0].VpcId' --output text)"
|
|
55
|
+
SG="$(aws ec2 describe-security-groups --filters "Name=group-name,Values=$SG_NAME" --query 'SecurityGroups[0].GroupId' --output text 2>/dev/null)"
|
|
56
|
+
if [ "$SG" = "None" ] || [ -z "$SG" ]; then
|
|
57
|
+
SG="$(aws ec2 create-security-group --group-name "$SG_NAME" --description "No-inbound SG for impel clean-machine tests" --vpc-id "$VPC" --query GroupId --output text)"
|
|
58
|
+
fi
|
|
59
|
+
SUBNET="$(aws ec2 describe-subnets --filters Name=default-for-az,Values=true --query 'Subnets[0].SubnetId' --output text)"
|
|
60
|
+
AMI="$(aws ssm get-parameter --name /aws/service/ami-windows-latest/Windows_Server-2022-English-Full-Base --query 'Parameter.Value' --output text)"
|
|
61
|
+
|
|
62
|
+
echo "== uploading artifacts to s3://$BUCKET"
|
|
63
|
+
aws s3 rm "s3://$BUCKET/results" --recursive --only-show-errors 2>/dev/null || true
|
|
64
|
+
aws s3 cp "$WORK/$TARBALL" "s3://$BUCKET/$TARBALL" --only-show-errors
|
|
65
|
+
aws s3 cp "$WORK/impel-cli-source.tgz" "s3://$BUCKET/impel-cli-source.tgz" --only-show-errors
|
|
66
|
+
|
|
67
|
+
sed -e "s|__BUCKET__|$BUCKET|g" -e "s|__REGION__|$REGION|g" \
|
|
68
|
+
-e "s|__CLI_TARBALL__|$TARBALL|g" -e "s|__NODE_VERSION__|$NODE_VERSION|g" \
|
|
69
|
+
"$HERE/userdata.ps1.template" > "$WORK/userdata.ps1"
|
|
70
|
+
|
|
71
|
+
echo "== launching $INSTANCE_TYPE ($AMI) in $SUBNET"
|
|
72
|
+
INSTANCE="$(aws ec2 run-instances --image-id "$AMI" --instance-type "$INSTANCE_TYPE" \
|
|
73
|
+
--iam-instance-profile "Name=$ROLE" --instance-initiated-shutdown-behavior terminate \
|
|
74
|
+
--user-data "file://$WORK/userdata.ps1" --security-group-ids "$SG" --subnet-id "$SUBNET" \
|
|
75
|
+
--metadata-options HttpTokens=required \
|
|
76
|
+
--tag-specifications 'ResourceType=instance,Tags=[{Key=Name,Value=impel-clean-machine-test},{Key=purpose,Value=impel-cli-testing-protocol}]' \
|
|
77
|
+
--query 'Instances[0].InstanceId' --output text)"
|
|
78
|
+
echo "instance: $INSTANCE (self-terminates when done)"
|
|
79
|
+
|
|
80
|
+
echo "== waiting for results (boot + install + suite; typically 8-20 min)"
|
|
81
|
+
for i in $(seq 1 45); do
|
|
82
|
+
if aws s3api head-object --bucket "$BUCKET" --key results/results.json >/dev/null 2>&1; then
|
|
83
|
+
aws s3 cp "s3://$BUCKET/results/results.json" "$WORK/results.json" --only-show-errors
|
|
84
|
+
echo "== results after ~${i} min:"
|
|
85
|
+
node -e '
|
|
86
|
+
const r = JSON.parse(require("fs").readFileSync(process.argv[1], "utf8").replace(/^/, ""));
|
|
87
|
+
const s = r.steps;
|
|
88
|
+
const fail = [];
|
|
89
|
+
if (s.preconditions.gitPresent) fail.push("machine not clean: git present");
|
|
90
|
+
if (s.nodeMsiExit !== 0) fail.push(`node MSI exit ${s.nodeMsiExit}`);
|
|
91
|
+
if (!s.impelCmdExists) fail.push("impel.cmd missing after global install");
|
|
92
|
+
if (!s.installIsRealDir) fail.push("global install is a reparse point (npm symlink class)");
|
|
93
|
+
if (s.version && !/^\d+\.\d+\.\d+/.test((s.version.stdoutHead || "").trim())) fail.push("impel --version malformed");
|
|
94
|
+
for (const k of ["doctor", "setupNoAuth"]) {
|
|
95
|
+
const st = s[k]; if (!st) continue;
|
|
96
|
+
if (st.timedOut) fail.push(`${k} hung`);
|
|
97
|
+
if (/\n\s+at /.test(st.stderrHead || "")) fail.push(`${k} printed a stack trace`);
|
|
98
|
+
}
|
|
99
|
+
const suite = s.testSuite || {};
|
|
100
|
+
const m = (suite.stdoutHead || "").match(/# fail (\d+)/) || (suite.stdoutHead || "").match(/✖/);
|
|
101
|
+
if (suite.timedOut) fail.push("test suite hung");
|
|
102
|
+
if ((suite.stdoutHead || "").includes("✖")) fail.push("test suite has failing tests (see results/node-test.out.txt)");
|
|
103
|
+
console.log(JSON.stringify({ preconditions: s.preconditions, node: (s.nodeVersion||"").trim(), npm: (s.npmVersion||"").trim(), verdict: fail.length ? "FAIL" : "PASS", failures: fail }, null, 2));
|
|
104
|
+
process.exitCode = fail.length ? 1 : 0;
|
|
105
|
+
' "$WORK/results.json" || { echo "full logs: s3://$BUCKET/results/"; exit 1; }
|
|
106
|
+
echo "full logs: s3://$BUCKET/results/"
|
|
107
|
+
exit 0
|
|
108
|
+
fi
|
|
109
|
+
STATE="$(aws ec2 describe-instances --instance-ids "$INSTANCE" --query 'Reservations[0].Instances[0].State.Name' --output text 2>/dev/null || echo unknown)"
|
|
110
|
+
if [ "$STATE" = "terminated" ]; then echo "instance terminated without uploading results"; exit 1; fi
|
|
111
|
+
sleep 60
|
|
112
|
+
done
|
|
113
|
+
echo "timed out waiting for results; terminating $INSTANCE"
|
|
114
|
+
aws ec2 terminate-instances --instance-ids "$INSTANCE" >/dev/null
|
|
115
|
+
exit 1
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
<powershell>
|
|
2
|
+
# Clean-machine protocol (docs/testing-protocol.md P0-4), Windows leg.
|
|
3
|
+
# Rendered by run-clean-windows.sh: __BUCKET__, __REGION__, __CLI_TARBALL__,
|
|
4
|
+
# __NODE_VERSION__ are substituted before launch. Runs at first boot as SYSTEM
|
|
5
|
+
# on a fresh Windows Server AMI (no git, no dev tooling), self-terminates.
|
|
6
|
+
$ErrorActionPreference = 'Continue'
|
|
7
|
+
$root = 'C:\clean-machine'
|
|
8
|
+
New-Item -ItemType Directory -Force -Path $root | Out-Null
|
|
9
|
+
Start-Transcript -Path "$root\transcript.txt" -Force
|
|
10
|
+
$bucket = '__BUCKET__'
|
|
11
|
+
$region = '__REGION__'
|
|
12
|
+
$results = [ordered]@{ startedAt = (Get-Date).ToUniversalTime().ToString('o'); steps = [ordered]@{} }
|
|
13
|
+
|
|
14
|
+
function Run-Step {
|
|
15
|
+
param([string]$name, [string]$file, [string[]]$argList, [int]$timeoutSec = 180, [string]$stdin = $null)
|
|
16
|
+
$out = "$root\$name.out.txt"; $err = "$root\$name.err.txt"
|
|
17
|
+
$procArgs = @{ FilePath = $file; ArgumentList = $argList; RedirectStandardOutput = $out; RedirectStandardError = $err; NoNewWindow = $true; PassThru = $true }
|
|
18
|
+
if ($null -ne $stdin) { $inFile = "$root\$name.in.txt"; Set-Content -Path $inFile -Value $stdin; $procArgs.RedirectStandardInput = $inFile }
|
|
19
|
+
$step = [ordered]@{ timedOut = $false; exitCode = $null; stdoutHead = ''; stderrHead = '' }
|
|
20
|
+
try {
|
|
21
|
+
$p = Start-Process @procArgs
|
|
22
|
+
if (-not $p.WaitForExit($timeoutSec * 1000)) {
|
|
23
|
+
$step.timedOut = $true
|
|
24
|
+
Stop-Process -Id $p.Id -Force -ErrorAction SilentlyContinue
|
|
25
|
+
Start-Sleep -Seconds 2
|
|
26
|
+
}
|
|
27
|
+
$step.exitCode = $p.ExitCode
|
|
28
|
+
} catch { $step.error = "$_" }
|
|
29
|
+
foreach ($pair in @(@('stdoutHead', $out, 3000), @('stderrHead', $err, 2000))) {
|
|
30
|
+
$key = $pair[0]; $file2 = $pair[1]; $keep = $pair[2]
|
|
31
|
+
if (Test-Path $file2) {
|
|
32
|
+
$text = Get-Content $file2 -Raw -ErrorAction SilentlyContinue
|
|
33
|
+
if ($null -eq $text) { $text = '' }
|
|
34
|
+
if ($text.Length -gt (2 * $keep)) { $text = $text.Substring(0, $keep) + "`n...TRUNCATED...`n" + $text.Substring($text.Length - $keep) }
|
|
35
|
+
$step[$key] = $text
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
return $step
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
# 1. Clean-machine preconditions: git must be absent; record winget state.
|
|
42
|
+
# (Windows Server AMIs never ship winget — the winget-specific install leg
|
|
43
|
+
# needs consumer Windows and stays covered by launch.test.js + the MSIX
|
|
44
|
+
# contract job; record the state so the gap is visible, never silent.)
|
|
45
|
+
$results.steps.preconditions = [ordered]@{
|
|
46
|
+
gitPresent = [bool](Get-Command git -ErrorAction SilentlyContinue)
|
|
47
|
+
wingetPresent = [bool](Get-Command winget -ErrorAction SilentlyContinue)
|
|
48
|
+
osCaption = (Get-CimInstance Win32_OperatingSystem).Caption
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
# 2. Install Node headless from the official MSI.
|
|
52
|
+
Invoke-WebRequest -Uri 'https://nodejs.org/dist/__NODE_VERSION__/node-__NODE_VERSION__-x64.msi' -OutFile "$root\node.msi" -UseBasicParsing
|
|
53
|
+
$msi = Start-Process msiexec.exe -ArgumentList '/i', "$root\node.msi", '/qn', '/norestart' -Wait -PassThru
|
|
54
|
+
$results.steps.nodeMsiExit = $msi.ExitCode
|
|
55
|
+
$env:Path = [System.Environment]::GetEnvironmentVariable('Path', 'Machine') + ';' + [System.Environment]::GetEnvironmentVariable('Path', 'User')
|
|
56
|
+
$nodeDir = 'C:\Program Files\nodejs'
|
|
57
|
+
$results.steps.nodeVersion = & "$nodeDir\node.exe" --version 2>&1 | Out-String
|
|
58
|
+
$results.steps.npmVersion = & "$nodeDir\npm.cmd" --version 2>&1 | Out-String
|
|
59
|
+
|
|
60
|
+
# 3. Fetch artifacts from S3 via the instance role.
|
|
61
|
+
Read-S3Object -BucketName $bucket -Key '__CLI_TARBALL__' -File "$root\impel-cli.tgz" -Region $region | Out-Null
|
|
62
|
+
Read-S3Object -BucketName $bucket -Key 'impel-cli-source.tgz' -File "$root\source.tgz" -Region $region | Out-Null
|
|
63
|
+
|
|
64
|
+
# 4. npm global tarball install on a git-less machine (install-channel class).
|
|
65
|
+
$results.steps.npmInstall = Run-Step 'npm-install' "$nodeDir\npm.cmd" @('install', '--global', "$root\impel-cli.tgz") 300
|
|
66
|
+
$npmPrefix = (& "$nodeDir\npm.cmd" prefix -g 2>$null | Out-String).Trim()
|
|
67
|
+
$results.steps.npmPrefix = $npmPrefix
|
|
68
|
+
$impelCmd = Join-Path $npmPrefix 'impel.cmd'
|
|
69
|
+
$results.steps.impelCmdExists = Test-Path $impelCmd
|
|
70
|
+
$pkgDir = Join-Path $npmPrefix 'node_modules\impel-cli'
|
|
71
|
+
$item = Get-Item $pkgDir -ErrorAction SilentlyContinue
|
|
72
|
+
$results.steps.installIsRealDir = ($null -ne $item) -and (-not ($item.Attributes -band [IO.FileAttributes]::ReparsePoint))
|
|
73
|
+
|
|
74
|
+
# 5. Post-conditions, not exit codes: version, doctor, setup must be
|
|
75
|
+
# actionable, never hang, never dump a stack trace at the user.
|
|
76
|
+
if ($results.steps.impelCmdExists) {
|
|
77
|
+
$results.steps.version = Run-Step 'impel-version' $impelCmd @('--version') 60
|
|
78
|
+
$results.steps.doctor = Run-Step 'impel-doctor' $impelCmd @('doctor') 180
|
|
79
|
+
$results.steps.setupNoAuth = Run-Step 'impel-setup' $impelCmd @('setup') 180 -stdin ''
|
|
80
|
+
}
|
|
81
|
+
$results.steps.nodeProcsAfter = @(Get-Process -Name node -ErrorAction SilentlyContinue).Count
|
|
82
|
+
$results.steps.cmdProcsAfter = @(Get-Process -Name cmd -ErrorAction SilentlyContinue).Count
|
|
83
|
+
|
|
84
|
+
# 6. The full ungated test suite on a machine with no git and no dev tooling —
|
|
85
|
+
# the leg that catches environment-sensitive tests CI can never see.
|
|
86
|
+
New-Item -ItemType Directory -Force -Path "$root\src" | Out-Null
|
|
87
|
+
tar.exe -xzf "$root\source.tgz" -C "$root\src"
|
|
88
|
+
Push-Location "$root\src"
|
|
89
|
+
$env:HOME = "$root\home"; New-Item -ItemType Directory -Force -Path $env:HOME | Out-Null
|
|
90
|
+
$results.steps.testSuite = Run-Step 'node-test' "$nodeDir\node.exe" @('--test') 1500
|
|
91
|
+
Pop-Location
|
|
92
|
+
|
|
93
|
+
$results.finishedAt = (Get-Date).ToUniversalTime().ToString('o')
|
|
94
|
+
$results | ConvertTo-Json -Depth 6 | Set-Content -Path "$root\results.json" -Encoding UTF8
|
|
95
|
+
Stop-Transcript
|
|
96
|
+
Write-S3Object -BucketName $bucket -Key 'results/results.json' -File "$root\results.json" -Region $region
|
|
97
|
+
Write-S3Object -BucketName $bucket -Key 'results/transcript.txt' -File "$root\transcript.txt" -Region $region
|
|
98
|
+
foreach ($f in Get-ChildItem "$root\*.out.txt", "$root\*.err.txt" -ErrorAction SilentlyContinue) {
|
|
99
|
+
Write-S3Object -BucketName $bucket -Key "results/$($f.Name)" -File $f.FullName -Region $region
|
|
100
|
+
}
|
|
101
|
+
Stop-Computer -Force
|
|
102
|
+
</powershell>
|