@ai-sdk/harness-pi 1.0.73 → 1.0.74

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.
@@ -54,46 +54,137 @@ async function readCommandOutput(
54
54
  command: string,
55
55
  ): Promise<string> {
56
56
  const result = await sandbox.run({ command });
57
- const output = result.stdout || result.stderr;
58
57
  if (result.exitCode != null && result.exitCode !== 0) {
59
58
  throw new Error(
60
- output || `Sandbox command failed with exit code ${result.exitCode}`,
59
+ result.stderr ||
60
+ result.stdout ||
61
+ `Sandbox command failed with exit code ${result.exitCode}`,
61
62
  );
62
63
  }
63
- return output;
64
+ return result.stdout || result.stderr;
64
65
  }
65
66
 
66
67
  async function listRemoteWorkspaceEntries(
67
68
  sandbox: Experimental_SandboxSession,
68
69
  sandboxWorkDir: string,
69
- ): Promise<{ directories: string[]; files: string[] }> {
70
- const contextPredicate = PI_CONTEXT_FILENAMES.map(
71
- name => `-name ${shellQuote(name)}`,
72
- ).join(' -o ');
73
-
70
+ ): Promise<{
71
+ directories: string[];
72
+ files: Array<{ relativePath: string; sandboxPath: string }>;
73
+ }> {
74
74
  // Enumerate only the `.pi`/`.agents` config subtrees plus the root-level
75
- // context files — never the rest of the workspace. `find -L` dereferences
76
- // symlinks so that linked targets (e.g. `.agents/skills` pointing elsewhere)
77
- // are walked and reported through the symlinked path; the resolved file/dir
78
- // types from `[ -d ]`/`[ -f ]` then tag each entry `d`/`f`, NUL-joined
79
- // exactly like a full-tree walk so the reconcile below is unchanged.
80
- const configFinds = PI_CONFIG_DIRS.map(
81
- dir =>
82
- ` if [ -d ./${dir} ]; then find -L ./${dir} \\( -type d -o -type f \\) -print0; fi;`,
83
- );
75
+ // context files — never the rest of the workspace. Use shell glob traversal
76
+ // instead of `find -L`, which is not supported by all sandbox shells.
77
+ // Resolve each scoped root path component explicitly with `readlink`,
78
+ // retaining both the scoped mirror path and the resolved sandbox path. Once
79
+ // a root is resolved, queued descendants use physical parent paths and only
80
+ // need resolution when the descendant itself is a symlink. This avoids
81
+ // relying on shells that can inspect a symlinked directory but cannot
82
+ // traverse paths below it, including when the symlink is an ancestor of the
83
+ // sandbox work directory, without repeatedly resolving every component of
84
+ // deep ordinary paths.
85
+ const scopedPaths = [
86
+ ...PI_CONFIG_DIRS.map(dir => `./${dir}`),
87
+ ...PI_CONTEXT_FILENAMES.map(name => `./${name}`),
88
+ ];
84
89
  const listCommand = [
85
- '{',
86
- ...configFinds,
87
- ` find . -maxdepth 1 -type f \\( ${contextPredicate} \\) -print0;`,
88
- '} |',
89
- "while IFS= read -r -d '' entry; do",
90
- ' rel=${entry#./}',
91
- ' if [ -d "$entry" ]; then',
92
- ` printf 'd\\t%s\\n' "$rel"`,
93
- ' elif [ -f "$entry" ]; then',
94
- ` printf 'f\\t%s\\n' "$rel"`,
90
+ `pi_config_sources=(${scopedPaths
91
+ .map(scopedPath =>
92
+ shellQuote(path.posix.join(sandboxWorkDir, scopedPath.slice(2))),
93
+ )
94
+ .join(' ')})`,
95
+ `pi_config_relatives=(${scopedPaths
96
+ .map(scopedPath => shellQuote(scopedPath.slice(2)))
97
+ .join(' ')})`,
98
+ `pi_config_ancestors=(${scopedPaths.map(() => "''").join(' ')})`,
99
+ `pi_config_resolve_ancestors=(${scopedPaths.map(() => '1').join(' ')})`,
100
+ 'pi_config_index=0',
101
+ 'while [ "$pi_config_index" -lt "${#pi_config_sources[@]}" ]; do',
102
+ ' source=${pi_config_sources[$pi_config_index]}',
103
+ ' relative=${pi_config_relatives[$pi_config_index]}',
104
+ ' ancestors=${pi_config_ancestors[$pi_config_index]}',
105
+ ' resolve_ancestors=${pi_config_resolve_ancestors[$pi_config_index]}',
106
+ ' pi_config_index=$((pi_config_index + 1))',
107
+ ' if [ "$resolve_ancestors" = 1 ] || [ -L "$source" ]; then',
108
+ ' pending=$source',
109
+ ' resolved=',
110
+ ' seen_links=',
111
+ ' case "$pending" in',
112
+ ' /*) ;;',
113
+ ' *) pending=$PWD/$pending ;;',
114
+ ' esac',
115
+ ' while [ -n "$pending" ]; do',
116
+ ' pending=${pending#/}',
117
+ ' [ -n "$pending" ] || break',
118
+ ' component=${pending%%/*}',
119
+ ' if [ "$pending" = "$component" ]; then',
120
+ ' pending=',
121
+ ' else',
122
+ ' pending=${pending#*/}',
123
+ ' fi',
124
+ ' case "$component" in',
125
+ ' ""|.) continue ;;',
126
+ ' ..)',
127
+ ' resolved=${resolved%/*}',
128
+ ' continue',
129
+ ' ;;',
130
+ ' esac',
131
+ ' candidate=$resolved/$component',
132
+ ' if [ -L "$candidate" ]; then',
133
+ ' case "$seen_links" in',
134
+ ' *"',
135
+ '"$candidate"',
136
+ '"*)',
137
+ ` printf 'Pi config traversal encountered a symlink cycle at %s\\n' "$candidate" >&2`,
138
+ ' exit 1',
139
+ ' ;;',
140
+ ' esac',
141
+ ' seen_links=$seen_links"',
142
+ '"$candidate"',
143
+ '"',
144
+ ' target=$(readlink "$candidate") || exit $?',
145
+ ' case "$target" in',
146
+ ' /*) pending=$target${pending:+/$pending} ;;',
147
+ ' *) pending=${candidate%/*}/$target${pending:+/$pending} ;;',
148
+ ' esac',
149
+ ' resolved=',
150
+ ' else',
151
+ ' resolved=$candidate',
152
+ ' fi',
153
+ ' done',
154
+ ' if [ -z "$resolved" ]; then',
155
+ ` printf 'Pi config traversal resolved %s to the filesystem root; skipping\\n' "$relative" >&2`,
156
+ ' continue',
157
+ ' fi',
158
+ ' source=$resolved',
159
+ ' fi',
160
+ ' if [ -d "$source" ]; then',
161
+ ' case "$ancestors" in',
162
+ ' *"',
163
+ '"$source"',
164
+ '"*)',
165
+ ` printf 'Pi config traversal encountered a symlink cycle at %s\\n' "$relative" >&2`,
166
+ ' exit 1',
167
+ ' ;;',
168
+ ' esac',
169
+ ' ancestors=$ancestors"',
170
+ '"$source"',
171
+ '"',
172
+ ` printf 'd\\t%s\\n' "$relative"`,
173
+ ' for child in "$source"/* "$source"/.[!.]* "$source"/..?*; do',
174
+ ' if [ -L "$child" ] || [ -d "$child" ] || [ -f "$child" ]; then',
175
+ ' child_name=${child##*/}',
176
+ ' pi_config_sources+=("$child")',
177
+ ' pi_config_relatives+=("$relative/$child_name")',
178
+ ' pi_config_ancestors+=("$ancestors")',
179
+ ' pi_config_resolve_ancestors+=(0)',
180
+ ' fi',
181
+ ' done',
182
+ ' elif [ -f "$source" ]; then',
183
+ ` printf 'f\\t%s\\t%s\\n' "$relative" "$source"`,
95
184
  ' fi',
96
- 'done | LC_ALL=C sort',
185
+ ' true',
186
+ 'done',
187
+ 'true',
97
188
  ].join('\n');
98
189
 
99
190
  const output = await readCommandOutput(
@@ -102,15 +193,17 @@ async function listRemoteWorkspaceEntries(
102
193
  );
103
194
 
104
195
  const directories: string[] = [];
105
- const files: string[] = [];
196
+ const files: Array<{ relativePath: string; sandboxPath: string }> = [];
106
197
 
107
198
  for (const line of output.split('\n').filter(Boolean)) {
108
- const [kind, rawPath] = line.split('\t', 2);
199
+ const [kind, rawPath, sandboxPath] = line.split('\t', 3);
109
200
  if (!rawPath) continue;
110
201
 
111
202
  const relativePath = normalizeRelativePath(rawPath);
112
203
  if (kind === 'd') directories.push(relativePath);
113
- else if (kind === 'f') files.push(relativePath);
204
+ else if (kind === 'f' && sandboxPath) {
205
+ files.push({ relativePath, sandboxPath });
206
+ }
114
207
  }
115
208
 
116
209
  return { directories, files };
@@ -180,14 +273,14 @@ async function collectHostScopedEntries(
180
273
 
181
274
  function buildRequiredDirectories(
182
275
  remoteDirectories: string[],
183
- remoteFiles: string[],
276
+ remoteFiles: Array<{ relativePath: string }>,
184
277
  ): Set<string> {
185
278
  const directories = new Set<string>();
186
279
  for (const directory of remoteDirectories) {
187
280
  directories.add(normalizeRelativePath(directory));
188
281
  }
189
282
  for (const file of remoteFiles) {
190
- let current = path.dirname(normalizeRelativePath(file));
283
+ let current = path.dirname(normalizeRelativePath(file.relativePath));
191
284
  while (current !== '.' && current !== path.sep && current.length > 0) {
192
285
  directories.add(current);
193
286
  current = path.dirname(current);
@@ -207,7 +300,9 @@ export async function syncHostWorkspaceFromSandbox(args: {
207
300
  sandboxWorkDir,
208
301
  );
209
302
  const hostEntries = await collectHostScopedEntries(hostWorkDir);
210
- const remoteFiles = new Set(remoteEntries.files);
303
+ const remoteFiles = new Set(
304
+ remoteEntries.files.map(file => file.relativePath),
305
+ );
211
306
  const requiredDirectories = buildRequiredDirectories(
212
307
  remoteEntries.directories,
213
308
  remoteEntries.files,
@@ -235,15 +330,11 @@ export async function syncHostWorkspaceFromSandbox(args: {
235
330
  await mkdir(path.join(hostWorkDir, relativePath), { recursive: true });
236
331
  }
237
332
 
238
- for (const relativePath of remoteEntries.files) {
239
- const remotePath = path.posix.join(
240
- sandboxWorkDir,
241
- relativePath.split(path.sep).join('/'),
242
- );
243
- const bytes = await sandbox.readBinaryFile({ path: remotePath });
333
+ for (const { relativePath, sandboxPath } of remoteEntries.files) {
334
+ const bytes = await sandbox.readBinaryFile({ path: sandboxPath });
244
335
  if (!bytes) {
245
336
  throw new Error(
246
- `Sandbox workspace file disappeared during mirror sync: ${remotePath}`,
337
+ `Sandbox workspace file disappeared during mirror sync: ${sandboxPath}`,
247
338
  );
248
339
  }
249
340
  const content = Buffer.from(bytes);