impel-cli 0.20.38 → 0.20.39

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
@@ -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 omits unrelated local repository, app, collaboration,
207
- permission, environment, and skill-catalog prompt context; the selected Eve
208
- agent still performs the substantive work. Model, profile,
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,14 @@
1
1
  # Release notes
2
2
 
3
+ ## 0.20.39 — Isolate fixed Codex agent adapters
4
+
5
+ - Disables local and remote plugin loading, locally built plugin sharing, and
6
+ recommended-plugin discovery only in qualified fixed-binding Codex agent
7
+ profiles while preserving the direct Code Mode namespace and ordinary
8
+ `impel codex` plugin behavior.
9
+ - Advances the managed-agent manifest and generated-config versions so existing
10
+ managed installs receive the isolated adapter profile on update.
11
+
3
12
  ## 0.20.38 — Restore managed Claude agent permissions
4
13
 
5
14
  - 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
- console.error(`impel: ${err?.stack || err?.message || err}`);
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-08 with isolated temporary homes and tenant profiles. The
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 disabled. The selected Eve
54
- agent, not this wrapper model, still owns the substantive task.
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "impel-cli",
3
- "version": "0.20.38",
3
+ "version": "0.20.39",
4
4
  "description": "Prepare isolated Claude and Codex workspaces for every accessible Impel tenant",
5
5
  "type": "module",
6
6
  "bin": {
@@ -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>