@swmansion/argent 0.22.1-next.6 → 0.22.1-next.8

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/dist/cli-cmds.mjs CHANGED
@@ -3274,8 +3274,8 @@ function describeExpectedValue(def) {
3274
3274
  var CONFIG_SCHEMA = [
3275
3275
  {
3276
3276
  key: "telemetry.enabled",
3277
- description: "Whether anonymous opt-out telemetry is enabled (on by default; environment opt-outs like DO_NOT_TRACK are not reflected here \u2014 `argent telemetry status` shows effective consent).",
3278
- scopes: ["global"],
3277
+ description: "Whether anonymous opt-out telemetry is enabled (on by default; environment opt-outs like DO_NOT_TRACK are not reflected here \u2014 `argent telemetry status` shows effective consent). `false` in either scope wins, so a committed project opt-out holds for every teammate.",
3278
+ scopes: ["project", "global"],
3279
3279
  parse: asBoolean,
3280
3280
  merge: "prioritize-restrictive",
3281
3281
  // Opt-out: consent.ts reads an unstored value as enabled, so the config
@@ -7210,7 +7210,7 @@ var _CI_VENDOR_COUNT_FOR_TEST = vendors_default.length;
7210
7210
  var SESSION_ID2 = randomUUID5();
7211
7211
  function readCliVersion() {
7212
7212
  if (true) {
7213
- return "0.22.1-next.6";
7213
+ return "0.22.1-next.8";
7214
7214
  }
7215
7215
  return "0.0.0";
7216
7216
  }
@@ -7572,32 +7572,30 @@ function resolveHostFingerprintAsync() {
7572
7572
 
7573
7573
  // ../telemetry/src/consent.ts
7574
7574
  import * as fs9 from "node:fs";
7575
- var cache = { current: null };
7575
+ var cache = /* @__PURE__ */ new Map();
7576
7576
  var sessionOverride = null;
7577
- function readConfigOverride() {
7577
+ function readConfigOverrideAt(filePath) {
7578
+ const miss = { mtimeMs: null, fingerprint: null, enabledOverride: null };
7578
7579
  let stats;
7579
7580
  try {
7580
- stats = fs9.lstatSync(configFilePath());
7581
- } catch (err) {
7582
- if (err.code === "ENOENT") {
7583
- cache.current = { mtimeMs: null, fingerprint: null, enabledOverride: null };
7584
- return null;
7585
- }
7586
- cache.current = { mtimeMs: null, fingerprint: null, enabledOverride: null };
7581
+ stats = fs9.lstatSync(filePath);
7582
+ } catch {
7583
+ cache.set(filePath, miss);
7587
7584
  return null;
7588
7585
  }
7589
7586
  if (!stats.isFile()) {
7590
- cache.current = { mtimeMs: null, fingerprint: null, enabledOverride: null };
7587
+ cache.set(filePath, miss);
7591
7588
  return null;
7592
7589
  }
7593
7590
  const fingerprint = `${stats.dev}:${stats.ino}:${stats.size}`;
7594
7591
  const mtimeMs = stats.mtimeMs;
7595
- if (cache.current && cache.current.fingerprint === fingerprint && cache.current.mtimeMs === mtimeMs) {
7596
- return cache.current.enabledOverride;
7592
+ const cached2 = cache.get(filePath);
7593
+ if (cached2 && cached2.fingerprint === fingerprint && cached2.mtimeMs === mtimeMs) {
7594
+ return cached2.enabledOverride;
7597
7595
  }
7598
7596
  let parsedEnabled = null;
7599
7597
  try {
7600
- const raw = fs9.readFileSync(configFilePath(), "utf8");
7598
+ const raw = fs9.readFileSync(filePath, "utf8");
7601
7599
  const json = JSON.parse(raw);
7602
7600
  if (json && typeof json === "object") {
7603
7601
  const t2 = json.telemetry;
@@ -7608,9 +7606,27 @@ function readConfigOverride() {
7608
7606
  }
7609
7607
  } catch {
7610
7608
  }
7611
- cache.current = { mtimeMs, fingerprint, enabledOverride: parsedEnabled };
7609
+ cache.set(filePath, { mtimeMs, fingerprint, enabledOverride: parsedEnabled });
7612
7610
  return parsedEnabled;
7613
7611
  }
7612
+ function readPersistedConsent(cwd) {
7613
+ const globalPath = configFilePath("global");
7614
+ const projectPath = configFilePath("project", { cwd });
7615
+ const global2 = readConfigOverrideAt(globalPath);
7616
+ const project = projectPath === globalPath ? null : readConfigOverrideAt(projectPath);
7617
+ if (project === false && global2 === false) {
7618
+ return { enabled: false, detail: "config.json (project and global)" };
7619
+ }
7620
+ if (project === false) return { enabled: false, detail: "config.json (project)" };
7621
+ if (global2 === false) return { enabled: false, detail: "config.json (global)" };
7622
+ if (project === true || global2 === true) {
7623
+ return {
7624
+ enabled: true,
7625
+ detail: project === true ? "config.json (project)" : "config.json (global)"
7626
+ };
7627
+ }
7628
+ return null;
7629
+ }
7614
7630
  function parseFalsy(value) {
7615
7631
  if (value === void 0) return false;
7616
7632
  const v = value.trim().toLowerCase();
@@ -7620,7 +7636,7 @@ function isDoNotTrackSet(value) {
7620
7636
  if (value === void 0 || value.trim() === "") return false;
7621
7637
  return !parseFalsy(value);
7622
7638
  }
7623
- function getConsentState(env = process.env) {
7639
+ function getConsentState(env = process.env, cwd = process.cwd()) {
7624
7640
  if (isDoNotTrackSet(env.DO_NOT_TRACK)) {
7625
7641
  return {
7626
7642
  enabled: false,
@@ -7637,24 +7653,28 @@ function getConsentState(env = process.env) {
7637
7653
  if (sessionOverride !== null) {
7638
7654
  return { enabled: sessionOverride, source: { source: "session_override" } };
7639
7655
  }
7640
- const persisted = readConfigOverride();
7641
- if (persisted === false) {
7642
- return { enabled: false, source: { source: "config_file", detail: "config.json" } };
7643
- }
7644
- if (persisted === true) {
7645
- return { enabled: true, source: { source: "config_file", detail: "config.json" } };
7656
+ const persisted = readPersistedConsent(cwd);
7657
+ if (persisted !== null) {
7658
+ return {
7659
+ enabled: persisted.enabled,
7660
+ source: { source: "config_file", detail: persisted.detail }
7661
+ };
7646
7662
  }
7647
7663
  return { enabled: true, source: { source: "default" } };
7648
7664
  }
7649
- function isEnabled(env = process.env) {
7650
- return getConsentState(env).enabled;
7665
+ function isEnabled(env = process.env, cwd) {
7666
+ return getConsentState(env, cwd).enabled;
7651
7667
  }
7652
- function writeConsentFlag(enabled) {
7653
- updateConfig((config2) => {
7654
- const telemetryBlock = typeof config2.telemetry === "object" && config2.telemetry ? config2.telemetry : {};
7655
- config2.telemetry = { ...telemetryBlock, enabled };
7656
- });
7657
- cache.current = null;
7668
+ function writeConsentFlag(enabled, scope = "global", options = {}) {
7669
+ updateConfig(
7670
+ (config2) => {
7671
+ const telemetryBlock = typeof config2.telemetry === "object" && config2.telemetry ? config2.telemetry : {};
7672
+ config2.telemetry = { ...telemetryBlock, enabled };
7673
+ },
7674
+ scope,
7675
+ options
7676
+ );
7677
+ cache.clear();
7658
7678
  }
7659
7679
 
7660
7680
  // ../telemetry/src/notice.ts
@@ -7750,13 +7770,13 @@ async function shutdown(timeoutMs = SHORT_FLUSH_TIMEOUT_MS) {
7750
7770
  state = null;
7751
7771
  }
7752
7772
  }
7753
- function markEnabled() {
7754
- writeConsentFlag(true);
7773
+ function markEnabled(scope = "global") {
7774
+ writeConsentFlag(true, scope);
7755
7775
  }
7756
- async function markDisabled() {
7776
+ async function markDisabled(scope = "global") {
7757
7777
  try {
7758
7778
  const client2 = getConstructedClient();
7759
- writeConsentFlag(false);
7779
+ writeConsentFlag(false, scope);
7760
7780
  if (client2) {
7761
7781
  try {
7762
7782
  await raceDrain(client2, SHORT_FLUSH_TIMEOUT_MS);
@@ -10803,8 +10823,8 @@ Options:
10803
10823
  console.log("Feature flags (project overrides global):");
10804
10824
  const maxName = registryView.reduce((m, f) => Math.max(m, f.name.length), 0);
10805
10825
  for (const f of registryView) {
10806
- const scopeLabel2 = f.scope ? ` (${f.scope})` : "";
10807
- console.log(` ${f.name.padEnd(maxName, " ")} ${colorState(f.enabled)}${scopeLabel2}`);
10826
+ const scopeLabel3 = f.scope ? ` (${f.scope})` : "";
10827
+ console.log(` ${f.name.padEnd(maxName, " ")} ${colorState(f.enabled)}${scopeLabel3}`);
10808
10828
  console.log(` ${" ".repeat(maxName)} ${f.description}`);
10809
10829
  }
10810
10830
  }
@@ -12804,8 +12824,80 @@ async function unlink3(argv) {
12804
12824
 
12805
12825
  // ../argent-cli/src/telemetry.ts
12806
12826
  var import_picocolors5 = __toESM(require_picocolors(), 1);
12827
+
12828
+ // ../argent-cli/src/command-args.ts
12829
+ var UsageError = class extends Error {
12830
+ constructor(message) {
12831
+ super(message);
12832
+ this.name = "UsageError";
12833
+ }
12834
+ };
12835
+ function parseCommandArgs(argv, specs) {
12836
+ const positionals = [];
12837
+ const options = {};
12838
+ for (let i2 = 0; i2 < argv.length; i2++) {
12839
+ const tok = argv[i2];
12840
+ if (tok === "--") {
12841
+ positionals.push(...argv.slice(i2 + 1));
12842
+ break;
12843
+ }
12844
+ if (!tok.startsWith("--")) {
12845
+ positionals.push(tok);
12846
+ continue;
12847
+ }
12848
+ const eq = tok.indexOf("=");
12849
+ const name = eq === -1 ? tok.slice(2) : tok.slice(2, eq);
12850
+ const inlineValue = eq === -1 ? void 0 : tok.slice(eq + 1);
12851
+ const spec = specs[name];
12852
+ if (!spec) throw new UsageError(`Unknown flag "${tok}".`);
12853
+ if (spec.kind === "boolean") {
12854
+ if (inlineValue !== void 0) throw new UsageError(`--${name} does not take a value.`);
12855
+ options[name] = true;
12856
+ continue;
12857
+ }
12858
+ let value = inlineValue;
12859
+ if (value === void 0) {
12860
+ const next = argv[i2 + 1];
12861
+ if (next !== void 0 && !next.startsWith("--")) {
12862
+ value = next;
12863
+ i2 += 1;
12864
+ }
12865
+ }
12866
+ if (value === void 0 || value === "") {
12867
+ throw new UsageError(
12868
+ `--${name} requires a value${spec.choices ? ` (${spec.choices.join("|")})` : ""}.`
12869
+ );
12870
+ }
12871
+ if (spec.choices && !spec.choices.includes(value)) {
12872
+ throw new UsageError(
12873
+ `--${name} must be one of ${spec.choices.map((c2) => `"${c2}"`).join(", ")} (got "${value}").`
12874
+ );
12875
+ }
12876
+ options[name] = value;
12877
+ }
12878
+ return { positionals, options };
12879
+ }
12880
+
12881
+ // ../argent-cli/src/telemetry.ts
12882
+ var SCOPES = ["global", "project"];
12883
+ var TELEMETRY_OPTIONS = {
12884
+ scope: { kind: "value", choices: SCOPES }
12885
+ };
12807
12886
  async function telemetry(args) {
12808
12887
  const sub = args[0];
12888
+ let scope = "global";
12889
+ try {
12890
+ const { positionals, options } = parseCommandArgs(args.slice(1), TELEMETRY_OPTIONS);
12891
+ if (positionals.length > 0) {
12892
+ throw new UsageError(`Unexpected argument "${positionals[0]}".`);
12893
+ }
12894
+ if (options.scope !== void 0) scope = options.scope;
12895
+ } catch (err) {
12896
+ if (!(err instanceof UsageError)) throw err;
12897
+ console.error(`Error: ${err.message}`);
12898
+ printUsage3();
12899
+ process.exit(2);
12900
+ }
12809
12901
  init("cli");
12810
12902
  switch (sub) {
12811
12903
  case void 0:
@@ -12817,10 +12909,10 @@ async function telemetry(args) {
12817
12909
  await shutdown();
12818
12910
  return;
12819
12911
  case "enable":
12820
- await cmdEnable();
12912
+ await cmdEnable(scope);
12821
12913
  return;
12822
12914
  case "disable":
12823
- await cmdDisable();
12915
+ await cmdDisable(scope);
12824
12916
  return;
12825
12917
  case "--help":
12826
12918
  case "-h":
@@ -12835,37 +12927,64 @@ async function telemetry(args) {
12835
12927
  }
12836
12928
  function printUsage3() {
12837
12929
  console.log(`Usage:
12838
- argent telemetry status Show telemetry state and device id
12839
- argent telemetry enable Enable telemetry
12840
- argent telemetry disable Disable telemetry
12930
+ argent telemetry status Show telemetry state and device id
12931
+ argent telemetry enable [--scope global|project] Enable telemetry
12932
+ argent telemetry disable [--scope global|project] Disable telemetry
12933
+
12934
+ The default scope is global (~/.argent/config.json). \`--scope project\` writes
12935
+ <project-root>/.argent/config.json instead \u2014 commit it and telemetry stays off
12936
+ for everyone who clones the repository. \`false\` in either scope wins.
12841
12937
  `);
12842
12938
  }
12939
+ function scopeLabel2(scope) {
12940
+ return scope === "project" ? "project scope" : "global scope";
12941
+ }
12843
12942
  function printStatus() {
12844
12943
  const s = status();
12845
12944
  const idLabel = s.anonIdPrefix ? `${s.anonIdPrefix}...` : s.hasAnonIdOnDisk ? "present" : "not created";
12846
12945
  console.log("telemetry:");
12847
12946
  console.log(` state: ${s.enabled ? "enabled" : "disabled"}`);
12947
+ console.log(` source: ${describeSource(s.source)}`);
12848
12948
  console.log(` device id: ${idLabel}`);
12849
12949
  }
12850
- async function cmdEnable() {
12950
+ function describeSource(source) {
12951
+ switch (source.source) {
12952
+ case "env_do_not_track":
12953
+ case "env_argent_telemetry":
12954
+ return `environment (${source.detail ?? source.source})`;
12955
+ case "config_file":
12956
+ return source.detail ?? "config.json";
12957
+ case "session_override":
12958
+ return "this session";
12959
+ case "default":
12960
+ return "default (no opt-out set)";
12961
+ }
12962
+ }
12963
+ async function cmdEnable(scope) {
12851
12964
  const wasEnabled = isEnabled();
12852
- markEnabled();
12853
- if (wasEnabled) {
12854
- console.log(import_picocolors5.default.dim("Telemetry was already enabled."));
12965
+ markEnabled(scope);
12966
+ const nowEnabled = isEnabled();
12967
+ if (!nowEnabled) {
12968
+ console.log(
12969
+ import_picocolors5.default.yellow(
12970
+ `Telemetry set to enabled at ${scopeLabel2(scope)}, but it stays disabled: ${describeSource(status().source)} still opts out.`
12971
+ )
12972
+ );
12973
+ } else if (wasEnabled) {
12974
+ console.log(import_picocolors5.default.dim(`Telemetry was already enabled (written at ${scopeLabel2(scope)}).`));
12855
12975
  } else {
12856
- console.log(import_picocolors5.default.green("Telemetry enabled."));
12976
+ console.log(import_picocolors5.default.green(`Telemetry enabled (${scopeLabel2(scope)}).`));
12857
12977
  }
12858
12978
  await shutdown();
12859
12979
  }
12860
- async function cmdDisable() {
12980
+ async function cmdDisable(scope) {
12861
12981
  const wasEnabled = isEnabled();
12982
+ await markDisabled(scope);
12862
12983
  if (!wasEnabled) {
12863
- console.log(import_picocolors5.default.dim("Telemetry was already disabled."));
12864
- await shutdown();
12865
- return;
12984
+ console.log(import_picocolors5.default.dim(`Telemetry was already disabled (written at ${scopeLabel2(scope)}).`));
12985
+ } else {
12986
+ console.log(import_picocolors5.default.red(`Telemetry disabled (${scopeLabel2(scope)}).`));
12866
12987
  }
12867
- await markDisabled();
12868
- console.log(import_picocolors5.default.red("Telemetry disabled."));
12869
12988
  await shutdown();
12870
12989
  }
12871
12990
  export {
@@ -16428,7 +16428,7 @@ var _CI_VENDOR_COUNT_FOR_TEST = vendors_default.length;
16428
16428
  var SESSION_ID = randomUUID4();
16429
16429
  function readCliVersion() {
16430
16430
  if (true) {
16431
- return "0.22.1-next.6";
16431
+ return "0.22.1-next.8";
16432
16432
  }
16433
16433
  return "0.0.0";
16434
16434
  }
@@ -16806,32 +16806,30 @@ function resolveHostFingerprintAsync() {
16806
16806
 
16807
16807
  // ../telemetry/src/consent.ts
16808
16808
  import * as fs8 from "node:fs";
16809
- var cache = { current: null };
16809
+ var cache = /* @__PURE__ */ new Map();
16810
16810
  var sessionOverride = null;
16811
- function readConfigOverride() {
16811
+ function readConfigOverrideAt(filePath) {
16812
+ const miss = { mtimeMs: null, fingerprint: null, enabledOverride: null };
16812
16813
  let stats;
16813
16814
  try {
16814
- stats = fs8.lstatSync(configFilePath());
16815
- } catch (err) {
16816
- if (err.code === "ENOENT") {
16817
- cache.current = { mtimeMs: null, fingerprint: null, enabledOverride: null };
16818
- return null;
16819
- }
16820
- cache.current = { mtimeMs: null, fingerprint: null, enabledOverride: null };
16815
+ stats = fs8.lstatSync(filePath);
16816
+ } catch {
16817
+ cache.set(filePath, miss);
16821
16818
  return null;
16822
16819
  }
16823
16820
  if (!stats.isFile()) {
16824
- cache.current = { mtimeMs: null, fingerprint: null, enabledOverride: null };
16821
+ cache.set(filePath, miss);
16825
16822
  return null;
16826
16823
  }
16827
16824
  const fingerprint = `${stats.dev}:${stats.ino}:${stats.size}`;
16828
16825
  const mtimeMs = stats.mtimeMs;
16829
- if (cache.current && cache.current.fingerprint === fingerprint && cache.current.mtimeMs === mtimeMs) {
16830
- return cache.current.enabledOverride;
16826
+ const cached2 = cache.get(filePath);
16827
+ if (cached2 && cached2.fingerprint === fingerprint && cached2.mtimeMs === mtimeMs) {
16828
+ return cached2.enabledOverride;
16831
16829
  }
16832
16830
  let parsedEnabled = null;
16833
16831
  try {
16834
- const raw = fs8.readFileSync(configFilePath(), "utf8");
16832
+ const raw = fs8.readFileSync(filePath, "utf8");
16835
16833
  const json = JSON.parse(raw);
16836
16834
  if (json && typeof json === "object") {
16837
16835
  const t2 = json.telemetry;
@@ -16842,9 +16840,27 @@ function readConfigOverride() {
16842
16840
  }
16843
16841
  } catch {
16844
16842
  }
16845
- cache.current = { mtimeMs, fingerprint, enabledOverride: parsedEnabled };
16843
+ cache.set(filePath, { mtimeMs, fingerprint, enabledOverride: parsedEnabled });
16846
16844
  return parsedEnabled;
16847
16845
  }
16846
+ function readPersistedConsent(cwd) {
16847
+ const globalPath = configFilePath("global");
16848
+ const projectPath = configFilePath("project", { cwd });
16849
+ const global2 = readConfigOverrideAt(globalPath);
16850
+ const project = projectPath === globalPath ? null : readConfigOverrideAt(projectPath);
16851
+ if (project === false && global2 === false) {
16852
+ return { enabled: false, detail: "config.json (project and global)" };
16853
+ }
16854
+ if (project === false) return { enabled: false, detail: "config.json (project)" };
16855
+ if (global2 === false) return { enabled: false, detail: "config.json (global)" };
16856
+ if (project === true || global2 === true) {
16857
+ return {
16858
+ enabled: true,
16859
+ detail: project === true ? "config.json (project)" : "config.json (global)"
16860
+ };
16861
+ }
16862
+ return null;
16863
+ }
16848
16864
  function parseFalsy(value) {
16849
16865
  if (value === void 0) return false;
16850
16866
  const v = value.trim().toLowerCase();
@@ -16854,7 +16870,7 @@ function isDoNotTrackSet(value) {
16854
16870
  if (value === void 0 || value.trim() === "") return false;
16855
16871
  return !parseFalsy(value);
16856
16872
  }
16857
- function getConsentState(env = process.env) {
16873
+ function getConsentState(env = process.env, cwd = process.cwd()) {
16858
16874
  if (isDoNotTrackSet(env.DO_NOT_TRACK)) {
16859
16875
  return {
16860
16876
  enabled: false,
@@ -16871,24 +16887,28 @@ function getConsentState(env = process.env) {
16871
16887
  if (sessionOverride !== null) {
16872
16888
  return { enabled: sessionOverride, source: { source: "session_override" } };
16873
16889
  }
16874
- const persisted = readConfigOverride();
16875
- if (persisted === false) {
16876
- return { enabled: false, source: { source: "config_file", detail: "config.json" } };
16877
- }
16878
- if (persisted === true) {
16879
- return { enabled: true, source: { source: "config_file", detail: "config.json" } };
16890
+ const persisted = readPersistedConsent(cwd);
16891
+ if (persisted !== null) {
16892
+ return {
16893
+ enabled: persisted.enabled,
16894
+ source: { source: "config_file", detail: persisted.detail }
16895
+ };
16880
16896
  }
16881
16897
  return { enabled: true, source: { source: "default" } };
16882
16898
  }
16883
- function isEnabled(env = process.env) {
16884
- return getConsentState(env).enabled;
16899
+ function isEnabled(env = process.env, cwd) {
16900
+ return getConsentState(env, cwd).enabled;
16885
16901
  }
16886
- function writeConsentFlag(enabled) {
16887
- updateConfig((config) => {
16888
- const telemetryBlock = typeof config.telemetry === "object" && config.telemetry ? config.telemetry : {};
16889
- config.telemetry = { ...telemetryBlock, enabled };
16890
- });
16891
- cache.current = null;
16902
+ function writeConsentFlag(enabled, scope = "global", options = {}) {
16903
+ updateConfig(
16904
+ (config) => {
16905
+ const telemetryBlock = typeof config.telemetry === "object" && config.telemetry ? config.telemetry : {};
16906
+ config.telemetry = { ...telemetryBlock, enabled };
16907
+ },
16908
+ scope,
16909
+ options
16910
+ );
16911
+ cache.clear();
16892
16912
  }
16893
16913
  function setSessionConsentOverride(enabled) {
16894
16914
  sessionOverride = enabled;
@@ -22455,6 +22475,18 @@ async function init2(args) {
22455
22475
  }
22456
22476
  tel.installMode = modeFromFlags ?? await promptInstallMode(recordedMode ?? "global");
22457
22477
  track("installation:install_mode_decision", { install_mode: tel.installMode });
22478
+ let wroteProjectTelemetryOptOut = false;
22479
+ if (parsed.noTelemetry && tel.installMode === "local") {
22480
+ try {
22481
+ writeConsentFlag(false, "project", { cwd: initProjectRoot });
22482
+ wroteProjectTelemetryOptOut = true;
22483
+ log.info(
22484
+ `${import_picocolors10.default.bold("Telemetry")} ${import_picocolors10.default.dim("also disabled for this project \u2014")} ${import_picocolors10.default.cyan(".argent/config.json")} ${import_picocolors10.default.dim("(commit it so the opt-out applies to every clone).")}`
22485
+ );
22486
+ } catch (err) {
22487
+ log.warn(`Could not write the project telemetry opt-out: ${err}`);
22488
+ }
22489
+ }
22458
22490
  version = await runInstall({
22459
22491
  installMode: tel.installMode,
22460
22492
  fromTar: parsed.fromTar,
@@ -22582,7 +22614,8 @@ async function init2(args) {
22582
22614
  scope,
22583
22615
  allowlistEnabled: allowlist.enabled,
22584
22616
  skillsMethod,
22585
- copiedRules: copyResults.length > 0
22617
+ copiedRules: copyResults.length > 0,
22618
+ wroteProjectTelemetryOptOut
22586
22619
  });
22587
22620
  note(
22588
22621
  [
@@ -22620,7 +22653,8 @@ function printSummary({
22620
22653
  scope,
22621
22654
  allowlistEnabled,
22622
22655
  skillsMethod,
22623
- copiedRules
22656
+ copiedRules,
22657
+ wroteProjectTelemetryOptOut
22624
22658
  }) {
22625
22659
  const summaryLines = [
22626
22660
  `${import_picocolors10.default.green("Install mode")} ${installMode === "local" ? "local (devDependency)" : "global"}`,
@@ -22638,7 +22672,7 @@ function printSummary({
22638
22672
  `${import_picocolors10.default.bold("Commit")} so your team shares the same setup:`,
22639
22673
  ` ${import_picocolors10.default.cyan("package.json")} + your lockfile`,
22640
22674
  ` the written MCP config (.mcp.json, .cursor/mcp.json, \u2026)`,
22641
- ` ${import_picocolors10.default.cyan(".argent/install.json")}, and the skills/rules/agents files`,
22675
+ ` ${import_picocolors10.default.cyan(".argent/install.json")}${wroteProjectTelemetryOptOut ? ` + ${import_picocolors10.default.cyan(".argent/config.json")}` : ""}, and the skills/rules/agents files`,
22642
22676
  "",
22643
22677
  `Teammates then get argent on ${import_picocolors10.default.cyan("npm install")} \u2014 no global install, no ${import_picocolors10.default.cyan("argent init")}.`,
22644
22678
  import_picocolors10.default.dim(
@@ -17142,8 +17142,8 @@ function asStringArray(raw) {
17142
17142
  var CONFIG_SCHEMA = [
17143
17143
  {
17144
17144
  key: "telemetry.enabled",
17145
- description: "Whether anonymous opt-out telemetry is enabled (on by default; environment opt-outs like DO_NOT_TRACK are not reflected here \u2014 `argent telemetry status` shows effective consent).",
17146
- scopes: ["global"],
17145
+ description: "Whether anonymous opt-out telemetry is enabled (on by default; environment opt-outs like DO_NOT_TRACK are not reflected here \u2014 `argent telemetry status` shows effective consent). `false` in either scope wins, so a committed project opt-out holds for every teammate.",
17146
+ scopes: ["project", "global"],
17147
17147
  parse: asBoolean,
17148
17148
  merge: "prioritize-restrictive",
17149
17149
  // Opt-out: consent.ts reads an unstored value as enabled, so the config
@@ -18177,32 +18177,30 @@ var DYLIB_TVOS_DIR = path11.join(DYLIB_DIR, "tvos");
18177
18177
 
18178
18178
  // ../telemetry/src/consent.ts
18179
18179
  import * as fs8 from "node:fs";
18180
- var cache = { current: null };
18180
+ var cache = /* @__PURE__ */ new Map();
18181
18181
  var sessionOverride = null;
18182
- function readConfigOverride() {
18182
+ function readConfigOverrideAt(filePath) {
18183
+ const miss = { mtimeMs: null, fingerprint: null, enabledOverride: null };
18183
18184
  let stats;
18184
18185
  try {
18185
- stats = fs8.lstatSync(configFilePath());
18186
- } catch (err) {
18187
- if (err.code === "ENOENT") {
18188
- cache.current = { mtimeMs: null, fingerprint: null, enabledOverride: null };
18189
- return null;
18190
- }
18191
- cache.current = { mtimeMs: null, fingerprint: null, enabledOverride: null };
18186
+ stats = fs8.lstatSync(filePath);
18187
+ } catch {
18188
+ cache.set(filePath, miss);
18192
18189
  return null;
18193
18190
  }
18194
18191
  if (!stats.isFile()) {
18195
- cache.current = { mtimeMs: null, fingerprint: null, enabledOverride: null };
18192
+ cache.set(filePath, miss);
18196
18193
  return null;
18197
18194
  }
18198
18195
  const fingerprint = `${stats.dev}:${stats.ino}:${stats.size}`;
18199
18196
  const mtimeMs = stats.mtimeMs;
18200
- if (cache.current && cache.current.fingerprint === fingerprint && cache.current.mtimeMs === mtimeMs) {
18201
- return cache.current.enabledOverride;
18197
+ const cached2 = cache.get(filePath);
18198
+ if (cached2 && cached2.fingerprint === fingerprint && cached2.mtimeMs === mtimeMs) {
18199
+ return cached2.enabledOverride;
18202
18200
  }
18203
18201
  let parsedEnabled = null;
18204
18202
  try {
18205
- const raw = fs8.readFileSync(configFilePath(), "utf8");
18203
+ const raw = fs8.readFileSync(filePath, "utf8");
18206
18204
  const json = JSON.parse(raw);
18207
18205
  if (json && typeof json === "object") {
18208
18206
  const t = json.telemetry;
@@ -18213,9 +18211,27 @@ function readConfigOverride() {
18213
18211
  }
18214
18212
  } catch {
18215
18213
  }
18216
- cache.current = { mtimeMs, fingerprint, enabledOverride: parsedEnabled };
18214
+ cache.set(filePath, { mtimeMs, fingerprint, enabledOverride: parsedEnabled });
18217
18215
  return parsedEnabled;
18218
18216
  }
18217
+ function readPersistedConsent(cwd) {
18218
+ const globalPath = configFilePath("global");
18219
+ const projectPath = configFilePath("project", { cwd });
18220
+ const global = readConfigOverrideAt(globalPath);
18221
+ const project = projectPath === globalPath ? null : readConfigOverrideAt(projectPath);
18222
+ if (project === false && global === false) {
18223
+ return { enabled: false, detail: "config.json (project and global)" };
18224
+ }
18225
+ if (project === false) return { enabled: false, detail: "config.json (project)" };
18226
+ if (global === false) return { enabled: false, detail: "config.json (global)" };
18227
+ if (project === true || global === true) {
18228
+ return {
18229
+ enabled: true,
18230
+ detail: project === true ? "config.json (project)" : "config.json (global)"
18231
+ };
18232
+ }
18233
+ return null;
18234
+ }
18219
18235
  function parseFalsy(value) {
18220
18236
  if (value === void 0) return false;
18221
18237
  const v = value.trim().toLowerCase();
@@ -18225,7 +18241,7 @@ function isDoNotTrackSet(value) {
18225
18241
  if (value === void 0 || value.trim() === "") return false;
18226
18242
  return !parseFalsy(value);
18227
18243
  }
18228
- function getConsentState(env = process.env) {
18244
+ function getConsentState(env = process.env, cwd = process.cwd()) {
18229
18245
  if (isDoNotTrackSet(env.DO_NOT_TRACK)) {
18230
18246
  return {
18231
18247
  enabled: false,
@@ -18242,17 +18258,17 @@ function getConsentState(env = process.env) {
18242
18258
  if (sessionOverride !== null) {
18243
18259
  return { enabled: sessionOverride, source: { source: "session_override" } };
18244
18260
  }
18245
- const persisted = readConfigOverride();
18246
- if (persisted === false) {
18247
- return { enabled: false, source: { source: "config_file", detail: "config.json" } };
18248
- }
18249
- if (persisted === true) {
18250
- return { enabled: true, source: { source: "config_file", detail: "config.json" } };
18261
+ const persisted = readPersistedConsent(cwd);
18262
+ if (persisted !== null) {
18263
+ return {
18264
+ enabled: persisted.enabled,
18265
+ source: { source: "config_file", detail: persisted.detail }
18266
+ };
18251
18267
  }
18252
18268
  return { enabled: true, source: { source: "default" } };
18253
18269
  }
18254
- function isEnabled(env = process.env) {
18255
- return getConsentState(env).enabled;
18270
+ function isEnabled(env = process.env, cwd) {
18271
+ return getConsentState(env, cwd).enabled;
18256
18272
  }
18257
18273
 
18258
18274
  // ../telemetry/src/notice.ts
@@ -92132,8 +92132,8 @@ function asStringArray(raw) {
92132
92132
  var CONFIG_SCHEMA = [
92133
92133
  {
92134
92134
  key: "telemetry.enabled",
92135
- description: "Whether anonymous opt-out telemetry is enabled (on by default; environment opt-outs like DO_NOT_TRACK are not reflected here \u2014 `argent telemetry status` shows effective consent).",
92136
- scopes: ["global"],
92135
+ description: "Whether anonymous opt-out telemetry is enabled (on by default; environment opt-outs like DO_NOT_TRACK are not reflected here \u2014 `argent telemetry status` shows effective consent). `false` in either scope wins, so a committed project opt-out holds for every teammate.",
92136
+ scopes: ["project", "global"],
92137
92137
  parse: asBoolean,
92138
92138
  merge: "prioritize-restrictive",
92139
92139
  // Opt-out: consent.ts reads an unstored value as enabled, so the config
@@ -95734,7 +95734,7 @@ var _CI_VENDOR_COUNT_FOR_TEST = vendors_default.length;
95734
95734
  var SESSION_ID = (0, import_node_crypto3.randomUUID)();
95735
95735
  function readCliVersion() {
95736
95736
  if (true) {
95737
- return "0.22.1-next.6";
95737
+ return "0.22.1-next.8";
95738
95738
  }
95739
95739
  return "0.0.0";
95740
95740
  }
@@ -96201,32 +96201,30 @@ function resolveHostFingerprintAsync() {
96201
96201
 
96202
96202
  // ../telemetry/src/consent.ts
96203
96203
  var fs8 = __toESM(require("node:fs"));
96204
- var cache = { current: null };
96204
+ var cache = /* @__PURE__ */ new Map();
96205
96205
  var sessionOverride = null;
96206
- function readConfigOverride() {
96206
+ function readConfigOverrideAt(filePath) {
96207
+ const miss = { mtimeMs: null, fingerprint: null, enabledOverride: null };
96207
96208
  let stats;
96208
96209
  try {
96209
- stats = fs8.lstatSync(configFilePath());
96210
- } catch (err) {
96211
- if (err.code === "ENOENT") {
96212
- cache.current = { mtimeMs: null, fingerprint: null, enabledOverride: null };
96213
- return null;
96214
- }
96215
- cache.current = { mtimeMs: null, fingerprint: null, enabledOverride: null };
96210
+ stats = fs8.lstatSync(filePath);
96211
+ } catch {
96212
+ cache.set(filePath, miss);
96216
96213
  return null;
96217
96214
  }
96218
96215
  if (!stats.isFile()) {
96219
- cache.current = { mtimeMs: null, fingerprint: null, enabledOverride: null };
96216
+ cache.set(filePath, miss);
96220
96217
  return null;
96221
96218
  }
96222
96219
  const fingerprint = `${stats.dev}:${stats.ino}:${stats.size}`;
96223
96220
  const mtimeMs = stats.mtimeMs;
96224
- if (cache.current && cache.current.fingerprint === fingerprint && cache.current.mtimeMs === mtimeMs) {
96225
- return cache.current.enabledOverride;
96221
+ const cached3 = cache.get(filePath);
96222
+ if (cached3 && cached3.fingerprint === fingerprint && cached3.mtimeMs === mtimeMs) {
96223
+ return cached3.enabledOverride;
96226
96224
  }
96227
96225
  let parsedEnabled = null;
96228
96226
  try {
96229
- const raw = fs8.readFileSync(configFilePath(), "utf8");
96227
+ const raw = fs8.readFileSync(filePath, "utf8");
96230
96228
  const json2 = JSON.parse(raw);
96231
96229
  if (json2 && typeof json2 === "object") {
96232
96230
  const t = json2.telemetry;
@@ -96237,9 +96235,27 @@ function readConfigOverride() {
96237
96235
  }
96238
96236
  } catch {
96239
96237
  }
96240
- cache.current = { mtimeMs, fingerprint, enabledOverride: parsedEnabled };
96238
+ cache.set(filePath, { mtimeMs, fingerprint, enabledOverride: parsedEnabled });
96241
96239
  return parsedEnabled;
96242
96240
  }
96241
+ function readPersistedConsent(cwd) {
96242
+ const globalPath = configFilePath("global");
96243
+ const projectPath = configFilePath("project", { cwd });
96244
+ const global2 = readConfigOverrideAt(globalPath);
96245
+ const project = projectPath === globalPath ? null : readConfigOverrideAt(projectPath);
96246
+ if (project === false && global2 === false) {
96247
+ return { enabled: false, detail: "config.json (project and global)" };
96248
+ }
96249
+ if (project === false) return { enabled: false, detail: "config.json (project)" };
96250
+ if (global2 === false) return { enabled: false, detail: "config.json (global)" };
96251
+ if (project === true || global2 === true) {
96252
+ return {
96253
+ enabled: true,
96254
+ detail: project === true ? "config.json (project)" : "config.json (global)"
96255
+ };
96256
+ }
96257
+ return null;
96258
+ }
96243
96259
  function parseFalsy(value) {
96244
96260
  if (value === void 0) return false;
96245
96261
  const v = value.trim().toLowerCase();
@@ -96249,7 +96265,7 @@ function isDoNotTrackSet(value) {
96249
96265
  if (value === void 0 || value.trim() === "") return false;
96250
96266
  return !parseFalsy(value);
96251
96267
  }
96252
- function getConsentState(env = process.env) {
96268
+ function getConsentState(env = process.env, cwd = process.cwd()) {
96253
96269
  if (isDoNotTrackSet(env.DO_NOT_TRACK)) {
96254
96270
  return {
96255
96271
  enabled: false,
@@ -96266,17 +96282,17 @@ function getConsentState(env = process.env) {
96266
96282
  if (sessionOverride !== null) {
96267
96283
  return { enabled: sessionOverride, source: { source: "session_override" } };
96268
96284
  }
96269
- const persisted = readConfigOverride();
96270
- if (persisted === false) {
96271
- return { enabled: false, source: { source: "config_file", detail: "config.json" } };
96272
- }
96273
- if (persisted === true) {
96274
- return { enabled: true, source: { source: "config_file", detail: "config.json" } };
96285
+ const persisted = readPersistedConsent(cwd);
96286
+ if (persisted !== null) {
96287
+ return {
96288
+ enabled: persisted.enabled,
96289
+ source: { source: "config_file", detail: persisted.detail }
96290
+ };
96275
96291
  }
96276
96292
  return { enabled: true, source: { source: "default" } };
96277
96293
  }
96278
- function isEnabled(env = process.env) {
96279
- return getConsentState(env).enabled;
96294
+ function isEnabled(env = process.env, cwd) {
96295
+ return getConsentState(env, cwd).enabled;
96280
96296
  }
96281
96297
 
96282
96298
  // ../telemetry/src/notice.ts
@@ -111139,7 +111155,7 @@ function isNativeDevtoolsBlockResult(toolId, result) {
111139
111155
  async function precheckNativeDevtools(api, udid, bundleId) {
111140
111156
  if (bundleId !== void 0 && !isInjectableBundleId(bundleId)) {
111141
111157
  throw new FailureError(
111142
- `${bundleId} is an Apple system app: it is a platform binary with library validation, so Argent native devtools cannot be relied on to inject into it \u2014 treat it as unavailable rather than retrying. ` + NON_INJECTABLE_RECOVERY,
111158
+ `${bundleId} is an Apple system app: it is never the app under test, so Argent native devtools refuse to read one \u2014 treat it as unavailable rather than retrying. ` + NON_INJECTABLE_RECOVERY,
111143
111159
  {
111144
111160
  error_code: FAILURE_CODES.NATIVE_DEVTOOLS_NOT_INJECTABLE,
111145
111161
  failure_stage: "native_devtools_precheck",
@@ -111838,7 +111854,7 @@ function tcpArtifactHint(err) {
111838
111854
  return /TCP-transport (?:binary|dylib) not found/.test(message) ? message : void 0;
111839
111855
  }
111840
111856
  var TVOS_HINT = "This is an Apple TV (tvOS) simulator, which the iOS accessibility service does not support. Use the `describe` tool to read the focused and focusable elements, `tv-remote` (up/down/left/right/select/back/menu/home) to move focus, and `keyboard` to type. See the argent-tv-interact skill.";
111841
- var NON_INJECTABLE_HINT = "This is an Apple system app (com.apple.*), which cannot be relied on to load argent's native-devtools instrumentation \u2014 the native view hierarchy is unavailable and restarting the app will NOT help. Take a `screenshot` to see the screen and interact by coordinate. " + NON_INJECTABLE_NATIVE_WARNING;
111857
+ var NON_INJECTABLE_HINT = "This is an Apple system app (com.apple.*), which argent's native-devtools instrumentation does not support \u2014 the native view hierarchy is unavailable and restarting the app will NOT help. Take a `screenshot` to see the screen and interact by coordinate. " + NON_INJECTABLE_NATIVE_WARNING;
111842
111858
  function emptyTree() {
111843
111859
  return parseDescribeResult({
111844
111860
  role: "AXGroup",
@@ -116209,10 +116225,10 @@ Returns { envSetup, appRunning, connected, requiresRestart, state, message, next
116209
116225
  - state: why devtools are or aren't live, measured from the running process. "connected"; "not_running"; "stale_process" (the process cannot reach this simulator's devtools endpoint \u2014 launched either before argent's instrumentation was in place or against an earlier tool-server's listener \u2014 so restart-app fixes it); "unregistered" (the process IS injected and pointed at this simulator's devtools endpoint yet the service never registered it, so restarting the app cannot help); "connecting" (the process IS injected but launched moments ago and is still connecting, so waiting is what helps); "indeterminate" (the process could not be inspected). Omitted when injectable is false, which is terminal on its own.
116210
116226
  - message: the remedy for that state, in full. Omitted when connected or non-injectable. Prefer it over inferring one from the booleans \u2014 it is the only field that can tell you to stop restarting the app.
116211
116227
  - nextLaunchWillBeInjected: if you launch this bundle now, native devtools env setup is already in place (always false for a non-injectable app)
116212
- - injectable: whether native devtools can be relied on to inject into this app. Apple system apps (bundle ids under com.apple.) are platform binaries with library validation, so the dylib cannot be counted on to load into them \u2014 it has been observed both loading and not loading, depending on the simulator runtime.
116228
+ - injectable: whether this app is a supported target for Argent native devtools. Apple system apps (bundle ids under com.apple.) are not: they are never the app under test, so the native tools refuse to read one.
116213
116229
 
116214
116230
  Call this before using app-scoped native hierarchy tools or native-network-logs.
116215
- If injectable is false: treat this as TERMINAL \u2014 injection cannot be relied on for this app, and no relaunch changes which way it goes. Do NOT restart/retry. Use the standard \`describe\` tool (its accessibility path reads the screen without injection) or \`screenshot\` (then interact by coordinate). Do not fall back to the native-devtools feature tools (native-describe-screen, native-find-views, native-full-hierarchy, native-network-logs, native-view-at-point, native-user-interactable-view-at-point) \u2014 they run the same injection precheck and fail with the same non-injectable error.
116231
+ If injectable is false: treat this as TERMINAL \u2014 the app is not a supported native-devtools target, and no relaunch changes that. Do NOT restart/retry. Use the standard \`describe\` tool (its accessibility path reads the screen without injection) or \`screenshot\` (then interact by coordinate). Do not fall back to the native-devtools feature tools (native-describe-screen, native-find-views, native-full-hierarchy, native-network-logs, native-view-at-point, native-user-interactable-view-at-point) \u2014 they run the same injection precheck and fail with the same non-injectable error.
116216
116232
  If appRunning is false and nextLaunchWillBeInjected is true: use launch-app normally.
116217
116233
  If requiresRestart is true: call restart-app once, then proceed with the native feature. Read state before acting on a second such reading \u2014 indeterminate reaches this rule too, and its line below bounds it at that one restart.
116218
116234
  If state is unregistered: do NOT restart the app again \u2014 it already launched under the terms a restart would recreate. Restart the tool-server (\`argent server stop && argent server start --detach\`), then retry. If it reads unregistered again after that restart, stop: the process loads argent's dylib but never dials, and no further restart on either side changes it \u2014 treat native devtools as unavailable, then use \`describe\` or \`screenshot\` and drive by coordinate.
@@ -144637,7 +144653,7 @@ var FULL_HIERARCHY_FIELDS = [
144637
144653
  ];
144638
144654
  async function unreadableHierarchyReason(nativeApi, bundleId) {
144639
144655
  if (!isInjectableBundleId(bundleId)) {
144640
- return `${bundleId} is an Apple system app: it is a platform binary with library validation, so argent's native devtools cannot be relied on to inject into it, and without them a flow has no view hierarchy to resolve selectors against. Replace the selector steps with coordinate ones \u2014 \`tap: { x: 0.5, y: 0.35 }\` takes a point directly and reads no tree \u2014 or target an app argent installs.`;
144656
+ return `${bundleId} is an Apple system app: it is never the app under test, so argent's native devtools refuse to read one, and without them a flow has no view hierarchy to resolve selectors against. Replace the selector steps with coordinate ones \u2014 \`tap: { x: 0.5, y: 0.35 }\` takes a point directly and reads no tree \u2014 or target an app argent installs.`;
144641
144657
  }
144642
144658
  const state3 = await nativeApi.appConnectionState(bundleId).catch(() => "indeterminate");
144643
144659
  if (state3 === "connected") {
@@ -144645,7 +144661,10 @@ async function unreadableHierarchyReason(nativeApi, bundleId) {
144645
144661
  }
144646
144662
  return `${buildAppStateMessage(bundleId, state3)} Flows resolve selectors against the full view hierarchy native devtools serve.`;
144647
144663
  }
144648
- async function queryFullHierarchyTree(registry2, device, launchedNativeApp) {
144664
+ function systemAppFlowTargetRefusal(bundleId) {
144665
+ return `${bundleId} is an Apple system app (com.apple.*) - never a valid flow target: it is not the app under test, and argent's native devtools refuse to read one (a system process either never services the read, or describes offscreen UI as if it were the launched app), so this flow has no view hierarchy to resolve selectors against and no relaunch or retry changes this verdict. Replace the selector steps with coordinate ones - \`tap: { x: 0.5, y: 0.35 }\` takes a point directly and reads no tree - or point this flow's \`launch\` step at the app under test.`;
144666
+ }
144667
+ async function queryFullHierarchyTree(registry2, device, target) {
144649
144668
  let nativeApi;
144650
144669
  try {
144651
144670
  const ndRef = nativeDevtoolsRef(device);
@@ -144656,22 +144675,113 @@ async function queryFullHierarchyTree(registry2, device, launchedNativeApp) {
144656
144675
  { cause: err }
144657
144676
  );
144658
144677
  }
144659
- if (launchedNativeApp !== void 0 && nativeApi.listConnectedBundleIds().length === 0) {
144660
- throw new Error(await unreadableHierarchyReason(nativeApi, launchedNativeApp));
144661
- }
144662
- let target;
144663
- try {
144664
- target = await resolveNativeTargetApp(nativeApi, void 0);
144665
- } catch (err) {
144666
- const timedOut = getFailureSignal(err)?.error_code === FAILURE_CODES.NATIVE_DEVTOOLS_RPC_TIMEOUT;
144667
- if (timedOut && launchedNativeApp !== void 0 && nativeApi.listConnectedBundleIds().includes(launchedNativeApp)) {
144668
- target = { bundleId: launchedNativeApp };
144669
- } else {
144670
- throw err;
144678
+ let bundleId;
144679
+ if (target?.pinned) {
144680
+ bundleId = target.bundleId;
144681
+ if (!isInjectableBundleId(bundleId)) {
144682
+ throw new FailureError(systemAppFlowTargetRefusal(bundleId), {
144683
+ error_code: FAILURE_CODES.NATIVE_DEVTOOLS_NOT_INJECTABLE,
144684
+ failure_stage: "flow_tree_pinned_target",
144685
+ failure_area: "tool_server",
144686
+ error_kind: "validation"
144687
+ });
144688
+ }
144689
+ if (!nativeApi.isConnected(bundleId)) {
144690
+ throw new FailureError(
144691
+ `${bundleId} lost its devtools connection after launch (the app crashed, was terminated, or its socket closed) - restart it (restart-app, or a flow \`launch\` step) so the full view hierarchy is readable; launch-app recovers only the causes that killed the process, since on iOS it just foregrounds one that is still alive`,
144692
+ {
144693
+ error_code: FAILURE_CODES.NATIVE_DEVTOOLS_NOT_CONNECTED,
144694
+ failure_stage: "flow_tree_pinned_target",
144695
+ failure_area: "tool_server",
144696
+ error_kind: "not_found"
144697
+ }
144698
+ );
144699
+ }
144700
+ let pinnedState;
144701
+ try {
144702
+ pinnedState = await nativeApi.getAppState(bundleId);
144703
+ target.probeAnswered = true;
144704
+ } catch (err) {
144705
+ if (getFailureSignal(err)?.error_code !== FAILURE_CODES.NATIVE_DEVTOOLS_RPC_TIMEOUT) {
144706
+ throw err;
144707
+ }
144708
+ if (target.probeAnswered) {
144709
+ throw new FailureError(
144710
+ `${bundleId} (the launched app) stopped answering Application.getState - the probe timed out although an earlier one in this run answered, so the app's main queue is no longer being serviced. For a pinned app that is usually the suspension iOS applies once a flow leaves it (e.g. a tap that opened another app), and a suspended app's hierarchy is not what is on screen; in-app work blocking the main thread past the probe timeout looks the same from here. Reading anyway parks on that same unserviced queue: certain to time out if the app is suspended, and paying the longer hierarchy timeout to find that out if it is not. If this flow's subject IS the other app, give the flow a \`launch:\` step for that app - it re-pins reads to it, and a pinned read probes only the app it names, so the silent ${bundleId} is never touched; \`tool: launch-app\` or \`tool: restart-app\` naming that app works too - each re-targets reads at the app it starts, and is how a recorded flow switches apps. A foreground-NEUTRAL raw \`tool:\` step does not work here, because demoting the pin sends reads back to auto-resolve, which probes every connection at once and is sunk by this same silent one. If the flow left ${bundleId} but is still about it, make it return before reading the UI. If it never left, the main thread is busy: raise the step's \`timeout:\` so the poll re-reads past the work, or \`launch\` ${bundleId} again.`,
144711
+ {
144712
+ // The timeout's own code, NOT
144713
+ // NATIVE_TARGET_SINGLE_APP_NOT_FOREGROUND: nothing answered, so no
144714
+ // app state was observed to classify it by.
144715
+ error_code: FAILURE_CODES.NATIVE_DEVTOOLS_RPC_TIMEOUT,
144716
+ failure_stage: "flow_tree_pinned_target",
144717
+ failure_area: "tool_server",
144718
+ error_kind: "timeout"
144719
+ },
144720
+ err instanceof Error ? { cause: err } : void 0
144721
+ );
144722
+ }
144723
+ }
144724
+ if (pinnedState && !chooseFrontmostConnectedApp([pinnedState])) {
144725
+ throw new FailureError(
144726
+ `${bundleId} (the launched app) has no foreground presence at all (applicationState=${pinnedState.applicationState}, foregroundActiveScenes=${pinnedState.foregroundActiveSceneCount}, foregroundInactiveScenes=${pinnedState.foregroundInactiveSceneCount}) - a step in this flow left the app (e.g. a tap that opened another app), so a read of its hierarchy would describe a screen that is not on screen. Transitional states are NOT refused here: an \`inactive\` app, or one still holding a foreground scene, is read as usual - under a system alert or mid-transition it is still the app on screen. If this flow's subject IS another app, give the flow a \`launch:\` step for that app - it re-pins reads to it, and no wedged sibling connection can sink a pinned read; a raw \`tool:\` step demotes the pin and returns reads to frontmost auto-resolve, which works when the app on screen answers but is sunk by a single wedged connection. Otherwise make the flow return to ${bundleId} before reading the UI, or \`launch\` it again.`,
144727
+ {
144728
+ error_code: FAILURE_CODES.NATIVE_TARGET_SINGLE_APP_NOT_FOREGROUND,
144729
+ failure_stage: "flow_tree_pinned_target",
144730
+ failure_area: "tool_server",
144731
+ error_kind: "validation"
144732
+ }
144733
+ );
144734
+ }
144735
+ } else {
144736
+ if (target && nativeApi.listConnectedBundleIds().length === 0) {
144737
+ throw new Error(await unreadableHierarchyReason(nativeApi, target.bundleId));
144738
+ }
144739
+ let resolved;
144740
+ try {
144741
+ resolved = await resolveNativeTargetApp(nativeApi, void 0);
144742
+ } catch (err) {
144743
+ const timedOut = getFailureSignal(err)?.error_code === FAILURE_CODES.NATIVE_DEVTOOLS_RPC_TIMEOUT;
144744
+ if (!timedOut || !target) throw err;
144745
+ if (!isInjectableBundleId(target.bundleId)) {
144746
+ throw new FailureError(
144747
+ systemAppFlowTargetRefusal(target.bundleId),
144748
+ {
144749
+ error_code: FAILURE_CODES.NATIVE_DEVTOOLS_NOT_INJECTABLE,
144750
+ failure_stage: "flow_tree_unpinned_hint",
144751
+ failure_area: "tool_server",
144752
+ error_kind: "validation"
144753
+ },
144754
+ err instanceof Error ? { cause: err } : void 0
144755
+ );
144756
+ }
144757
+ if (!nativeApi.listConnectedBundleIds().includes(target.bundleId)) throw err;
144758
+ let hintState;
144759
+ try {
144760
+ hintState = await nativeApi.getAppState(target.bundleId);
144761
+ } catch (probeErr) {
144762
+ if (getFailureSignal(probeErr)?.error_code === FAILURE_CODES.NATIVE_DEVTOOLS_RPC_TIMEOUT) {
144763
+ throw err;
144764
+ }
144765
+ throw probeErr;
144766
+ }
144767
+ if (!chooseFrontmostConnectedApp([hintState])) {
144768
+ throw new FailureError(
144769
+ `${target.bundleId} (the launched app) has no foreground presence at all (applicationState=${hintState.applicationState}, foregroundActiveScenes=${hintState.foregroundActiveSceneCount}, foregroundInactiveScenes=${hintState.foregroundInactiveSceneCount}) - auto-resolve's probe of every connection timed out and the read fell back to the launched app, but a step in this flow left it (e.g. a tap that opened another app), so reading its hierarchy would describe a screen that is not on screen. Transitional states are NOT refused here: an \`inactive\` app, or one still holding a foreground scene, is read as usual. If this flow's subject IS another app, give the flow a \`launch:\` step for that app; otherwise make the flow return to ${target.bundleId} before reading the UI, or \`launch\` it again.`,
144770
+ {
144771
+ error_code: FAILURE_CODES.NATIVE_TARGET_SINGLE_APP_NOT_FOREGROUND,
144772
+ failure_stage: "flow_tree_unpinned_hint",
144773
+ failure_area: "tool_server",
144774
+ error_kind: "validation"
144775
+ },
144776
+ err instanceof Error ? { cause: err } : void 0
144777
+ );
144778
+ }
144779
+ resolved = { bundleId: target.bundleId };
144671
144780
  }
144781
+ bundleId = resolved.bundleId;
144672
144782
  }
144673
144783
  const rawResult = await nativeApi.queryViewHierarchy(
144674
- target.bundleId,
144784
+ bundleId,
144675
144785
  "ViewHierarchy.getFullHierarchy",
144676
144786
  {
144677
144787
  fields: FULL_HIERARCHY_FIELDS,
@@ -144679,11 +144789,11 @@ async function queryFullHierarchyTree(registry2, device, launchedNativeApp) {
144679
144789
  }
144680
144790
  );
144681
144791
  if (rawResult.error) {
144682
- throw new Error(`getFullHierarchy failed for ${target.bundleId}: ${rawResult.error}`);
144792
+ throw new Error(`getFullHierarchy failed for ${bundleId}: ${rawResult.error}`);
144683
144793
  }
144684
144794
  if (!Array.isArray(rawResult.windows) || rawResult.windows.length === 0) {
144685
144795
  throw new Error(
144686
- `getFullHierarchy returned no windows for ${target.bundleId} \u2014 the app is not injectable (e.g. an Apple system app) or has no readable foreground window, so flows cannot resolve selectors against its view hierarchy`
144796
+ `getFullHierarchy returned no windows for ${bundleId} - it has no window attached to read (backgrounded, or its first window not attached yet), so flows cannot resolve selectors against its view hierarchy; foreground or relaunch it, and if that bundle id is a com.apple.* system process the read resolved to a background system app rather than the app under test, so give this flow a \`launch\` step to pin reads to the right app`
144687
144797
  );
144688
144798
  }
144689
144799
  const { tree, screen } = adaptFullHierarchy(rawResult);
@@ -144890,13 +145000,15 @@ async function queryVegaTree(device) {
144890
145000
  }
144891
145001
 
144892
145002
  // ../tool-server/src/tools/flows/flow-tree.ts
144893
- async function fetchFlowTree(registry2, device, launchedNativeApp) {
145003
+ async function fetchFlowTree(registry2, device, target) {
144894
145004
  const source = FLOW_TREE_SOURCES[device.platform];
144895
145005
  if (!source) return fetchTree(registry2, device);
144896
- return source(registry2, device, launchedNativeApp);
145006
+ return source(registry2, device, target);
144897
145007
  }
144898
145008
  var FLOW_TREE_SOURCES = {
144899
- ios: (registry2, device, launchedNativeApp) => queryFullHierarchyTree(registry2, device, launchedNativeApp),
145009
+ // Only iOS consumes the target: the platforms below resolve their tree
145010
+ // source per-device and never auto-resolve.
145011
+ ios: (registry2, device, target) => queryFullHierarchyTree(registry2, device, target),
144900
145012
  android: (registry2, device) => queryAndroidFullHierarchy(registry2, device),
144901
145013
  chromium: (registry2, device) => queryChromiumTree(registry2, device),
144902
145014
  vega: (_registry, device) => queryVegaTree(device)
@@ -145102,7 +145214,7 @@ function provenTreeOutage(env) {
145102
145214
  return proven && proven.deviceId === env.device.id ? proven.error : void 0;
145103
145215
  }
145104
145216
  function readFlowTree(env) {
145105
- return fetchFlowTree(env.registry, env.device, env.launchedNativeApp).then((data) => {
145217
+ return fetchFlowTree(env.registry, env.device, env.treeTarget).then((data) => {
145106
145218
  if (env.treeOutage) env.treeOutage.proven = void 0;
145107
145219
  return data;
145108
145220
  });
@@ -146157,7 +146269,11 @@ async function captureTapSelector(registry2, session, udid, point) {
146157
146269
  try {
146158
146270
  const device = resolveDevice(udid);
146159
146271
  const launched = recordedLaunchedApp(session, device.platform);
146160
- const { tree, source } = await fetchFlowTree(registry2, device, launched);
146272
+ const { tree, source } = await fetchFlowTree(
146273
+ registry2,
146274
+ device,
146275
+ launched ? { bundleId: launched, pinned: false, probeAnswered: false } : void 0
146276
+ );
146161
146277
  const node = nodeAtPoint(tree, point);
146162
146278
  if (!node) return { warning: "no element found under the tap; kept coordinates (brittle)" };
146163
146279
  const selector = deriveSelector(node);
@@ -149095,6 +149211,7 @@ async function runLaunch(state3, app) {
149095
149211
  reason: `no app id declared for platform "${device.platform}" \u2014 add a launch entry for it`
149096
149212
  };
149097
149213
  }
149214
+ state3.treeTarget = void 0;
149098
149215
  let restart;
149099
149216
  try {
149100
149217
  restart = await invokeOnDevice(env, "restart-app", { bundleId });
@@ -149109,7 +149226,7 @@ async function runLaunch(state3, app) {
149109
149226
  const gate = await treeSourceGate(registry2, device, bundleId, signal);
149110
149227
  if (signal?.aborted) return ABORTED_OUTCOME;
149111
149228
  if (gate) return { ok: false, reason: gate };
149112
- state3.launchedNativeApp = bundleId;
149229
+ state3.treeTarget = { bundleId, pinned: true, probeAnswered: false };
149113
149230
  return { ok: true };
149114
149231
  }
149115
149232
  async function runChromiumLaunch(state3, app) {
@@ -149296,7 +149413,11 @@ function createRunFlowTool(registry2) {
149296
149413
  },
149297
149414
  description: `Run a saved flow from the .argent/flows/ directory, or an explicit boundary-managed flow_path.
149298
149415
  Steps run in order: \`launch\` starts an app from scratch (terminate + relaunch) and waits until it is
149299
- ready; \`tool\` calls dispatch through the registry; \`tap\`/\`long-press\`/\`type\` resolve a selector to an
149416
+ ready (on iOS it also pins later element lookups to that app rather than auto-detecting the frontmost
149417
+ one); \`tool\` calls dispatch through the registry (a raw \`tool\` step ends that iOS pin, so lookups
149418
+ auto-detect again until the next \`launch\`, though a tool that cannot change the foreground app leaves the
149419
+ launched id as a fallback for a timed-out auto-detect, and \`launch-app\`/\`restart-app\` leave the id they
149420
+ started as that fallback instead); \`tap\`/\`long-press\`/\`type\` resolve a selector to an
149300
149421
  element and act on it (\`tap: { on, times: 2 }\` double-taps; \`long-press: { on, duration }\` presses and
149301
149422
  holds; \`tap\`/\`long-press\` alternatively take a raw normalized point \u2014 bare \`{ x, y }\` or \`on: { x, y }\`;
149302
149423
  any selector may scope its matches geometrically, the CSS combinators read off frames: \`within: <selector>\`
@@ -150005,14 +150126,17 @@ async function execLeafStep(state3, step, index, scope) {
150005
150126
  if (step.delayMs && !await sleepOrAbort(step.delayMs, signal)) {
150006
150127
  return { ...base, status: "skip", tool: step.name, reason: "run aborted during delay" };
150007
150128
  }
150129
+ if (FOREGROUND_CHANGING_TOOLS.has(step.name)) {
150130
+ state3.treeTarget = void 0;
150131
+ if (state3.treeOutage) state3.treeOutage.proven = void 0;
150132
+ } else if (state3.treeTarget?.pinned) {
150133
+ state3.treeTarget = { ...state3.treeTarget, pinned: false };
150134
+ if (state3.treeOutage) state3.treeOutage.proven = void 0;
150135
+ }
150136
+ if (isNestedOrchestratorTool(step.name) && state3.treeOutage) {
150137
+ state3.treeOutage.proven = void 0;
150138
+ }
150008
150139
  try {
150009
- if (FOREGROUND_CHANGING_TOOLS.has(step.name)) {
150010
- state3.launchedNativeApp = void 0;
150011
- if (state3.treeOutage) state3.treeOutage.proven = void 0;
150012
- }
150013
- if (isNestedOrchestratorTool(step.name) && state3.treeOutage) {
150014
- state3.treeOutage.proven = void 0;
150015
- }
150016
150140
  const result = await invokeSubTool(registry2, ctx, step.name, args);
150017
150141
  if (isUnmetUiWaitResult(step.name, result)) {
150018
150142
  const note = result.note;
@@ -150059,7 +150183,9 @@ async function execLeafStep(state3, step, index, scope) {
150059
150183
  }
150060
150184
  if (step.name === "launch-app" || step.name === "restart-app") {
150061
150185
  const launched = args.bundleId;
150062
- if (typeof launched === "string") state3.launchedNativeApp = launched;
150186
+ if (typeof launched === "string") {
150187
+ state3.treeTarget = { bundleId: launched, pinned: false, probeAnswered: false };
150188
+ }
150063
150189
  }
150064
150190
  return { ...base, status: "pass", tool: step.name, result, outputHint, args };
150065
150191
  } catch (err) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@swmansion/argent",
3
- "version": "0.22.1-next.6",
3
+ "version": "0.22.1-next.8",
4
4
  "mcpName": "io.github.software-mansion/argent",
5
5
  "description": "MCP server for iOS Simulator and Android Emulator control",
6
6
  "license": "Apache-2.0",
@@ -70,6 +70,12 @@ On iOS, Android, and Chromium, an id absent from `describe` can still resolve in
70
70
 
71
71
  The recorder rechecks each successful `await-ui-element` against the runner tree. Follow any `message` warning and replay each conversion. On Vega, a mismatch usually means the screen changed. A `text` check can also select different elements from the same source. See [Live waits and checks](live-authoring.md#live-waits-and-checks).
72
72
 
73
+ **On iOS, a `launch:` step also decides which app the runner reads.** A successful `launch:` pins later runner-tree reads to that app, so a read probes only that app instead of fanning out over every connected one to find the frontmost. A pinned read still refuses, naming the reason, when the app has no foreground presence left, when it stops answering after an earlier read got through, when its devtools connection dropped, or when the pinned id is a `com.apple.*` system app.
74
+
75
+ Any raw `tool:` step ends the pin, because its effect on the screen is opaque to the runner, and reads auto-detect the frontmost app again until the next `launch:` re-pins. A tool that cannot change the foreground app leaves the launched id as an unpinned fallback, which takes the read only when auto-detection times out and the launched app vouches for itself with a probe of its own. `launch-app`, `restart-app`, `reinstall-app`, `open-url`, and `button` drop even that; `launch-app` and `restart-app` replace it with the app they just started, still unpinned. Nested `run:` fragments inherit both the pin and its clearing.
76
+
77
+ So on iOS recording and replay can read different apps, not only different projections: recording has no run state and always auto-detects the frontmost connected app, while a replay read between a `launch:` and the next raw `tool:` step reads the launched app.
78
+
73
79
  **On iOS, never copy a `role` from `describe` into a flow selector.** The runner derives iOS roles from the UIView class name and `describe` from accessibility traits, so a React Native `Pressable` (class `RCTView`) is `AXGroup` to the runner and `AXButton` to `describe`. Select on `id`/`text`, or confirm the role against the runner's own tree.
74
80
 
75
81
  When several nodes match, the directive decides:
@@ -99,7 +105,7 @@ Scopes can combine and nest, with at most six scope keys. Use strict selectors f
99
105
 
100
106
  Directives stop the flow on failure and skip later steps. `flow-execute` documents their shapes. The available directives are `launch`, `tap`, `long-press`, `type`, `scroll-to`, `pinch`, `rotate`, `await`, `assert`, `wait`, `snapshot`, `run`, `when`, `echo`, and `tool`.
101
107
 
102
- Use the launch map for cross-platform flows. A bare launch applies everywhere and becomes an app path on Chromium. The map takes `native:`, `ios:`, `android:`, `vega:`, and `chromium:`. `native:` is one id shared by iOS, Android, and Vega, and a per-platform key overrides it for that platform. `chromium:` accepts a relative or absolute app path. A launch that declares no id for the run's platform is an error, not a cue to switch platforms.
108
+ Use the launch map for cross-platform flows. A bare launch applies everywhere and becomes an app path on Chromium. The map takes `native:`, `ios:`, `android:`, `vega:`, and `chromium:`. `native:` is one id shared by iOS, Android, and Vega, and a per-platform key overrides it for that platform. `chromium:` accepts a relative or absolute app path. A launch that declares no id for the run's platform is an error, not a cue to switch platforms. On iOS, a successful launch also pins later tree reads to that app until the next raw `tool:` step, so read [The runner tree is not the discovery tree](#the-runner-tree-is-not-the-discovery-tree) when a read describes the wrong screen.
103
109
 
104
110
  ```yaml
105
111
  - launch: { native: com.acme.app, chromium: ../../app }
@@ -53,9 +53,9 @@ Use the same explicit UDID throughout. Multiple booted simulators are not an inj
53
53
 
54
54
  This fallback applies only to `com.apple.*` system apps. A connection failure in another app never authorizes it.
55
55
 
56
- Apple system apps are platform binaries with library validation, so the instrumentation cannot be relied on to load into them it has been seen both loading and not loading, depending on the simulator runtime. Either way it is no basis for a selector.
56
+ Argent refuses `com.apple.*` bundle ids at every native-devtools read that names one, because a system app is never the app under test. The instrumentation has been seen both loading and not loading into one, depending on the simulator runtime either way it is no basis for a selector. `restart-app`, `launch-app`, and `describe` still work on one; it just never gets a flow tree.
57
57
 
58
- Give the flow a `launch:` step as usual. On iOS the launch waits the full devtools budget out, then passes for one of these bundle ids: starting the app is all that step is for, and a coordinate-driven flow needs nothing more. The flow stays e2e; it just pays roughly sixteen seconds at the launch. Where the impossibility bites is selector resolution, and the first selector step reports it there — terminally, naming the coordinate remedy — rather than as a launch failure. The rest of the injection-free form:
58
+ Give the flow a `launch:` step as usual. On iOS the launch waits the full devtools budget out, then passes for one of these bundle ids: starting the app is all that step is for, and a coordinate-driven flow needs nothing more. The flow stays e2e; it just pays roughly sixteen seconds at the launch. Where the refusal bites is selector resolution, and the first selector step reports it there — terminally, naming the coordinate remedy — rather than as a launch failure. The rest of the tree-free form:
59
59
 
60
60
  - Raw `tool: await-ui-element` accessibility checks.
61
61
  - Point taps or long-presses derived from `describe`, each named by an echo.
@@ -66,9 +66,9 @@ Every point tap or long-press in such a flow passes **carrying a warning** for a
66
66
 
67
67
  A recorded wait carries a different warning: it adds about one second and reports that the runner tree is unavailable. That warning is expected too. Keep the wait as a raw `tool:` step.
68
68
 
69
- Report that the flow is injection-free and its coordinates are not portable. It cannot satisfy the QA contract. Report the artifact and platform blocker instead.
69
+ Report that the flow has no flow tree and its coordinates are not portable. It cannot satisfy the QA contract. Report the artifact and platform blocker instead.
70
70
 
71
- A normally injectable app that is broken in the environment gets the same coordinate-only treatment, but not the same launch: there the `launch:` step fails, since the gate withholds its verdict only for a bundle id injection may never reach. Start such a flow with a raw `tool: restart-app`, which terminates and relaunches without the readiness gate, and accept that the result is a **fragment** — its first non-echo step is not `launch:`, so the runner never classifies it as e2e, and it cannot complete `argent-qa-flows`, which requires a leading `launch:`. Report the blocker rather than labeling that fallback a completed QA test.
71
+ A normally injectable app that is broken in the environment gets the same coordinate-only treatment, but not the same launch: there the `launch:` step fails, since the gate withholds its verdict only for a bundle id argent refuses outright. Start such a flow with a raw `tool: restart-app`, which terminates and relaunches without the readiness gate, and accept that the result is a **fragment** — its first non-echo step is not `launch:`, so the runner never classifies it as e2e, and it cannot complete `argent-qa-flows`, which requires a leading `launch:`. Report the blocker rather than labeling that fallback a completed QA test.
72
72
 
73
73
  ## Tree source recovery on Android, Chromium, and Vega
74
74