apphx 1.1.0 → 2.0.0

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 (120) hide show
  1. package/README.md +50 -182
  2. package/benchmarks/peer-context-worker.mjs +186 -0
  3. package/benchmarks/peer-context.mjs +150 -0
  4. package/dist/apple-types.js.map +1 -1
  5. package/dist/cli.js +370 -118
  6. package/dist/cli.js.map +1 -1
  7. package/dist/config.js +42 -4
  8. package/dist/config.js.map +1 -1
  9. package/dist/dependencies.js +455 -16
  10. package/dist/dependencies.js.map +1 -1
  11. package/dist/export-options.js +44 -2
  12. package/dist/export-options.js.map +1 -1
  13. package/dist/flutter.js +13 -25
  14. package/dist/flutter.js.map +1 -1
  15. package/dist/go-dependencies.js +389 -0
  16. package/dist/go-dependencies.js.map +1 -0
  17. package/dist/inbox.js +7 -8
  18. package/dist/inbox.js.map +1 -1
  19. package/dist/integrations.js +3 -3
  20. package/dist/isolation.js +190 -0
  21. package/dist/isolation.js.map +1 -0
  22. package/dist/macos.js +57 -0
  23. package/dist/macos.js.map +1 -0
  24. package/dist/mygo-build.js +55 -0
  25. package/dist/mygo-build.js.map +1 -0
  26. package/dist/policy.js +35 -0
  27. package/dist/policy.js.map +1 -0
  28. package/dist/preflight.js +1 -1
  29. package/dist/process.js +3 -3
  30. package/dist/project-profiles.js +163 -0
  31. package/dist/project-profiles.js.map +1 -0
  32. package/dist/proof.js +33 -0
  33. package/dist/proof.js.map +1 -0
  34. package/dist/remote.js +1 -1
  35. package/dist/runner.js +269 -287
  36. package/dist/runner.js.map +1 -1
  37. package/dist/scheduler.js +0 -3
  38. package/dist/scheduler.js.map +1 -1
  39. package/dist/schemas.js +156 -44
  40. package/dist/schemas.js.map +1 -1
  41. package/dist/store.js +166 -44
  42. package/dist/store.js.map +1 -1
  43. package/dist/vendor/obfuscator/dist/cli.js +72 -0
  44. package/dist/vendor/obfuscator/dist/cli.js.map +1 -0
  45. package/dist/vendor/obfuscator/dist/code.js +585 -0
  46. package/dist/vendor/obfuscator/dist/code.js.map +1 -0
  47. package/dist/vendor/obfuscator/dist/common.js +262 -0
  48. package/dist/vendor/obfuscator/dist/common.js.map +1 -0
  49. package/dist/vendor/obfuscator/dist/config.js +123 -0
  50. package/dist/vendor/obfuscator/dist/config.js.map +1 -0
  51. package/dist/vendor/obfuscator/dist/content.js +89 -0
  52. package/dist/vendor/obfuscator/dist/content.js.map +1 -0
  53. package/dist/vendor/obfuscator/dist/obfuscate.js +93 -0
  54. package/dist/vendor/obfuscator/dist/obfuscate.js.map +1 -0
  55. package/dist/vendor/obfuscator/dist/png.js +283 -0
  56. package/dist/vendor/obfuscator/dist/png.js.map +1 -0
  57. package/dist/vendor/obfuscator/dist/project.js +159 -0
  58. package/dist/vendor/obfuscator/dist/project.js.map +1 -0
  59. package/dist/vendor/obfuscator/dist/resources.js +335 -0
  60. package/dist/vendor/obfuscator/dist/resources.js.map +1 -0
  61. package/dist/vendor/obfuscator/package.json +22 -0
  62. package/dist/workspace.js +113 -0
  63. package/dist/workspace.js.map +1 -0
  64. package/evals/control.mjs +73 -0
  65. package/evals/expectations.mjs +37 -0
  66. package/evals/host.mjs +51 -0
  67. package/evals/oracle.mjs +80 -0
  68. package/evals/policy.mjs +268 -0
  69. package/examples/android-java.factory.json +18 -0
  70. package/examples/android-java.json +16 -0
  71. package/examples/existing-project.factory.json +18 -0
  72. package/examples/existing-project.json +17 -0
  73. package/examples/expo.eas.json +17 -0
  74. package/examples/expo.factory.json +18 -0
  75. package/examples/expo.json +16 -0
  76. package/examples/factory.json +0 -2
  77. package/examples/flutter.factory.json +13 -0
  78. package/examples/flutter.json +16 -0
  79. package/examples/luggage.json +5 -3
  80. package/examples/mygo-web.factory.json +18 -0
  81. package/examples/mygo-web.json +16 -0
  82. package/examples/mygo.factory.json +18 -0
  83. package/examples/mygo.json +16 -0
  84. package/examples/react-native.factory.json +18 -0
  85. package/examples/react-native.json +16 -0
  86. package/examples/runtime/android-java/app/build.gradle +7 -0
  87. package/examples/runtime/android-java/app/src/main/AndroidManifest.xml +8 -0
  88. package/examples/runtime/android-java/app/src/main/java/dev/apphx/runtime/MainActivity.java +44 -0
  89. package/examples/runtime/android-java/sdk-build.mjs +29 -0
  90. package/examples/runtime/android-java/settings.gradle +2 -0
  91. package/examples/runtime/android-ui.mjs +70 -0
  92. package/examples/runtime/mygo-native/go.mod +13 -0
  93. package/examples/runtime/mygo-native/go.sum +10 -0
  94. package/examples/runtime/mygo-native/main.go +173 -0
  95. package/examples/runtime/mygo-native/main_test.go +14 -0
  96. package/examples/runtime/mygo-native/mygo.json +1 -0
  97. package/examples/runtime/mygo-web/bun.lock +15 -0
  98. package/examples/runtime/mygo-web/go.mod +9 -0
  99. package/examples/runtime/mygo-web/go.sum +8 -0
  100. package/examples/runtime/mygo-web/index.html +1 -0
  101. package/examples/runtime/mygo-web/main.go +60 -0
  102. package/examples/runtime/mygo-web/mygo.config.ts +10 -0
  103. package/examples/runtime/mygo-web/package.json +1 -0
  104. package/examples/runtime/mygo-web/src/main.ts +23 -0
  105. package/examples/runtime/mygo-web/src/mygo.ts +14 -0
  106. package/examples/swift.factory.json +18 -0
  107. package/examples/swift.json +16 -0
  108. package/examples/uni-app.factory.json +18 -0
  109. package/examples/uni-app.json +16 -0
  110. package/package.json +12 -5
  111. package/scripts/bundle-obfuscator.mjs +19 -0
  112. package/scripts/runtime-smoke.mjs +209 -0
  113. package/skills/apphx/SKILL.md +94 -17
  114. package/skills/apphx/resources/google-play-research-result.json +6 -0
  115. package/skills/apphx/resources/local-research-result.json +6 -0
  116. package/skills/apphx/resources/requirement.json +5 -8
  117. package/skills/apphx/resources/research-result.json +5 -14
  118. package/skills/apphx/resources/review-result.json +1 -1
  119. package/dist/apple.js +0 -900
  120. package/dist/apple.js.map +0 -1
@@ -1 +1 @@
1
- {"version":3,"file":"apple-types.js","names":[],"sources":["../src/apple-types.ts"],"sourcesContent":["import { z } from 'zod';\n\nconst text = z.string().min(1);\nconst effect = z.strictObject({ id: text.optional() });\n\nexport const AppleReceiptSchema = z.strictObject({\n identity: z.strictObject({ appId: text, bundleId: text, version: text, buildNumber: text }).optional(),\n pending: z.strictObject({ key: text, path: text.optional(), method: z.enum(['POST', 'PATCH', 'PUT']).optional(), startedAt: z.iso.datetime().optional() }).nullable().optional(),\n effects: z.record(z.string(), effect).optional(),\n lastEffect: z.strictObject({ key: text, id: text.optional(), reconciled: z.boolean().optional(), completedAt: z.iso.datetime().optional() }).optional(),\n upload: z.strictObject({ status: z.enum(['intent', 'delivered', 'reconciled']), startedAt: z.iso.datetime().optional() }).optional(),\n phase: z.enum(['processing', 'submitted']).optional(),\n versionId: text.optional(),\n buildId: text.optional(),\n submissionId: text.optional(),\n assetParts: z.record(z.string(), z.number().int().nonnegative().safe()).optional(),\n});\n\nexport interface AppleReceipt extends z.infer<typeof AppleReceiptSchema> {}\n\nexport interface ApplePollResult {\n status: 'waiting' | 'approved' | 'rejected';\n detail: string;\n distributionReady?: boolean;\n available?: boolean;\n}\n"],"mappings":";;AAEA,MAAM,OAAO,EAAE,OAAO,CAAC,CAAC,IAAI,CAAC;AAC7B,MAAM,SAAS,EAAE,aAAa,EAAE,IAAI,KAAK,SAAS,EAAE,CAAC;AAErD,MAAa,qBAAqB,EAAE,aAAa;CAC/C,UAAU,EAAE,aAAa;EAAE,OAAO;EAAM,UAAU;EAAM,SAAS;EAAM,aAAa;CAAK,CAAC,CAAC,CAAC,SAAS;CACrG,SAAS,EAAE,aAAa;EAAE,KAAK;EAAM,MAAM,KAAK,SAAS;EAAG,QAAQ,EAAE,KAAK;GAAC;GAAQ;GAAS;EAAK,CAAC,CAAC,CAAC,SAAS;EAAG,WAAW,EAAE,IAAI,SAAS,CAAC,CAAC,SAAS;CAAE,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,SAAS;CAC/K,SAAS,EAAE,OAAO,EAAE,OAAO,GAAG,MAAM,CAAC,CAAC,SAAS;CAC/C,YAAY,EAAE,aAAa;EAAE,KAAK;EAAM,IAAI,KAAK,SAAS;EAAG,YAAY,EAAE,QAAQ,CAAC,CAAC,SAAS;EAAG,aAAa,EAAE,IAAI,SAAS,CAAC,CAAC,SAAS;CAAE,CAAC,CAAC,CAAC,SAAS;CACtJ,QAAQ,EAAE,aAAa;EAAE,QAAQ,EAAE,KAAK;GAAC;GAAU;GAAa;EAAY,CAAC;EAAG,WAAW,EAAE,IAAI,SAAS,CAAC,CAAC,SAAS;CAAE,CAAC,CAAC,CAAC,SAAS;CACnI,OAAO,EAAE,KAAK,CAAC,cAAc,WAAW,CAAC,CAAC,CAAC,SAAS;CACpD,WAAW,KAAK,SAAS;CACzB,SAAS,KAAK,SAAS;CACvB,cAAc,KAAK,SAAS;CAC5B,YAAY,EAAE,OAAO,EAAE,OAAO,GAAG,EAAE,OAAO,CAAC,CAAC,IAAI,CAAC,CAAC,YAAY,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,SAAS;AACnF,CAAC"}
1
+ {"version":3,"file":"apple-types.js","names":[],"sources":["../src/apple-types.ts"],"sourcesContent":["import { z } from 'zod';\n\nconst text = z.string().min(1);\nconst effect = z.strictObject({ id: text.optional() });\n\nexport const AppleReceiptSchema = z.strictObject({\n identity: z.strictObject({ appId: text, bundleId: text, version: text, buildNumber: text }).optional(),\n pending: z.strictObject({ key: text, path: text.optional(), method: z.enum(['POST', 'PATCH', 'PUT']).optional(), startedAt: z.iso.datetime().optional() }).nullable().optional(),\n effects: z.record(z.string(), effect).optional(),\n lastEffect: z.strictObject({ key: text, id: text.optional(), reconciled: z.boolean().optional(), completedAt: z.iso.datetime().optional() }).optional(),\n upload: z.strictObject({ status: z.enum(['intent', 'delivered', 'reconciled']), startedAt: z.iso.datetime().optional() }).optional(),\n phase: z.enum(['processing', 'submitted']).optional(),\n versionId: text.optional(),\n buildId: text.optional(),\n submissionId: text.optional(),\n assetParts: z.record(z.string(), z.number().int().nonnegative().safe()).optional(),\n});\n\nexport interface AppleReceipt extends z.infer<typeof AppleReceiptSchema> {}\n\n"],"mappings":";;AAEA,MAAM,OAAO,EAAE,OAAO,CAAC,CAAC,IAAI,CAAC;AAC7B,MAAM,SAAS,EAAE,aAAa,EAAE,IAAI,KAAK,SAAS,EAAE,CAAC;AAErD,MAAa,qBAAqB,EAAE,aAAa;CAC/C,UAAU,EAAE,aAAa;EAAE,OAAO;EAAM,UAAU;EAAM,SAAS;EAAM,aAAa;CAAK,CAAC,CAAC,CAAC,SAAS;CACrG,SAAS,EAAE,aAAa;EAAE,KAAK;EAAM,MAAM,KAAK,SAAS;EAAG,QAAQ,EAAE,KAAK;GAAC;GAAQ;GAAS;EAAK,CAAC,CAAC,CAAC,SAAS;EAAG,WAAW,EAAE,IAAI,SAAS,CAAC,CAAC,SAAS;CAAE,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,SAAS;CAC/K,SAAS,EAAE,OAAO,EAAE,OAAO,GAAG,MAAM,CAAC,CAAC,SAAS;CAC/C,YAAY,EAAE,aAAa;EAAE,KAAK;EAAM,IAAI,KAAK,SAAS;EAAG,YAAY,EAAE,QAAQ,CAAC,CAAC,SAAS;EAAG,aAAa,EAAE,IAAI,SAAS,CAAC,CAAC,SAAS;CAAE,CAAC,CAAC,CAAC,SAAS;CACtJ,QAAQ,EAAE,aAAa;EAAE,QAAQ,EAAE,KAAK;GAAC;GAAU;GAAa;EAAY,CAAC;EAAG,WAAW,EAAE,IAAI,SAAS,CAAC,CAAC,SAAS;CAAE,CAAC,CAAC,CAAC,SAAS;CACnI,OAAO,EAAE,KAAK,CAAC,cAAc,WAAW,CAAC,CAAC,CAAC,SAAS;CACpD,WAAW,KAAK,SAAS;CACzB,SAAS,KAAK,SAAS;CACvB,cAAc,KAAK,SAAS;CAC5B,YAAY,EAAE,OAAO,EAAE,OAAO,GAAG,EAAE,OAAO,CAAC,CAAC,IAAI,CAAC,CAAC,YAAY,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,SAAS;AACnF,CAAC"}
package/dist/cli.js CHANGED
@@ -1,17 +1,23 @@
1
1
  #!/usr/bin/env node
2
- import { toError } from "./schemas.js";
2
+ import { validateMyGoCommand } from "./mygo-build.js";
3
+ import { FrameworkSchema, PackageTargetSchema, PlatformSchema, toError, validateJobConfig } from "./schemas.js";
4
+ import { canonicalPath, expandArgv, loadConfig, loadJSON, normalizeIsolationPaths, validateProjectSource } from "./config.js";
3
5
  import { executionEnvironment, run } from "./process.js";
4
- import { appleDoctor, validateAppleSpec } from "./apple.js";
5
- import { loadConfig, loadJSON } from "./config.js";
6
+ import { closestOwnedGoWorkspace } from "./go-dependencies.js";
6
7
  import { Store } from "./store.js";
8
+ import { prepareIsolatedCommand, removeConfinedPath } from "./isolation.js";
9
+ import { assertProject, doctorCommands } from "./project-profiles.js";
10
+ import { invalidateVerification, verificationConfigChanged } from "./proof.js";
7
11
  import { abandonTurn, completeTurn, nextTurn } from "./runner.js";
8
12
  import { runStep } from "./scheduler.js";
9
13
  import { hosts, inspectIntegration, installIntegration, uninstallIntegration, updateIntegration } from "./integrations.js";
10
14
  import { MAX_INPUT_BYTES, parseRequirement, readInputFile } from "./requirements.js";
11
15
  import { scanInbox, submitRequirement } from "./inbox.js";
12
16
  import { dispatchRemote } from "./remote.js";
17
+ import { existsSync, mkdirSync, mkdtempSync, readFileSync, writeFileSync } from "node:fs";
18
+ import { spawnSync } from "node:child_process";
13
19
  import { dirname, join, resolve } from "node:path";
14
- import { mkdirSync, readFileSync, writeFileSync } from "node:fs";
20
+ import { tmpdir } from "node:os";
15
21
  import { fileURLToPath } from "node:url";
16
22
  import { parseArgs } from "node:util";
17
23
  // src/cli.ts
@@ -20,7 +26,10 @@ const print = (value) => console.log(JSON.stringify(value, null, 2));
20
26
  const view = (job) => ({
21
27
  id: job.id,
22
28
  product: job.spec.id,
23
- version: job.spec.apple?.version ?? "1.0.0",
29
+ framework: job.spec.framework,
30
+ platform: job.spec.platform,
31
+ channel: job.spec.channel,
32
+ version: job.spec.version,
24
33
  stage: job.stage,
25
34
  status: job.status,
26
35
  hostTurns: job.hostTurns,
@@ -41,10 +50,38 @@ function outcome(job) {
41
50
  print(view(job));
42
51
  if (job.status === "blocked" || job.status === "failed") process.exitCode = 2;
43
52
  }
44
- async function probe(argv, config) {
53
+ async function probe(argv, config, context) {
54
+ let temporary;
55
+ let isolated;
45
56
  try {
46
- const result = await run(argv, {
47
- env: executionEnvironment(config),
57
+ const env = context?.env ?? executionEnvironment(config);
58
+ const expanded = expandArgv(argv, env);
59
+ validateMyGoCommand(config, expanded);
60
+ if (config.isolation) {
61
+ if (!config.allowLocalExecution) return {
62
+ available: false,
63
+ detail: "Owner must enable local execution before isolated probes run"
64
+ };
65
+ temporary = mkdtempSync(join(tmpdir(), "apphx-doctor-"));
66
+ const scoped = {
67
+ ...config,
68
+ isolation: {
69
+ ...config.isolation,
70
+ readOnlyPaths: [...config.isolation.readOnlyPaths, ...context?.sourceRoot ? [canonicalPath(context.sourceRoot)] : []]
71
+ }
72
+ };
73
+ normalizeIsolationPaths(scoped);
74
+ isolated = prepareIsolatedCommand(expanded, {
75
+ config: scoped,
76
+ workspace: temporary,
77
+ cwd: context?.cwd ?? temporary,
78
+ env,
79
+ scratchParent: temporary
80
+ });
81
+ }
82
+ const result = await run(isolated?.argv ?? expanded, {
83
+ cwd: context?.cwd ?? temporary,
84
+ env: isolated?.env ?? env,
48
85
  timeoutMs: 3e4
49
86
  });
50
87
  return {
@@ -56,7 +93,195 @@ async function probe(argv, config) {
56
93
  available: false,
57
94
  detail: toError(error).message
58
95
  };
96
+ } finally {
97
+ isolated?.cleanup();
98
+ if (temporary) removeConfinedPath(temporary, temporary);
99
+ }
100
+ }
101
+ const frameworks = FrameworkSchema.options;
102
+ const packageTargets = PackageTargetSchema.options;
103
+ function initialConfig(framework, platform, target) {
104
+ if (framework === "mygo" !== (platform === "macos")) throw new Error(`${framework} does not support ${platform}`);
105
+ if (framework === "android-java" && platform !== "android" || framework === "swift" && platform !== "ios") throw new Error(`${framework} does not support ${platform}`);
106
+ if (!(platform === "macos" ? ["app", "dmg"] : platform === "ios" ? ["simulator", "ipa"] : ["apk", "aab"]).includes(target)) throw new Error(`${target} does not match platform ${platform}`);
107
+ const template = JSON.parse(readFileSync(join(packageRoot, "examples", `${framework}.factory.json`), "utf8"));
108
+ template.packageTarget = target;
109
+ if (framework === "mygo") template.packaging = {
110
+ provider: "local",
111
+ commands: [[
112
+ "go",
113
+ "tool",
114
+ "mygo",
115
+ "build",
116
+ "-platform",
117
+ "darwin/arm64",
118
+ "-sign",
119
+ "-",
120
+ "-skip-notarize",
121
+ ...target === "app" ? ["-skip-dmg"] : [],
122
+ "-o",
123
+ "build"
124
+ ]],
125
+ artifactPath: target === "app" ? "build/darwin-arm64/MyGo App.app" : "build/darwin-arm64/MyGo App 0.1.0.dmg"
126
+ };
127
+ else if (framework === "flutter") {
128
+ template.executionEnv = platform === "ios" ? ["IOS_DEVICE_ID"] : ["ANDROID_DEVICE_ID"];
129
+ template.verification = {
130
+ prerequisites: platform === "ios" ? [["xcodebuild", "-version"]] : [["java", "-version"], ["adb", "version"]],
131
+ checks: [[
132
+ "flutter",
133
+ "analyze",
134
+ "--fatal-infos"
135
+ ], ["flutter", "test"]],
136
+ runtime: [[
137
+ "flutter",
138
+ "drive",
139
+ "--driver",
140
+ "test_driver/integration_test.dart",
141
+ "--target",
142
+ "integration_test/app_test.dart",
143
+ "-d",
144
+ platform === "ios" ? "${IOS_DEVICE_ID}" : "${ANDROID_DEVICE_ID}",
145
+ "--no-pub"
146
+ ]]
147
+ };
148
+ } else if (framework === "expo") {
149
+ const device = platform === "ios" ? "IOS_DEVICE_ID" : "ANDROID_DEVICE_ID";
150
+ const profile = target === "simulator" ? "simulator" : target === "apk" ? "apk" : "production";
151
+ const output = target === "simulator" ? "build/simulator.tar.gz" : `build/app.${target}`;
152
+ template.executionEnv = [device, ...platform === "android" ? ["ANDROID_HOME"] : []];
153
+ template.verification = {
154
+ prerequisites: [["node", "--version"], ["eas", "--version"]],
155
+ checks: [[
156
+ "npm",
157
+ "run",
158
+ "lint"
159
+ ], [
160
+ "npm",
161
+ "test",
162
+ "--",
163
+ "--runInBand"
164
+ ]],
165
+ runtime: [[
166
+ "npm",
167
+ "run",
168
+ "test:e2e",
169
+ "--",
170
+ "--platform",
171
+ platform,
172
+ "--device",
173
+ "${" + device + "}"
174
+ ]]
175
+ };
176
+ template.packaging = {
177
+ provider: "local",
178
+ commands: [[
179
+ "eas",
180
+ "build",
181
+ "--platform",
182
+ platform,
183
+ "--profile",
184
+ profile,
185
+ "--local",
186
+ "--non-interactive",
187
+ "--output",
188
+ output
189
+ ], ...target === "simulator" ? [[
190
+ "tar",
191
+ "-xzf",
192
+ output,
193
+ "-C",
194
+ "build"
195
+ ]] : []],
196
+ artifactPath: target === "simulator" ? "build/App.app" : output
197
+ };
198
+ } else if (platform === "ios") {
199
+ const base = [
200
+ "xcodebuild",
201
+ ...framework === "swift" ? ["-project", "App.xcodeproj"] : ["-workspace", `${framework === "swift" ? "." : framework === "uni-app" ? "native/ios" : "ios"}/App.xcworkspace`],
202
+ "-scheme",
203
+ "App"
204
+ ];
205
+ template.executionEnv = ["IOS_DEVICE_ID"];
206
+ template.verification = {
207
+ prerequisites: [["xcodebuild", "-version"], ...framework === "swift" ? [] : [["node", "--version"], ["npm", "--version"]]],
208
+ checks: framework === "swift" ? [[
209
+ ...base,
210
+ "build-for-testing",
211
+ "-destination",
212
+ "generic/platform=iOS Simulator",
213
+ "-derivedDataPath",
214
+ "build/DerivedData"
215
+ ]] : [[
216
+ "npm",
217
+ "run",
218
+ "lint"
219
+ ], [
220
+ "npm",
221
+ "test",
222
+ "--",
223
+ "--runInBand"
224
+ ]],
225
+ runtime: framework === "swift" ? [[
226
+ ...base,
227
+ "test",
228
+ "-destination",
229
+ "id=${IOS_DEVICE_ID}",
230
+ "-derivedDataPath",
231
+ "build/DerivedData"
232
+ ]] : [[
233
+ "npm",
234
+ "run",
235
+ "test:e2e",
236
+ "--",
237
+ "--platform",
238
+ "ios",
239
+ "--device",
240
+ "${IOS_DEVICE_ID}"
241
+ ]]
242
+ };
243
+ template.packaging = target === "simulator" ? {
244
+ commands: [[
245
+ ...base,
246
+ "build",
247
+ "-configuration",
248
+ "Debug",
249
+ "-destination",
250
+ "generic/platform=iOS Simulator",
251
+ "-derivedDataPath",
252
+ "build/DerivedData"
253
+ ]],
254
+ artifactPath: "build/DerivedData/Build/Products/Debug-iphonesimulator/App.app",
255
+ provider: "local"
256
+ } : {
257
+ commands: [[
258
+ ...base,
259
+ "archive",
260
+ "-configuration",
261
+ "Release",
262
+ "-destination",
263
+ "generic/platform=iOS",
264
+ "-archivePath",
265
+ "build/App.xcarchive"
266
+ ], [
267
+ "xcodebuild",
268
+ "-exportArchive",
269
+ "-archivePath",
270
+ "build/App.xcarchive",
271
+ "-exportPath",
272
+ "build/export",
273
+ "-exportOptionsPlist",
274
+ "ExportOptions.plist"
275
+ ]],
276
+ artifactPath: "build/export/App.ipa",
277
+ provider: "local"
278
+ };
279
+ } else if (target === "apk") {
280
+ const packaging = template.packaging;
281
+ packaging.commands = packaging.commands.map((argv) => argv.map((arg) => arg === ":app:bundleRelease" ? ":app:assembleRelease" : arg));
282
+ packaging.artifactPath = packaging.artifactPath.replace("bundle/release/app-release.aab", "apk/release/app-release.apk");
59
283
  }
284
+ return template;
60
285
  }
61
286
  const help = `apphx <command> [--config factory.json] [--build-only]
62
287
  hosts List supported agent hosts and activation commands
@@ -66,28 +291,33 @@ const help = `apphx <command> [--config factory.json] [--build-only]
66
291
  --project <directory> Select the project (default: current directory)
67
292
  --global Use the host's user-level skill directory instead
68
293
  doctor --host <host> Check installed workflow files; does not run the agent
69
- init [--allow-local-execution] Create factory.json without overwriting it
70
- doctor Check local build tools and execution permissions
294
+ init [--allow-local-execution] Create owner-editable factory.json; default Flutter/iOS
295
+ --framework <stack> flutter|android-java|swift|react-native|expo|uni-app|mygo
296
+ --platform <platform> ios|android|macos (mygo defaults macos; android-java android)
297
+ --package-target <target> simulator|ipa|apk|aab|app|dmg (MyGo app; Expo iOS ipa)
298
+ doctor [spec.json] Check configured prerequisites and optional stack tools
71
299
  enqueue <spec.json|-> Persist requirements and return a task ID
72
300
  next <id> Resume a host turn or show the next deterministic action
73
301
  complete <id> <turn> [json|-] Record research/review, or finish the host's build turn
74
302
  abandon <id> <turn> Abandon a host turn without resetting its budget
75
- step <id> Execute one verification, packaging or Apple stage
303
+ step <id> Execute one obfuscation, verification or packaging stage
76
304
  status [id] Read durable task state as JSON
77
305
  events <id> Read task transitions and evidence
78
- retry <id> Resume stopped work; preserve budgets and permissions
306
+ retry <id> Resume stopped work; preserve budgets and proofs
79
307
  submit <spec.json|-> Write requirements to the configured inbox
80
308
  scan [--observe] Validate or ingest inbox files; never start an agent
81
309
  inbox Read durable intake results
82
310
  reconcile-intake <id> <json|-> Restore a legacy request identity from original input
83
- authorize <id> --submit [--release] Explicitly grant publication permission
84
- feedback <id> <file> Resume using actual App Review rejection feedback
85
311
  remote <command> Call an installed Mac CLI over SSH (build-only)
312
+ obfuscator <project> Run the bundled deterministic obfuscator on a project copy
313
+ --config <file> Obfuscator options as JSON (required)
314
+ --framework <stack> flutter|android-java|swift|react-native|expo|uni-app|mygo
315
+ --seed <text> Determinism seed
86
316
  --version Print the installed package version
87
317
 
88
318
  Your agent supplies the model, session, tools and native delegation.
89
319
  No coding-agent subprocess or autonomous daemon is started by this CLI.
90
- Only verified local artifacts reach built; uploads require separate authorization.`;
320
+ Only verified local artifacts reach built. Store submission and review are manual human work outside this tool.`;
91
321
  async function main() {
92
322
  const { values, positionals, tokens } = parseArgs({
93
323
  allowPositionals: true,
@@ -105,9 +335,11 @@ async function main() {
105
335
  global: { type: "boolean" },
106
336
  host: { type: "string" },
107
337
  "allow-local-execution": { type: "boolean" },
338
+ framework: { type: "string" },
339
+ seed: { type: "string" },
340
+ platform: { type: "string" },
341
+ "package-target": { type: "string" },
108
342
  observe: { type: "boolean" },
109
- submit: { type: "boolean" },
110
- release: { type: "boolean" },
111
343
  version: {
112
344
  type: "boolean",
113
345
  short: "v"
@@ -133,7 +365,13 @@ async function main() {
133
365
  install: ["project", "global"],
134
366
  update: ["project", "global"],
135
367
  uninstall: ["project", "global"],
136
- init: ["config", "allow-local-execution"],
368
+ init: [
369
+ "config",
370
+ "allow-local-execution",
371
+ "framework",
372
+ "platform",
373
+ "package-target"
374
+ ],
137
375
  doctor: [
138
376
  "config",
139
377
  "build-only",
@@ -157,14 +395,12 @@ async function main() {
157
395
  ],
158
396
  inbox: ["config", "build-only"],
159
397
  "reconcile-intake": ["config", "build-only"],
160
- authorize: [
398
+ remote: ["config", "observe"],
399
+ obfuscator: [
161
400
  "config",
162
- "build-only",
163
- "submit",
164
- "release"
165
- ],
166
- feedback: ["config", "build-only"],
167
- remote: ["config", "observe"]
401
+ "framework",
402
+ "seed"
403
+ ]
168
404
  };
169
405
  if (!(command in flags)) throw new Error(`Unknown command ${command}; run apphx --help`);
170
406
  for (const token of tokens) if (token.kind === "option" && !flags[command].includes(token.name)) throw new Error(`${command} does not accept --${token.name}`);
@@ -175,6 +411,23 @@ async function main() {
175
411
  project: values.project,
176
412
  global: values.global
177
413
  };
414
+ if (command === "obfuscator") {
415
+ arity(args, 1, 1, "obfuscator <project> --config <file> [--framework <stack>] [--seed <text>]");
416
+ const bundled = join(packageRoot, "dist", "vendor", "obfuscator", "dist", "cli.js");
417
+ if (!existsSync(bundled)) throw new Error(`Bundled obfuscator is missing at ${bundled}; reinstall the apphx package`);
418
+ const forward = [
419
+ bundled,
420
+ "--project",
421
+ resolve(args[0])
422
+ ];
423
+ if (tokens.some((token) => token.kind === "option" && token.name === "config")) forward.push("--config", resolve(values.config));
424
+ if (values.framework !== void 0) forward.push("--framework", values.framework);
425
+ if (values.seed !== void 0) forward.push("--seed", values.seed);
426
+ const result = spawnSync(process.execPath, forward, { stdio: "inherit" });
427
+ if (result.error) throw result.error;
428
+ process.exitCode = result.status ?? 1;
429
+ return;
430
+ }
178
431
  if (command === "hosts") {
179
432
  arity(args, 0, 0, "hosts");
180
433
  print(hosts);
@@ -194,8 +447,13 @@ async function main() {
194
447
  }
195
448
  if (command === "doctor" && (values.project || values.global)) throw new Error("--project and --global require doctor --host <host>");
196
449
  if (command === "init") {
197
- arity(args, 0, 0, "init [--config <file>] [--allow-local-execution]");
198
- const template = JSON.parse(readFileSync(join(packageRoot, "examples", "factory.json"), "utf8"));
450
+ arity(args, 0, 0, "init [--framework <stack>] [--platform ios|android|macos] [--package-target simulator|ipa|apk|aab|app|dmg] [--config <file>] [--allow-local-execution]");
451
+ const framework = values.framework ?? "flutter";
452
+ if (!frameworks.includes(framework)) throw new Error(`Unknown framework ${framework}; choose ${frameworks.join(", ")}`);
453
+ const platform = PlatformSchema.parse(values.platform ?? (framework === "mygo" ? "macos" : framework === "android-java" ? "android" : "ios"));
454
+ const target = values["package-target"] ?? (platform === "macos" ? "app" : platform === "ios" ? framework === "expo" ? "ipa" : "simulator" : "apk");
455
+ if (!packageTargets.includes(target)) throw new Error(`Unknown package target ${target}; choose ${packageTargets.join(", ")}`);
456
+ const template = initialConfig(framework, platform, target);
199
457
  if (values["allow-local-execution"]) template.allowLocalExecution = true;
200
458
  mkdirSync(dirname(resolve(configPath)), { recursive: true });
201
459
  writeFileSync(configPath, `${JSON.stringify(template, null, 2)}\n`, {
@@ -204,8 +462,11 @@ async function main() {
204
462
  });
205
463
  print({
206
464
  config: resolve(configPath),
465
+ framework,
466
+ platform,
467
+ packageTarget: target,
207
468
  localExecution: template.allowLocalExecution,
208
- next: "Configure Flutter/Xcode and IOS_DEVICE_ID, then run apphx doctor --build-only"
469
+ next: framework === "mygo" ? "Pin MyGo v0.3.0 in go.mod (Go 1.27.1), match app name/version/arch and artifact path, and implement real desktop UI acceptance in cmd/acceptance. Native UI needs no Bun. Local ad-hoc builds only; init creates configuration, not source." : framework === "expo" ? "Configure your existing eas.json production (store IPA/AAB), apk (Android buildType apk), or simulator (ios.simulator true) profile, real device acceptance, and exact app product/output path; no prebuild/eject is requested. Run apphx doctor <spec.json> --build-only." : "Adapt module/scheme, real device acceptance, signing and artifact path to your project; then run apphx doctor <spec.json> --build-only. init creates configuration, not app source."
209
470
  });
210
471
  return;
211
472
  }
@@ -214,58 +475,106 @@ async function main() {
214
475
  return;
215
476
  }
216
477
  const config = loadConfig(configPath);
217
- if (buildOnly && (config.allowSubmit || config.allowRelease)) throw new Error("--build-only refuses configurations with upload or release enabled");
218
- if (buildOnly && ["authorize", "feedback"].includes(command)) throw new Error("Publication commands are forbidden in --build-only mode");
219
478
  if (command === "submit") {
220
479
  arity(args, 1, 1, "submit <spec.json|->");
221
480
  print(submitRequirement(config, inputJSON(args[0])));
222
481
  return;
223
482
  }
224
483
  if (command === "doctor") {
225
- arity(args, 0, 0, "doctor [--build-only]");
226
- const flutter = config.toolchain ? join(config.toolchain.flutterSdk, "bin", "flutter") : "flutter";
227
- const [flutterCheck, xcode] = await Promise.all([probe([flutter, "--version"], config), probe(["xcodebuild", "-version"], config)]);
484
+ arity(args, 0, 1, "doctor [spec.json] [--build-only]");
485
+ const spec = args[0] ? parseRequirement(inputJSON(args[0])).spec : void 0;
486
+ let prerequisiteContext;
487
+ if (spec) {
488
+ validateJobConfig(spec, config);
489
+ let source;
490
+ try {
491
+ source = validateProjectSource(spec, config);
492
+ } catch (error) {
493
+ if (spec.framework !== "mygo" || error.code !== "ENOENT") throw error;
494
+ }
495
+ const directory = spec.project?.directory ?? ".";
496
+ const project = source ? resolve(source, directory) : void 0;
497
+ if (source && project && (spec.framework !== "mygo" || existsSync(project))) {
498
+ assertProject(project, spec);
499
+ if (spec.framework === "mygo" || config.isolation) prerequisiteContext = {
500
+ cwd: project,
501
+ sourceRoot: source,
502
+ env: {
503
+ ...executionEnvironment(config),
504
+ ...spec.framework === "mygo" ? {
505
+ GOWORK: closestOwnedGoWorkspace(source, directory) ?? "off",
506
+ GOENV: "off",
507
+ GOTOOLCHAIN: "local",
508
+ GOFLAGS: "-mod=readonly"
509
+ } : {}
510
+ }
511
+ };
512
+ }
513
+ }
514
+ const commands = spec && config.packaging?.provider !== "cloud" ? doctorCommands(spec.framework, spec.platform) : [];
515
+ const tools = await Promise.all(commands.map(async ({ name, argv }) => ({
516
+ name,
517
+ ...await probe(argv[0] === "flutter" && config.toolchain ? [join(config.toolchain.flutterSdk, "bin", "flutter"), ...argv.slice(1)] : argv, config)
518
+ })));
519
+ const prerequisites = await Promise.all(config.verification.prerequisites.map(async (argv, index) => ({
520
+ name: `configured-prerequisite-${index + 1}`,
521
+ ...!config.allowLocalExecution ? {
522
+ available: false,
523
+ detail: "Owner must enable allowLocalExecution before configured prerequisite commands run"
524
+ } : spec?.framework === "mygo" && !prerequisiteContext ? {
525
+ available: false,
526
+ detail: "Project-scoped prerequisite cannot be checked yet: select an existing MyGo source app with project.sourceDir and project.directory; doctor does not create source or use the control module"
527
+ } : await probe(argv, config, prerequisiteContext)
528
+ })));
228
529
  const environment = executionEnvironment(config);
229
- const neededEnv = [...new Set([
530
+ const configuredCommands = [
230
531
  ...config.verification.prerequisites,
231
532
  ...config.verification.checks,
232
- ...config.verification.runtime
233
- ].flat().flatMap((arg) => [...arg.matchAll(/\$\{([A-Z][A-Z0-9_]*)\}/g)].map((match) => match[1])))];
533
+ ...config.verification.runtime,
534
+ ...config.packaging?.commands ?? []
535
+ ];
536
+ const neededEnv = [...new Set(configuredCommands.flat().flatMap((arg) => [...arg.matchAll(/\$\{([A-Z][A-Z0-9_]*)\}/g)].map((match) => match[1])))];
234
537
  const checks = [
235
538
  {
236
539
  name: "local-execution-permission",
237
540
  available: config.allowLocalExecution,
238
541
  detail: config.executionWarning ?? "Trusted local commands permitted; host tools remain under the host permission policy"
239
542
  },
240
- {
241
- name: "flutter",
242
- ...flutterCheck
243
- },
244
- {
245
- name: "xcode",
246
- ...xcode
247
- },
543
+ ...config.isolation ? [{
544
+ name: "subprocess-isolation",
545
+ ...await probe(["/usr/bin/true"], config),
546
+ mode: config.isolation.mode,
547
+ network: config.isolation.network
548
+ }] : [],
549
+ ...tools,
550
+ ...prerequisites,
248
551
  ...neededEnv.map((name) => ({
249
552
  name,
250
553
  available: Boolean(environment[name]),
251
554
  detail: environment[name] ? "Available in the configured execution environment; value not printed" : `Set ${name} and include its name in executionEnv`
252
555
  }))
253
556
  ];
254
- if (config.allowSubmit) checks.push(...await appleDoctor());
255
557
  print({
558
+ framework: spec?.framework ?? null,
559
+ platform: spec?.platform ?? null,
256
560
  checks,
257
- submissionEnabled: config.allowSubmit,
561
+ note: spec ? "Tool availability is not runtime acceptance; configured checks run in the task workspace during verify" : "No stack assumed; pass spec.json for profile-specific probes and compatibility checks",
258
562
  model: "Provided by your agent host; no separate model authentication is configured here"
259
563
  });
260
564
  if (checks.some((check) => !check.available)) process.exitCode = 2;
261
565
  return;
262
566
  }
567
+ const queuedRequirement = command === "enqueue" ? (() => {
568
+ arity(args, 1, 1, "enqueue <spec.json|->");
569
+ const requirement = parseRequirement(inputJSON(args[0]));
570
+ validateJobConfig(requirement.spec, config);
571
+ validateProjectSource(requirement.spec, config);
572
+ return requirement;
573
+ })() : void 0;
263
574
  const store = new Store(config.stateDir);
264
575
  try {
265
576
  if (command === "enqueue") {
266
- arity(args, 1, 1, "enqueue <spec.json|->");
267
- const { spec, baseJobId } = parseRequirement(inputJSON(args[0]));
268
- if (config.allowSubmit) validateAppleSpec(spec);
577
+ const { spec, baseJobId } = queuedRequirement;
269
578
  print(view(store.enqueue(spec, config, baseJobId)));
270
579
  } else if (command === "scan") {
271
580
  arity(args, 0, 0, "scan [--observe]");
@@ -314,28 +623,26 @@ async function main() {
314
623
  } else if (command === "retry") {
315
624
  arity(args, 1, 1, "retry <id>");
316
625
  print(view(store.transition(args[0], (job) => {
317
- if (buildOnly && (job.config.allowSubmit || job.config.allowRelease)) throw new Error("--build-only refuses a task with snapshotted publication permission");
318
626
  if (![
319
627
  "blocked",
320
628
  "ready",
321
629
  "built"
322
630
  ].includes(job.status)) throw new Error(`Cannot retry ${job.status}; budgets and active work cannot be reset by retry`);
323
631
  if (job.turn) throw new Error("Abandon the active host turn before changing task configuration");
324
- const proofChanged = JSON.stringify(config.verification) !== JSON.stringify(job.config.verification) || JSON.stringify(config.executionEnv) !== JSON.stringify(job.config.executionEnv) || JSON.stringify(config.toolchain) !== JSON.stringify(job.config.toolchain) || job.verifiedDigest != null && !job.verifiedDependencyDigest;
325
- if (proofChanged && Object.keys(job.receipt).length) throw new Error("Verification proof is no longer valid after publication began; reconcile the existing upload before creating a new version");
326
- const patch = proofChanged && job.verifiedDigest ? {
327
- stage: "verify",
328
- verifiedDigest: null,
329
- verifiedDependencyDigest: null,
330
- review: null,
331
- artifact: null
332
- } : {};
632
+ if (job.cloudBuildPending) throw new Error("Cloud packaging outcome is uncertain; reconcile the existing provider run rather than replaying it with retry");
333
633
  const resumedConfig = {
334
634
  ...config,
335
- packageTarget: job.config.allowSubmit ? "ipa" : config.packageTarget,
336
- allowSubmit: job.config.allowSubmit,
337
- allowRelease: job.config.allowRelease
635
+ packageTarget: config.packageTarget
338
636
  };
637
+ const patch = verificationConfigChanged(job, resumedConfig) ? {
638
+ ...invalidateVerification(),
639
+ stage: [
640
+ "verify",
641
+ "review",
642
+ "package"
643
+ ].includes(job.stage) ? "verify" : job.stage
644
+ } : {};
645
+ validateJobConfig(job.spec, resumedConfig);
339
646
  return {
340
647
  ...patch,
341
648
  config: resumedConfig,
@@ -345,62 +652,7 @@ async function main() {
345
652
  error: null
346
653
  };
347
654
  })));
348
- } else if (command === "authorize") {
349
- arity(args, 1, 1, "authorize <id> --submit [--release]");
350
- if (!values.submit) throw new Error("authorize requires --submit");
351
- print(view(store.transition(args[0], (job) => {
352
- if (job.turn || ![
353
- "ready",
354
- "blocked",
355
- "built"
356
- ].includes(job.status) || Object.keys(job.receipt).length || job.stage === "monitor") throw new Error("Publication permission can change only on stopped work before any remote intent");
357
- const publishing = validateAppleSpec(job.spec);
358
- if (values.release && !/^[A-Za-z]{2}$/.test(publishing.storefrontCountry ?? "")) throw new Error("--release requires apple.storefrontCountry in the immutable product specification");
359
- return {
360
- config: {
361
- ...job.config,
362
- packageTarget: "ipa",
363
- allowSubmit: true,
364
- allowRelease: Boolean(values.release)
365
- },
366
- status: ["ready", "built"].includes(job.status) ? "queued" : "blocked",
367
- nextAt: Date.now()
368
- };
369
- })));
370
- } else if (command === "feedback") {
371
- arity(args, 2, 2, "feedback <id> <file>");
372
- const feedback = readInputFile(args[1], "App Review feedback").toString("utf8").trim();
373
- if (!feedback) throw new Error("App Review feedback file is empty");
374
- print(view(store.transition(args[0], (job) => {
375
- if (job.turn || job.stage !== "monitor" || job.appleReview?.status !== "rejected" || job.status !== "blocked") throw new Error("feedback is only allowed for an observed App Review rejection");
376
- const spec = structuredClone(job.spec);
377
- if (!spec.apple?.buildNumber || !/^\d+$/.test(spec.apple.buildNumber)) throw new Error("A numeric apple.buildNumber is required to prepare a replacement build");
378
- spec.apple.buildNumber = String(BigInt(spec.apple.buildNumber) + 1n);
379
- return {
380
- spec,
381
- config: {
382
- ...config,
383
- packageTarget: job.config.allowSubmit ? "ipa" : config.packageTarget,
384
- allowSubmit: job.config.allowSubmit,
385
- allowRelease: job.config.allowRelease
386
- },
387
- feedback,
388
- status: "queued",
389
- stage: "build",
390
- createdAt: Date.now(),
391
- attempts: {},
392
- hostTurns: 0,
393
- nextAt: Date.now(),
394
- receipt: {},
395
- ipaPath: null,
396
- archiveDigest: null,
397
- verifiedDigest: null,
398
- verifiedDependencyDigest: null,
399
- review: null,
400
- error: null
401
- };
402
- })));
403
- }
655
+ } else throw new Error(`Unknown command ${command}; use --help`);
404
656
  } finally {
405
657
  store.close();
406
658
  }