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.
- package/AGENTS.md +1 -1
- package/README.md +36 -13
- package/dist/agents/judge.d.ts.map +1 -1
- package/dist/agents/judge.js +8 -2
- package/dist/agents/judge.js.map +1 -1
- package/dist/agents/types.d.ts +14 -0
- package/dist/agents/types.d.ts.map +1 -1
- package/dist/agents/workers/android.d.ts.map +1 -1
- package/dist/agents/workers/android.js +27 -3
- package/dist/agents/workers/android.js.map +1 -1
- package/dist/agents/workers/copy.d.ts +1 -0
- package/dist/agents/workers/copy.d.ts.map +1 -1
- package/dist/agents/workers/copy.js +23 -4
- package/dist/agents/workers/copy.js.map +1 -1
- package/dist/agents/workers/ios.d.ts.map +1 -1
- package/dist/agents/workers/ios.js +23 -3
- package/dist/agents/workers/ios.js.map +1 -1
- package/dist/agents/workers/rails.d.ts.map +1 -1
- package/dist/agents/workers/rails.js +6 -1
- package/dist/agents/workers/rails.js.map +1 -1
- package/dist/app-domain.d.ts +32 -0
- package/dist/app-domain.d.ts.map +1 -0
- package/dist/app-domain.js +93 -0
- package/dist/app-domain.js.map +1 -0
- package/dist/dispatch.d.ts +7 -0
- package/dist/dispatch.d.ts.map +1 -1
- package/dist/dispatch.js +61 -10
- package/dist/dispatch.js.map +1 -1
- package/dist/env-bridge.d.ts +1 -0
- package/dist/env-bridge.d.ts.map +1 -1
- package/dist/env-bridge.js +53 -16
- package/dist/env-bridge.js.map +1 -1
- package/dist/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +55 -9
- package/dist/index.js.map +1 -1
- package/dist/mcp.d.ts +5 -1
- package/dist/mcp.d.ts.map +1 -1
- package/dist/mcp.js +43 -16
- package/dist/mcp.js.map +1 -1
- package/dist/mobile.d.ts +1 -1
- package/dist/mobile.d.ts.map +1 -1
- package/dist/mobile.js +3 -3
- package/dist/mobile.js.map +1 -1
- package/dist/preflight.d.ts +27 -0
- package/dist/preflight.d.ts.map +1 -0
- package/dist/preflight.js +106 -0
- package/dist/preflight.js.map +1 -0
- package/dist/report/collect.d.ts +5 -0
- package/dist/report/collect.d.ts.map +1 -1
- package/dist/report/collect.js +15 -0
- package/dist/report/collect.js.map +1 -1
- package/dist/report/model.d.ts +14 -0
- package/dist/report/model.d.ts.map +1 -1
- package/dist/report/render.d.ts.map +1 -1
- package/dist/report/render.js +69 -6
- package/dist/report/render.js.map +1 -1
- package/dist/report/theme.d.ts +1 -1
- package/dist/report/theme.d.ts.map +1 -1
- package/dist/report/theme.js +1 -0
- package/dist/report/theme.js.map +1 -1
- package/dist/run-options.d.ts +7 -0
- package/dist/run-options.d.ts.map +1 -0
- package/dist/run-options.js +24 -0
- package/dist/run-options.js.map +1 -0
- package/dist/validation/autoformat.d.ts +9 -0
- package/dist/validation/autoformat.d.ts.map +1 -0
- package/dist/validation/autoformat.js +131 -0
- package/dist/validation/autoformat.js.map +1 -0
- package/dist/validation/launch.d.ts +23 -2
- package/dist/validation/launch.d.ts.map +1 -1
- package/dist/validation/launch.js +47 -18
- package/dist/validation/launch.js.map +1 -1
- package/dist/validation/layer1.d.ts.map +1 -1
- package/dist/validation/layer1.js +4 -0
- package/dist/validation/layer1.js.map +1 -1
- package/dist/validation/protected-references.d.ts +2 -0
- package/dist/validation/protected-references.d.ts.map +1 -0
- package/dist/validation/protected-references.js +18 -0
- package/dist/validation/protected-references.js.map +1 -0
- package/dist/validation/run-step.d.ts +16 -0
- package/dist/validation/run-step.d.ts.map +1 -0
- package/dist/validation/run-step.js +49 -0
- package/dist/validation/run-step.js.map +1 -0
- package/dist/validation/scenarios/queue.d.ts.map +1 -1
- package/dist/validation/scenarios/queue.js +7 -3
- package/dist/validation/scenarios/queue.js.map +1 -1
- package/dist/validation/stage2-judge.d.ts +4 -2
- package/dist/validation/stage2-judge.d.ts.map +1 -1
- package/dist/validation/stage2-judge.js +79 -4
- package/dist/validation/stage2-judge.js.map +1 -1
- package/dist/validation/test-suites.d.ts +10 -0
- package/dist/validation/test-suites.d.ts.map +1 -0
- package/dist/validation/test-suites.js +135 -0
- package/dist/validation/test-suites.js.map +1 -0
- package/package.json +1 -1
- 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
|
+
"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",
|
package/scripts/ruby/rename.rb
CHANGED
|
@@ -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
|
-
|
|
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 =
|
|
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)
|