nativeapptemplate-agent 0.3.2 → 0.4.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.
Files changed (97) hide show
  1. package/AGENTS.md +1 -1
  2. package/README.md +36 -13
  3. package/dist/agents/judge.d.ts.map +1 -1
  4. package/dist/agents/judge.js +8 -2
  5. package/dist/agents/judge.js.map +1 -1
  6. package/dist/agents/types.d.ts +14 -0
  7. package/dist/agents/types.d.ts.map +1 -1
  8. package/dist/agents/workers/android.d.ts.map +1 -1
  9. package/dist/agents/workers/android.js +27 -3
  10. package/dist/agents/workers/android.js.map +1 -1
  11. package/dist/agents/workers/copy.d.ts +1 -0
  12. package/dist/agents/workers/copy.d.ts.map +1 -1
  13. package/dist/agents/workers/copy.js +23 -4
  14. package/dist/agents/workers/copy.js.map +1 -1
  15. package/dist/agents/workers/ios.d.ts.map +1 -1
  16. package/dist/agents/workers/ios.js +23 -3
  17. package/dist/agents/workers/ios.js.map +1 -1
  18. package/dist/agents/workers/rails.d.ts.map +1 -1
  19. package/dist/agents/workers/rails.js +6 -1
  20. package/dist/agents/workers/rails.js.map +1 -1
  21. package/dist/app-domain.d.ts +32 -0
  22. package/dist/app-domain.d.ts.map +1 -0
  23. package/dist/app-domain.js +93 -0
  24. package/dist/app-domain.js.map +1 -0
  25. package/dist/dispatch.d.ts +7 -0
  26. package/dist/dispatch.d.ts.map +1 -1
  27. package/dist/dispatch.js +61 -10
  28. package/dist/dispatch.js.map +1 -1
  29. package/dist/env-bridge.d.ts +1 -0
  30. package/dist/env-bridge.d.ts.map +1 -1
  31. package/dist/env-bridge.js +53 -16
  32. package/dist/env-bridge.js.map +1 -1
  33. package/dist/index.d.ts +4 -0
  34. package/dist/index.d.ts.map +1 -1
  35. package/dist/index.js +55 -9
  36. package/dist/index.js.map +1 -1
  37. package/dist/mcp.d.ts +5 -1
  38. package/dist/mcp.d.ts.map +1 -1
  39. package/dist/mcp.js +43 -16
  40. package/dist/mcp.js.map +1 -1
  41. package/dist/mobile.d.ts +1 -1
  42. package/dist/mobile.d.ts.map +1 -1
  43. package/dist/mobile.js +3 -3
  44. package/dist/mobile.js.map +1 -1
  45. package/dist/preflight.d.ts +27 -0
  46. package/dist/preflight.d.ts.map +1 -0
  47. package/dist/preflight.js +106 -0
  48. package/dist/preflight.js.map +1 -0
  49. package/dist/report/collect.d.ts +5 -0
  50. package/dist/report/collect.d.ts.map +1 -1
  51. package/dist/report/collect.js +15 -0
  52. package/dist/report/collect.js.map +1 -1
  53. package/dist/report/model.d.ts +14 -0
  54. package/dist/report/model.d.ts.map +1 -1
  55. package/dist/report/render.d.ts.map +1 -1
  56. package/dist/report/render.js +69 -6
  57. package/dist/report/render.js.map +1 -1
  58. package/dist/report/theme.d.ts +1 -1
  59. package/dist/report/theme.d.ts.map +1 -1
  60. package/dist/report/theme.js +1 -0
  61. package/dist/report/theme.js.map +1 -1
  62. package/dist/run-options.d.ts +7 -0
  63. package/dist/run-options.d.ts.map +1 -0
  64. package/dist/run-options.js +24 -0
  65. package/dist/run-options.js.map +1 -0
  66. package/dist/validation/autoformat.d.ts +9 -0
  67. package/dist/validation/autoformat.d.ts.map +1 -0
  68. package/dist/validation/autoformat.js +131 -0
  69. package/dist/validation/autoformat.js.map +1 -0
  70. package/dist/validation/launch.d.ts +23 -2
  71. package/dist/validation/launch.d.ts.map +1 -1
  72. package/dist/validation/launch.js +47 -18
  73. package/dist/validation/launch.js.map +1 -1
  74. package/dist/validation/layer1.d.ts.map +1 -1
  75. package/dist/validation/layer1.js +4 -0
  76. package/dist/validation/layer1.js.map +1 -1
  77. package/dist/validation/protected-references.d.ts +2 -0
  78. package/dist/validation/protected-references.d.ts.map +1 -0
  79. package/dist/validation/protected-references.js +18 -0
  80. package/dist/validation/protected-references.js.map +1 -0
  81. package/dist/validation/run-step.d.ts +16 -0
  82. package/dist/validation/run-step.d.ts.map +1 -0
  83. package/dist/validation/run-step.js +49 -0
  84. package/dist/validation/run-step.js.map +1 -0
  85. package/dist/validation/scenarios/queue.d.ts.map +1 -1
  86. package/dist/validation/scenarios/queue.js +7 -3
  87. package/dist/validation/scenarios/queue.js.map +1 -1
  88. package/dist/validation/stage2-judge.d.ts +4 -2
  89. package/dist/validation/stage2-judge.d.ts.map +1 -1
  90. package/dist/validation/stage2-judge.js +79 -4
  91. package/dist/validation/stage2-judge.js.map +1 -1
  92. package/dist/validation/test-suites.d.ts +10 -0
  93. package/dist/validation/test-suites.d.ts.map +1 -0
  94. package/dist/validation/test-suites.js +135 -0
  95. package/dist/validation/test-suites.js.map +1 -0
  96. package/package.json +1 -1
  97. package/scripts/ruby/rename.rb +97 -2
@@ -0,0 +1,10 @@
1
+ import type { JudgeResult, Platform, StepResult } from "../agents/types.js";
2
+ export type TestSuiteOptions = {
3
+ useMise?: boolean;
4
+ env?: NodeJS.ProcessEnv;
5
+ };
6
+ export declare function testsEnabled(): boolean;
7
+ export declare function runTestSuite(platform: Platform, outDir: string, options?: TestSuiteOptions): Promise<StepResult>;
8
+ export declare function extractSummary(output: string): string | undefined;
9
+ export declare function applyTestResults(judge: JudgeResult, results: Partial<Record<Platform, StepResult>>): JudgeResult;
10
+ //# sourceMappingURL=test-suites.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"test-suites.d.ts","sourceRoot":"","sources":["../../src/validation/test-suites.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,WAAW,EAAE,QAAQ,EAAE,UAAU,EAAE,MAAM,oBAAoB,CAAC;AAiB5E,MAAM,MAAM,gBAAgB,GAAG;IAE7B,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,GAAG,CAAC,EAAE,MAAM,CAAC,UAAU,CAAC;CACzB,CAAC;AAEF,wBAAgB,YAAY,IAAI,OAAO,CAEtC;AAED,wBAAsB,YAAY,CAAC,QAAQ,EAAE,QAAQ,EAAE,MAAM,EAAE,MAAM,EAAE,OAAO,GAAE,gBAAqB,GAAG,OAAO,CAAC,UAAU,CAAC,CAM1H;AAsDD,wBAAgB,cAAc,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CASjE;AAkCD,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,WAAW,EAAE,OAAO,EAAE,OAAO,CAAC,MAAM,CAAC,QAAQ,EAAE,UAAU,CAAC,CAAC,GAAG,WAAW,CAahH"}
@@ -0,0 +1,135 @@
1
+ import { access, readdir } from "node:fs/promises";
2
+ import { join } from "node:path";
3
+ import { iosSimulatorDestination } from "./ios-destination.js";
4
+ import { commandAvailable, runStep, tailLines, withMisePrefix } from "./run-step.js";
5
+ import { testsFromEnv } from "../run-options.js";
6
+ // SPEC §4 Operation 3: the generated apps' own test suites must pass. Layer 2
7
+ // only builds, and the suites take minutes (Rails ~650 tests, iOS ~1,100,
8
+ // Android ~900 on the paid substrates), so they run only when
9
+ // NATIVEAPPTEMPLATE_TESTS=1. A rename can break a test without breaking the
10
+ // build (#150: the iOS substrate's "unknown type" example became a real type).
11
+ const RAILS_TEST_TIMEOUT_MS = 1_800_000;
12
+ const IOS_TEST_TIMEOUT_MS = 2_700_000;
13
+ const ANDROID_TEST_TIMEOUT_MS = 1_800_000;
14
+ const DB_PREPARE_TIMEOUT_MS = 300_000;
15
+ const OUTPUT_TAIL_LINES = 60;
16
+ export function testsEnabled() {
17
+ return testsFromEnv();
18
+ }
19
+ export async function runTestSuite(platform, outDir, options = {}) {
20
+ switch (platform) {
21
+ case "rails": return runRailsTests(outDir, options);
22
+ case "ios": return runIosTests(outDir, options);
23
+ case "android": return runAndroidTests(outDir, options);
24
+ }
25
+ }
26
+ async function runRailsTests(outDir, options) {
27
+ if (!(await exists(join(outDir, "bin", "rails"))))
28
+ return skipped("bin/rails test", "no bin/rails in the generated app");
29
+ const useMise = options.useMise ?? (await commandAvailable("mise", options.env));
30
+ const env = options.env ? { env: options.env } : {};
31
+ // The generated app's own <slug>_api_test database; Layer 2's db:prepare
32
+ // only sets up development.
33
+ const prepare = await runStep(outDir, ["bin/rails", "db:test:prepare"], { timeoutMs: DB_PREPARE_TIMEOUT_MS, useMise, ...env });
34
+ if (prepare.exitCode !== 0) {
35
+ return fromRun(withMisePrefix("bin/rails db:test:prepare", useMise), prepare, "tests not run: db:test:prepare failed");
36
+ }
37
+ const run = await runStep(outDir, ["bin/rails", "test"], { timeoutMs: RAILS_TEST_TIMEOUT_MS, useMise, ...env });
38
+ return fromRun(withMisePrefix("bin/rails test", useMise), run);
39
+ }
40
+ async function runIosTests(outDir, options) {
41
+ let entries = [];
42
+ try {
43
+ entries = await readdir(outDir);
44
+ }
45
+ catch { /* reported below */ }
46
+ const xcodeproj = entries.find((e) => e.endsWith(".xcodeproj"));
47
+ if (!xcodeproj)
48
+ return skipped("xcodebuild test", `no .xcodeproj in ${outDir}`);
49
+ const scheme = xcodeproj.slice(0, -".xcodeproj".length);
50
+ // Same project, scheme, destination and configuration as Layer 2's build,
51
+ // so xcodebuild reuses that build.
52
+ const args = ["xcodebuild", "-project", xcodeproj, "-scheme", scheme, "-destination", iosSimulatorDestination(), "-configuration", "Debug", "test"];
53
+ const run = await runStep(outDir, args, { timeoutMs: IOS_TEST_TIMEOUT_MS, ...(options.env ? { env: options.env } : {}) });
54
+ return fromRun(`xcodebuild test -scheme ${scheme}`, run);
55
+ }
56
+ async function runAndroidTests(outDir, options) {
57
+ if (!(await exists(join(outDir, "gradlew"))))
58
+ return skipped("./gradlew testDebugUnitTest", "no ./gradlew wrapper in the generated app");
59
+ const run = await runStep(outDir, ["./gradlew", "testDebugUnitTest", "--no-daemon"], {
60
+ timeoutMs: ANDROID_TEST_TIMEOUT_MS,
61
+ ...(options.env ? { env: options.env } : {}),
62
+ });
63
+ return fromRun("./gradlew testDebugUnitTest", run);
64
+ }
65
+ // The last line each runner prints about the whole run:
66
+ // Minitest: "643 runs, 2108 assertions, 0 failures, 0 errors, 0 skips"
67
+ // XCTest: "Executed 1117 tests, with 1 failure (0 unexpected) in ..."
68
+ // Swift Testing: "✘ Test run with 1117 tests in 85 suites failed after ..."
69
+ // xcodebuild: "** TEST SUCCEEDED **" / "** TEST FAILED **"
70
+ // Gradle: "BUILD SUCCESSFUL in 1m 2s" / "BUILD FAILED in 40s",
71
+ // "888 tests completed, 1 failed"
72
+ const SUMMARY_PATTERNS = [
73
+ /^\d+ runs, \d+ assertions, \d+ failures, \d+ errors, \d+ skips$/,
74
+ /Executed \d+ tests?, with \d+ failures?.*$/,
75
+ /Test run with \d+ tests?.*$/,
76
+ /^\d+ tests completed, \d+ failed.*$/,
77
+ /^\*\* TEST (SUCCEEDED|FAILED) \*\*$/,
78
+ /^BUILD (SUCCESSFUL|FAILED) in .*$/,
79
+ ];
80
+ export function extractSummary(output) {
81
+ const lines = output.split("\n").map((l) => l.trim()).filter(Boolean);
82
+ for (const pattern of SUMMARY_PATTERNS) {
83
+ for (let i = lines.length - 1; i >= 0; i--) {
84
+ const m = lines[i].match(pattern);
85
+ if (m)
86
+ return m[0];
87
+ }
88
+ }
89
+ return undefined;
90
+ }
91
+ function fromRun(command, run, failNote) {
92
+ const pass = run.exitCode === 0;
93
+ const tail = tailLines(run.output, OUTPUT_TAIL_LINES);
94
+ const summary = extractSummary(run.output);
95
+ return {
96
+ status: pass ? "pass" : "fail",
97
+ command,
98
+ exitCode: run.exitCode,
99
+ durationMs: run.durationMs,
100
+ ...(tail ? { outputTail: tail } : {}),
101
+ ...(summary ? { summary } : {}),
102
+ ...(!pass && failNote ? { note: failNote } : {}),
103
+ ...(!pass && !failNote && run.timedOut ? { note: "timed out" } : {}),
104
+ };
105
+ }
106
+ function skipped(command, note) {
107
+ return { status: "skipped", command, exitCode: null, durationMs: 0, note };
108
+ }
109
+ async function exists(path) {
110
+ try {
111
+ await access(path);
112
+ return true;
113
+ }
114
+ catch {
115
+ return false;
116
+ }
117
+ }
118
+ // Fold suite results into the judge result: each platform carries its
119
+ // `tests`, and an opted-in suite that fails fails the run (SPEC §4
120
+ // Operation 3). Skipped suites are shown but don't count.
121
+ export function applyTestResults(judge, results) {
122
+ const platforms = (judge.platforms ?? []).map((p) => {
123
+ const tests = results[p.platform];
124
+ return tests ? { ...p, tests } : p;
125
+ });
126
+ const ran = platforms.filter((p) => p.tests && p.tests.status !== "skipped");
127
+ const passed = ran.filter((p) => p.tests.status === "pass").length;
128
+ return {
129
+ ...judge,
130
+ overallPass: judge.overallPass && passed === ran.length,
131
+ summary: `${judge.summary} · tests ${passed}/${ran.length} pass`,
132
+ platforms,
133
+ };
134
+ }
135
+ //# sourceMappingURL=test-suites.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"test-suites.js","sourceRoot":"","sources":["../../src/validation/test-suites.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nativeapptemplate-agent",
3
- "version": "0.3.2",
3
+ "version": "0.4.1",
4
4
  "description": "A Claude Code agent that turns a natural-language SaaS spec into a working three-platform implementation (Rails API + SwiftUI iOS + Jetpack Compose Android).",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -3,6 +3,9 @@
3
3
  # Apply a rename plan to a Rails project tree.
4
4
  # Stdin: { "renamePlan": [{"from":"Shop","to":"Clinic"}, ...],
5
5
  # "japaneseRenamePlan": [{"from":"店舗","to":"医院"}, ...], (optional)
6
+ # "appDomain": {"domain":"orangebaton.app", (optional)
7
+ # "bundleIdPrefix":"app.orangebaton",
8
+ # "androidPackagePrefix":"app.orangebaton"},
6
9
  # "root": "/abs/path" }
7
10
  # Stdout: { "files_scanned": N, "files_changed": N, "substitutions": N, "files_renamed": N }
8
11
  #
@@ -15,10 +18,12 @@
15
18
  # them from their (un-renamed) sources, e.g. Turbo's VisitState.completed.
16
19
 
17
20
  require "json"
21
+ require "fileutils"
18
22
 
19
23
  input = JSON.parse($stdin.read)
20
24
  plan = input.fetch("renamePlan")
21
25
  japanese_plan = input.fetch("japaneseRenamePlan", [])
26
+ app_domain = input["appDomain"]
22
27
  root = input.fetch("root")
23
28
 
24
29
  stats = { files_scanned: 0, files_changed: 0, substitutions: 0, files_renamed: 0 }
@@ -187,7 +192,32 @@ japanese_patterns = japanese_plan
187
192
  .sort_by { |p| -p.fetch("from").length }
188
193
  .map { |p| [/#{Regexp.escape(p.fetch("from"))}/, p.fetch("to")] }
189
194
 
190
- all_patterns = japanese_patterns + plan.flat_map { |p| build_patterns(p.fetch("from"), p.fetch("to")) }
195
+ # The template's domain and reverse-DNS prefix, when the app has a domain of
196
+ # its own (src/app-domain.ts). Matched whole before any product-name pattern
197
+ # can reach the `nativeapptemplate` inside them; the single-pass substitution
198
+ # below then renames what follows the prefix
199
+ # (com.nativeapptemplate.NativeAppTemplate.ios -> app.orangebaton.OrangeBaton.ios)
200
+ # and never re-scans the replaced text. The Android package (the lowercase
201
+ # product after the prefix, also in google-services.json and the API's FCM
202
+ # test) takes the Java-safe prefix; every other com.nativeapptemplate (iOS
203
+ # bundle IDs, keychain services, log subsystems) the plain reverse domain. Without appDomain these are absent and
204
+ # the flat product rule turns both into <flat product>.com / com.<flat product>.
205
+ def domain_patterns(app_domain)
206
+ return [] unless app_domain
207
+
208
+ domain = app_domain.fetch("domain")
209
+ bundle = app_domain.fetch("bundleIdPrefix")
210
+ package = app_domain.fetch("androidPackagePrefix")
211
+ android_product = "nativeapptemplate[a-z0-9]*(?![A-Za-z0-9_])"
212
+ [
213
+ [/(?<![A-Za-z0-9_-])nativeapptemplate\.com(?![A-Za-z0-9_-]|\.[A-Za-z0-9])/, domain],
214
+ [/(?<![A-Za-z0-9_.-])com\.nativeapptemplate(?=\.#{android_product})/, package],
215
+ [/(?<![A-Za-z0-9_-])com\/nativeapptemplate(?=\/#{android_product})/, package.tr(".", "/")],
216
+ [/(?<![A-Za-z0-9_.-])com\.nativeapptemplate(?![A-Za-z0-9_-])/, bundle],
217
+ ]
218
+ end
219
+
220
+ all_patterns = domain_patterns(app_domain) + japanese_patterns + plan.flat_map { |p| build_patterns(p.fetch("from"), p.fetch("to")) }
191
221
 
192
222
  # One combined regex, applied in a single pass, so a replacement is never
193
223
  # re-scanned by a later pair: with NativeAppTemplate→PetShop and Shop→Store,
@@ -207,6 +237,37 @@ def rename_all(text)
207
237
  [result, count]
208
238
  end
209
239
 
240
+ # References to the template's own GitHub repos and the agent's name are left
241
+ # byte-for-byte as they are: renamed, they become dead links
242
+ # (github.com/orangebaton/OrangeBaton-iOS) and a tool that doesn't exist
243
+ # (orangebaton-agent). They are masked with placeholders before the
244
+ # substitution and restored after it, so nothing inside them is renamed: not
245
+ # the product name, nor a Shop / ItemTag token in a URL path. The product's
246
+ # domains and emails (nativeapptemplate.com) are not protected; they become
247
+ # the app's own. Mirrored in src/validation/protected-references.ts, which
248
+ # keeps Layer 1 from flagging what is left here.
249
+ PROTECTED_REFERENCE = %r{
250
+ github\.com[/:]nativeapptemplate(?![A-Za-z0-9_-])(?:/[^\s)\]"'<>`]*)?
251
+ |
252
+ (?<![A-Za-z0-9_-])nativeapptemplate-agent(?:-mcp)?(?![A-Za-z0-9_])
253
+ }x
254
+ MASK_OPEN = "\u{E000}"
255
+ MASK_CLOSE = "\u{E001}"
256
+ MASK = /#{MASK_OPEN}(\d+)#{MASK_CLOSE}/
257
+
258
+ def rename_content(text)
259
+ # A file that already holds the sentinel can't be masked unambiguously.
260
+ return rename_all(text) if text.include?(MASK_OPEN)
261
+
262
+ spans = []
263
+ masked = text.gsub(PROTECTED_REFERENCE) do
264
+ spans << Regexp.last_match(0)
265
+ "#{MASK_OPEN}#{spans.length - 1}#{MASK_CLOSE}"
266
+ end
267
+ result, count = rename_all(masked)
268
+ [result.gsub(MASK) { spans[Regexp.last_match(1).to_i] }, count]
269
+ end
270
+
210
271
  def skip?(path, root)
211
272
  # Compare against the path RELATIVE to the project root, not the absolute
212
273
  # path. Otherwise SKIP_DIR_SEGMENTS like "tmp" / "log" / "build" silently
@@ -245,7 +306,7 @@ Dir.glob("#{root}/**/*", File::FNM_DOTMATCH).each do |path|
245
306
  next
246
307
  end
247
308
 
248
- content, local_subst = rename_all(original)
309
+ content, local_subst = rename_content(original)
249
310
  next if content == original
250
311
 
251
312
  File.write(path, content)
@@ -253,10 +314,44 @@ Dir.glob("#{root}/**/*", File::FNM_DOTMATCH).each do |path|
253
314
  stats[:substitutions] += local_subst
254
315
  end
255
316
 
317
+ # Pass 1.5 — move the Android package directories (<source root>/com/
318
+ # nativeapptemplate/nativeapptemplate) under the app's package prefix
319
+ # (app/orangebaton/nativeapptemplate; Pass 2 then renames the last segment).
320
+ # The directories made for the prefix are pinned so Pass 2 leaves them as they
321
+ # are even when a domain label happens to be a rename token (shop.example).
322
+ pinned = {}
323
+ if app_domain
324
+ package_dirs = app_domain.fetch("androidPackagePrefix").split(".")
325
+ Dir.glob("#{root}/**/com/nativeapptemplate", File::FNM_DOTMATCH).sort.each do |template_dir|
326
+ next if skip?(template_dir, root)
327
+ next unless File.directory?(template_dir) && !File.symlink?(template_dir)
328
+ next unless Dir.children(template_dir).any? { |c| c.match?(/\Anativeapptemplate[a-z0-9]*\z/) }
329
+
330
+ source_root = File.dirname(File.dirname(template_dir))
331
+ target = File.join(source_root, *package_dirs)
332
+ if File.exist?(target)
333
+ warn "rename.rb: not moving #{template_dir}: #{target} already exists"
334
+ next
335
+ end
336
+ FileUtils.mkdir_p(File.dirname(target))
337
+ File.rename(template_dir, target)
338
+ stats[:files_renamed] += 1
339
+ com_dir = File.dirname(template_dir)
340
+ # Finder's .DS_Store alone doesn't keep the template's com/ directory.
341
+ leftovers = Dir.children(com_dir)
342
+ if leftovers.all? { |c| c == ".DS_Store" }
343
+ leftovers.each { |c| File.delete(File.join(com_dir, c)) }
344
+ Dir.rmdir(com_dir)
345
+ end
346
+ package_dirs.each_index { |i| pinned[File.join(source_root, *package_dirs[0..i])] = true }
347
+ end
348
+ end
349
+
256
350
  # Pass 2 — rename paths. Deepest-first so renaming a parent directory
257
351
  # doesn't invalidate paths we haven't visited yet.
258
352
  Dir.glob("#{root}/**/*", File::FNM_DOTMATCH).sort_by { |p| -p.length }.each do |path|
259
353
  next if skip?(path, root)
354
+ next if pinned[path]
260
355
  next unless File.exist?(path)
261
356
 
262
357
  old_name = File.basename(path)