@zenalexa/unicli 0.217.3 → 0.218.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (149) hide show
  1. package/AGENTS.md +51 -194
  2. package/README.md +2 -2
  3. package/README.zh-CN.md +2 -2
  4. package/dist/bin/unicli-mcp.d.ts +10 -0
  5. package/dist/bin/unicli-mcp.d.ts.map +1 -0
  6. package/dist/bin/unicli-mcp.js +10 -0
  7. package/dist/bin/unicli-mcp.js.map +1 -0
  8. package/dist/cli.d.ts.map +1 -1
  9. package/dist/cli.js +12 -1
  10. package/dist/cli.js.map +1 -1
  11. package/dist/commands/auth.d.ts +1 -0
  12. package/dist/commands/auth.d.ts.map +1 -1
  13. package/dist/commands/auth.js +250 -0
  14. package/dist/commands/auth.js.map +1 -1
  15. package/dist/commands/dev.js +1 -0
  16. package/dist/commands/dev.js.map +1 -1
  17. package/dist/commands/health.d.ts.map +1 -1
  18. package/dist/commands/health.js +1 -0
  19. package/dist/commands/health.js.map +1 -1
  20. package/dist/commands/lint.d.ts.map +1 -1
  21. package/dist/commands/lint.js +15 -6
  22. package/dist/commands/lint.js.map +1 -1
  23. package/dist/commands/repair.d.ts +1 -0
  24. package/dist/commands/repair.d.ts.map +1 -1
  25. package/dist/commands/repair.js +103 -61
  26. package/dist/commands/repair.js.map +1 -1
  27. package/dist/engine/chromium-cookies-platform.d.ts +69 -0
  28. package/dist/engine/chromium-cookies-platform.d.ts.map +1 -0
  29. package/dist/engine/chromium-cookies-platform.js +315 -0
  30. package/dist/engine/chromium-cookies-platform.js.map +1 -0
  31. package/dist/engine/chromium-cookies-types.d.ts +26 -0
  32. package/dist/engine/chromium-cookies-types.d.ts.map +1 -0
  33. package/dist/engine/chromium-cookies-types.js +16 -0
  34. package/dist/engine/chromium-cookies-types.js.map +1 -0
  35. package/dist/engine/chromium-cookies.d.ts +56 -0
  36. package/dist/engine/chromium-cookies.d.ts.map +1 -0
  37. package/dist/engine/chromium-cookies.js +361 -0
  38. package/dist/engine/chromium-cookies.js.map +1 -0
  39. package/dist/engine/cookies.d.ts +13 -5
  40. package/dist/engine/cookies.d.ts.map +1 -1
  41. package/dist/engine/cookies.js +55 -9
  42. package/dist/engine/cookies.js.map +1 -1
  43. package/dist/engine/executor.d.ts +8 -0
  44. package/dist/engine/executor.d.ts.map +1 -1
  45. package/dist/engine/executor.js +1 -1
  46. package/dist/engine/executor.js.map +1 -1
  47. package/dist/engine/kernel/execute.d.ts.map +1 -1
  48. package/dist/engine/kernel/execute.js +1 -0
  49. package/dist/engine/kernel/execute.js.map +1 -1
  50. package/dist/engine/repair/quarantine-discovery.d.ts +25 -0
  51. package/dist/engine/repair/quarantine-discovery.d.ts.map +1 -0
  52. package/dist/engine/repair/quarantine-discovery.js +78 -0
  53. package/dist/engine/repair/quarantine-discovery.js.map +1 -0
  54. package/dist/engine/steps/navigate.d.ts +1 -1
  55. package/dist/engine/steps/navigate.d.ts.map +1 -1
  56. package/dist/engine/steps/navigate.js +5 -1
  57. package/dist/engine/steps/navigate.js.map +1 -1
  58. package/dist/engine/template.d.ts +14 -4
  59. package/dist/engine/template.d.ts.map +1 -1
  60. package/dist/engine/template.js +93 -65
  61. package/dist/engine/template.js.map +1 -1
  62. package/dist/engine/verify-row-shape.d.ts +17 -0
  63. package/dist/engine/verify-row-shape.d.ts.map +1 -0
  64. package/dist/engine/verify-row-shape.js +36 -0
  65. package/dist/engine/verify-row-shape.js.map +1 -0
  66. package/dist/fast-path/handlers/adapter.d.ts +15 -0
  67. package/dist/fast-path/handlers/adapter.d.ts.map +1 -0
  68. package/dist/fast-path/handlers/adapter.js +169 -0
  69. package/dist/fast-path/handlers/adapter.js.map +1 -0
  70. package/dist/fast-path/handlers/discovery.d.ts +14 -0
  71. package/dist/fast-path/handlers/discovery.d.ts.map +1 -0
  72. package/dist/fast-path/handlers/discovery.js +280 -0
  73. package/dist/fast-path/handlers/discovery.js.map +1 -0
  74. package/dist/fast-path/manifest.d.ts +47 -0
  75. package/dist/fast-path/manifest.d.ts.map +1 -0
  76. package/dist/fast-path/manifest.js +32 -0
  77. package/dist/fast-path/manifest.js.map +1 -0
  78. package/dist/fast-path/parsed-argv.d.ts +16 -0
  79. package/dist/fast-path/parsed-argv.d.ts.map +1 -0
  80. package/dist/fast-path/parsed-argv.js +6 -0
  81. package/dist/fast-path/parsed-argv.js.map +1 -0
  82. package/dist/fast-path/policy.d.ts +25 -0
  83. package/dist/fast-path/policy.d.ts.map +1 -0
  84. package/dist/fast-path/policy.js +96 -0
  85. package/dist/fast-path/policy.js.map +1 -0
  86. package/dist/fast-path/render.d.ts +26 -0
  87. package/dist/fast-path/render.d.ts.map +1 -0
  88. package/dist/fast-path/render.js +200 -0
  89. package/dist/fast-path/render.js.map +1 -0
  90. package/dist/fast-path.d.ts +8 -10
  91. package/dist/fast-path.d.ts.map +1 -1
  92. package/dist/fast-path.js +66 -810
  93. package/dist/fast-path.js.map +1 -1
  94. package/dist/manifest.json +7 -6
  95. package/dist/transport/adapters/cdp-browser.d.ts +6 -6
  96. package/dist/transport/adapters/cdp-browser.js +7 -7
  97. package/dist/transport/adapters/cdp-browser.js.map +1 -1
  98. package/dist/transport/adapters/subprocess.d.ts +4 -4
  99. package/dist/transport/adapters/subprocess.js +4 -4
  100. package/package.json +22 -8
  101. package/server.json +44 -0
  102. package/skills/README.md +39 -0
  103. package/skills/talk-normal/SKILL.md +25 -0
  104. package/skills/talk-normal/prompt.md +35 -0
  105. package/skills/unicli/SKILL.md +389 -0
  106. package/skills/unicli/references/auth.md +188 -0
  107. package/skills/unicli/references/output.md +238 -0
  108. package/skills/unicli/references/sites.md +259 -0
  109. package/skills/unicli-browser/SKILL.md +99 -0
  110. package/skills/unicli-claude-code/SKILL.md +172 -0
  111. package/skills/unicli-explorer/SKILL.md +90 -0
  112. package/skills/unicli-hermes/SKILL.md +83 -0
  113. package/skills/unicli-oneshot/SKILL.md +59 -0
  114. package/skills/unicli-operate/SKILL.md +113 -0
  115. package/skills/unicli-repair/SKILL.md +209 -0
  116. package/skills/unicli-repair/references/error-codes.md +149 -0
  117. package/skills/unicli-repair/references/yaml-patches.md +321 -0
  118. package/skills/unicli-smart-search/SKILL.md +80 -0
  119. package/skills/unicli-usage/SKILL.md +65 -0
  120. package/src/adapters/adguardhome/rules.yaml +1 -1
  121. package/src/adapters/adguardhome/stats.yaml +1 -1
  122. package/src/adapters/adguardhome/status.yaml +1 -1
  123. package/src/adapters/apple-music/rate-album.yaml +1 -2
  124. package/src/adapters/arxiv/trending.yaml +1 -1
  125. package/src/adapters/az/account.yaml +1 -1
  126. package/src/adapters/coupang/hot.yaml +2 -1
  127. package/src/adapters/ctrip/hot.yaml +2 -1
  128. package/src/adapters/ctrip/search.yaml +2 -1
  129. package/src/adapters/douban/top250.yaml +1 -1
  130. package/src/adapters/figma/export-selected.yaml +1 -2
  131. package/src/adapters/gcloud/projects.yaml +1 -1
  132. package/src/adapters/github-trending/developers.yaml +1 -1
  133. package/src/adapters/github-trending/weekly.yaml +1 -1
  134. package/src/adapters/homebrew/search.yaml +1 -1
  135. package/src/adapters/imdb/top.yaml +1 -1
  136. package/src/adapters/itch-io/popular.yaml +1 -1
  137. package/src/adapters/itch-io/top.yaml +1 -1
  138. package/src/adapters/mastodon/timeline.yaml +1 -1
  139. package/src/adapters/openrouter/search.yaml +1 -1
  140. package/src/adapters/pypi/search.yaml +2 -1
  141. package/src/adapters/sspai/hot.yaml +4 -1
  142. package/src/adapters/sspai/latest.yaml +3 -3
  143. package/src/adapters/tieba/hot.yaml +1 -1
  144. package/src/adapters/wikipedia/today.yaml +1 -1
  145. package/src/adapters/zoom/toggle-mute.yaml +1 -2
  146. package/dist/engine/yaml-runner.d.ts +0 -11
  147. package/dist/engine/yaml-runner.d.ts.map +0 -1
  148. package/dist/engine/yaml-runner.js +0 -18
  149. package/dist/engine/yaml-runner.js.map +0 -1
package/dist/fast-path.js CHANGED
@@ -1,303 +1,83 @@
1
1
  /**
2
- * Fast-path handlers for discovery-only commands.
3
- *
4
- * The full Commander tree loads every adapter before it can dispatch. That is
5
- * correct for execution commands, but wasteful for discovery surfaces that can
6
- * be answered from the generated manifest and search index.
2
+ * @owner src/fast-path.ts
3
+ * @does Argv parsing and dispatch for discovery surfaces (list/search/describe/repair, plus pre-Commander adapter dry-run, policy gate, and site help).
4
+ * @needs ./fast-path/{manifest,parsed-argv,render,policy,handlers/discovery,handlers/adapter}, ./types (OutputFormat)
5
+ * @feeds src/main.ts (dispatched before the full Commander tree loads)
6
+ * @breaks Missing dist/manifest.json returns false so caller falls through to Commander; structured errors propagate via emitStderrAndExit.
7
7
  */
8
- import { existsSync, readFileSync, writeSync } from "node:fs";
9
- import { dirname, join } from "node:path";
10
- import { fileURLToPath } from "node:url";
11
- import { search } from "./discovery/search.js";
12
- import { buildMacosDynamicCommands, discoverMacosDynamicData, dynamicMacosDiscoveryEnabled, } from "./discovery/macos-dynamic.js";
13
- import { createApprovalStore, isStoredApproval, } from "./engine/approval-store.js";
14
- import { buildDefaultConfig } from "./engine/repair/config.js";
15
- import { evaluateOperationPolicy, InvalidPermissionProfileError, resolveOperationAdapterPath, resolveOperationTargetSurface, } from "./engine/operation-policy.js";
16
- import { applyDenyRuleToPolicy, findDenyRuleForPolicySync, PermissionRulesConfigError, } from "./engine/permission-rules.js";
17
- import { defaultErrorNextActions } from "./output/next-actions.js";
18
- import { format, detectFormat } from "./output/formatter.js";
19
- import { ExitCode } from "./types.js";
20
- const DEFAULT_IO = {
21
- stdout: (text) => process.stdout.write(`${text}\n`),
22
- stderr: (text) => process.stderr.write(`${text}\n`),
23
- };
24
- function emitStderrAndExit(io, text, code) {
25
- if (io === DEFAULT_IO) {
26
- writeSync(process.stderr.fd, `${text}\n`);
27
- process.exit(code);
28
- }
29
- io.stderr(text);
30
- process.exitCode = code;
31
- }
32
- function isOutputFormat(value) {
33
- return (value === "json" ||
34
- value === "yaml" ||
35
- value === "md" ||
36
- value === "csv" ||
37
- value === "compact" ||
38
- value === "table");
39
- }
40
- function isHelpToken(value) {
41
- return value === "-h" || value === "--help" || value === "help";
42
- }
43
- function jsonSchemaType(type) {
44
- switch (type) {
45
- case "int":
46
- return "integer";
47
- case "float":
48
- return "number";
49
- case "bool":
50
- return "boolean";
51
- default:
52
- return "string";
53
- }
54
- }
55
- function argsToJsonSchema(args) {
56
- const properties = {};
57
- const required = [];
58
- for (const arg of args) {
59
- const prop = {
60
- type: jsonSchemaType(arg.type),
61
- };
62
- if (arg.description)
63
- prop.description = arg.description;
64
- if (arg.default !== undefined)
65
- prop.default = arg.default;
66
- if (arg.choices && arg.choices.length > 0)
67
- prop.enum = arg.choices;
68
- if (arg.format)
69
- prop.format = arg.format;
70
- if (arg["x-unicli-kind"])
71
- prop["x-unicli-kind"] = arg["x-unicli-kind"];
72
- if (arg["x-unicli-accepts"]) {
73
- prop["x-unicli-accepts"] = arg["x-unicli-accepts"];
8
+ import { handleAdapterDryRun, handleAdapterPolicyGate, handleSiteHelp, } from "./fast-path/handlers/adapter.js";
9
+ import { handleDescribe, handleList, handleRepair, handleSearch, } from "./fast-path/handlers/discovery.js";
10
+ import { isMissingManifestError } from "./fast-path/manifest.js";
11
+ import { DEFAULT_IO, isOutputFormat } from "./fast-path/render.js";
12
+ /**
13
+ * Try to consume one global flag at `args[i]`. Returns the new index
14
+ * (i + skipped tokens) or `null` when the arg is not a recognized flag.
15
+ * The single source of truth adding a flag means editing one branch
16
+ * here, not two parallel passes (rule 02 third-copy halt).
17
+ */
18
+ function tryConsumeFlag(args, i, acc) {
19
+ const arg = args[i];
20
+ if (arg === "-f" || arg === "--format") {
21
+ const next = args[i + 1];
22
+ if (next && isOutputFormat(next)) {
23
+ acc.formatValue = next;
24
+ return i + 1;
74
25
  }
75
- properties[arg.name] = prop;
76
- if (arg.required)
77
- required.push(arg.name);
26
+ return null;
78
27
  }
79
- return {
80
- $schema: "https://json-schema.org/draft/2020-12/schema",
81
- type: "object",
82
- properties,
83
- required,
84
- additionalProperties: true,
85
- };
86
- }
87
- function buildExample(args) {
88
- const example = {};
89
- for (const arg of args) {
90
- if (arg.default !== undefined) {
91
- example[arg.name] = arg.default;
92
- continue;
93
- }
94
- if (arg.choices && arg.choices.length > 0) {
95
- example[arg.name] = arg.choices[0];
96
- continue;
97
- }
98
- switch (arg.type) {
99
- case "int":
100
- example[arg.name] = 10;
101
- break;
102
- case "float":
103
- example[arg.name] = 0.5;
104
- break;
105
- case "bool":
106
- example[arg.name] = false;
107
- break;
108
- default:
109
- example[arg.name] = `<${arg.name}>`;
28
+ if (arg.startsWith("--format=")) {
29
+ const next = arg.slice("--format=".length);
30
+ if (isOutputFormat(next)) {
31
+ acc.formatValue = next;
32
+ return i;
110
33
  }
34
+ return null;
111
35
  }
112
- return example;
113
- }
114
- function buildChannels(site, command, args) {
115
- const positionals = args
116
- .filter((arg) => arg.positional)
117
- .map((arg) => (arg.required ? `<${arg.name}>` : `[${arg.name}]`))
118
- .join(" ");
119
- const options = args
120
- .filter((arg) => !arg.positional)
121
- .map((arg) => `[--${arg.name} <${arg.type ?? "value"}>]`)
122
- .join(" ");
123
- const shell = `unicli ${site} ${command}` +
124
- (positionals ? ` ${positionals}` : "") +
125
- (options ? ` ${options}` : "");
126
- return {
127
- shell: shell.trim(),
128
- args_file: `unicli ${site} ${command} --args-file <path.json>`,
129
- stdin: `echo '{...}' | unicli ${site} ${command}`,
130
- };
131
- }
132
- function summarizeArgs(args = []) {
133
- return args.map((arg) => ({
134
- name: arg.name,
135
- type: arg.type ?? "str",
136
- required: arg.required === true,
137
- positional: arg.positional === true,
138
- }));
139
- }
140
- function coerceArgValue(value, type) {
141
- if (type === "int") {
142
- const parsed = parseInt(String(value), 10);
143
- return Number.isNaN(parsed) ? value : parsed;
36
+ if (arg === "--dry-run") {
37
+ acc.dryRun = true;
38
+ return i;
144
39
  }
145
- if (type === "float") {
146
- const parsed = parseFloat(String(value));
147
- return Number.isNaN(parsed) ? value : parsed;
40
+ if (arg === "--permission-profile") {
41
+ acc.permissionProfile = args[i + 1];
42
+ return i + 1;
148
43
  }
149
- if (type === "bool") {
150
- if (typeof value === "boolean")
151
- return value;
152
- const text = String(value).toLowerCase();
153
- return text === "1" || text === "true" || text === "yes";
44
+ if (arg.startsWith("--permission-profile=")) {
45
+ acc.permissionProfile = arg.slice("--permission-profile=".length);
46
+ return i;
154
47
  }
155
- return value;
156
- }
157
- function resolveDryRunArgs(schema = [], tokens) {
158
- const values = {};
159
- for (const arg of schema) {
160
- if (arg.default !== undefined)
161
- values[arg.name] = arg.default;
48
+ if (arg === "--yes") {
49
+ acc.yes = true;
50
+ return i;
162
51
  }
163
- const positionals = schema.filter((arg) => arg.positional);
164
- let positionalIndex = 0;
165
- for (let i = 0; i < tokens.length; i += 1) {
166
- const token = tokens[i];
167
- if (token.startsWith("--")) {
168
- const eqIdx = token.indexOf("=");
169
- const name = eqIdx === -1 ? token.slice(2) : token.slice(2, Math.max(2, eqIdx));
170
- const arg = schema.find((candidate) => candidate.name === name);
171
- if (!arg) {
172
- if (eqIdx === -1 && tokens[i + 1] && !tokens[i + 1].startsWith("-")) {
173
- i += 1;
174
- }
175
- continue;
176
- }
177
- let raw;
178
- if (arg.type === "bool" && eqIdx === -1) {
179
- raw = true;
180
- }
181
- else if (eqIdx !== -1) {
182
- raw = token.slice(eqIdx + 1);
183
- }
184
- else {
185
- raw = tokens[i + 1];
186
- i += 1;
187
- }
188
- values[arg.name] = coerceArgValue(raw, arg.type);
189
- continue;
190
- }
191
- const arg = positionals[positionalIndex];
192
- if (arg) {
193
- values[arg.name] = coerceArgValue(token, arg.type);
194
- positionalIndex += 1;
195
- }
52
+ if (arg === "--remember-approval") {
53
+ acc.rememberApproval = true;
54
+ return i;
196
55
  }
197
- for (const arg of schema) {
198
- if (arg.required && values[arg.name] === undefined)
199
- return null;
56
+ if (arg === "--record") {
57
+ acc.record = true;
58
+ return i;
200
59
  }
201
- return values;
60
+ return null;
202
61
  }
203
62
  function parseArgv(argv) {
204
63
  const args = argv.slice(2);
205
- let formatValue;
206
- let dryRun = false;
207
- let permissionProfile;
208
- let yes = false;
209
- let rememberApproval = false;
210
- let record = false;
64
+ const acc = {
65
+ dryRun: false,
66
+ yes: false,
67
+ rememberApproval: false,
68
+ record: false,
69
+ };
211
70
  let command;
212
71
  const rest = [];
213
72
  for (let i = 0; i < args.length; i += 1) {
214
- const arg = args[i];
215
- if (!command) {
216
- if (arg === "-f" || arg === "--format") {
217
- const next = args[i + 1];
218
- if (next && isOutputFormat(next)) {
219
- formatValue = next;
220
- i += 1;
221
- continue;
222
- }
223
- }
224
- if (arg.startsWith("--format=")) {
225
- const next = arg.slice("--format=".length);
226
- if (isOutputFormat(next)) {
227
- formatValue = next;
228
- continue;
229
- }
230
- }
231
- if (arg === "--dry-run") {
232
- dryRun = true;
233
- continue;
234
- }
235
- if (arg === "--permission-profile") {
236
- permissionProfile = args[i + 1];
237
- i += 1;
238
- continue;
239
- }
240
- if (arg.startsWith("--permission-profile=")) {
241
- permissionProfile = arg.slice("--permission-profile=".length);
242
- continue;
243
- }
244
- if (arg === "--yes") {
245
- yes = true;
246
- continue;
247
- }
248
- if (arg === "--remember-approval") {
249
- rememberApproval = true;
250
- continue;
251
- }
252
- if (arg === "--record") {
253
- record = true;
254
- continue;
255
- }
256
- if (!arg.startsWith("-")) {
257
- command = arg;
258
- continue;
259
- }
260
- rest.push(arg);
261
- continue;
262
- }
263
- if (arg === "-f" || arg === "--format") {
264
- const next = args[i + 1];
265
- if (next && isOutputFormat(next)) {
266
- formatValue = next;
267
- i += 1;
268
- continue;
269
- }
270
- }
271
- if (arg.startsWith("--format=")) {
272
- const next = arg.slice("--format=".length);
273
- if (isOutputFormat(next)) {
274
- formatValue = next;
275
- continue;
276
- }
277
- }
278
- if (arg === "--dry-run") {
279
- dryRun = true;
280
- continue;
281
- }
282
- if (arg === "--permission-profile") {
283
- permissionProfile = args[i + 1];
284
- i += 1;
73
+ const consumed = tryConsumeFlag(args, i, acc);
74
+ if (consumed !== null) {
75
+ i = consumed;
285
76
  continue;
286
77
  }
287
- if (arg.startsWith("--permission-profile=")) {
288
- permissionProfile = arg.slice("--permission-profile=".length);
289
- continue;
290
- }
291
- if (arg === "--yes") {
292
- yes = true;
293
- continue;
294
- }
295
- if (arg === "--remember-approval") {
296
- rememberApproval = true;
297
- continue;
298
- }
299
- if (arg === "--record") {
300
- record = true;
78
+ const arg = args[i];
79
+ if (!command && !arg.startsWith("-")) {
80
+ command = arg;
301
81
  continue;
302
82
  }
303
83
  rest.push(arg);
@@ -305,538 +85,14 @@ function parseArgv(argv) {
305
85
  return {
306
86
  command,
307
87
  rest,
308
- format: formatValue,
309
- dryRun,
310
- permissionProfile,
311
- yes,
312
- rememberApproval,
313
- record,
88
+ format: acc.formatValue,
89
+ dryRun: acc.dryRun,
90
+ permissionProfile: acc.permissionProfile,
91
+ yes: acc.yes,
92
+ rememberApproval: acc.rememberApproval,
93
+ record: acc.record,
314
94
  };
315
95
  }
316
- function manifestPath() {
317
- const here = dirname(fileURLToPath(import.meta.url));
318
- const candidates = [
319
- join(here, "manifest.json"),
320
- join(here, "..", "dist", "manifest.json"),
321
- join(here, "..", "..", "dist", "manifest.json"),
322
- ];
323
- const found = candidates.find((candidate) => existsSync(candidate));
324
- if (!found) {
325
- throw new Error("Missing dist/manifest.json. Run: npm run build:manifest");
326
- }
327
- return found;
328
- }
329
- function readManifest() {
330
- return JSON.parse(readFileSync(manifestPath(), "utf8"));
331
- }
332
- function isMissingManifestError(error) {
333
- return (error instanceof Error &&
334
- error.message.includes("Missing dist/manifest.json"));
335
- }
336
- function emit(io, data, columns, fmt, command, startedAt) {
337
- io.stdout(format(data, columns, detectFormat(fmt), {
338
- command,
339
- duration_ms: Date.now() - startedAt,
340
- surface: "web",
341
- }));
342
- }
343
- function evaluateManifestOperationPolicy(input) {
344
- try {
345
- const policyInput = {
346
- site: input.site,
347
- command: input.commandName,
348
- description: input.command.description,
349
- adapterType: input.adapterType,
350
- targetSurface: input.targetSurface,
351
- strategy: input.command.strategy,
352
- domain: input.command.domain,
353
- base: input.command.base,
354
- browser: input.command.browser === true,
355
- args: input.command.args,
356
- profile: input.parsed.permissionProfile,
357
- approved: input.parsed.yes,
358
- };
359
- const policy = evaluateOperationPolicy(policyInput);
360
- const denyRule = findDenyRuleForPolicySync(policy);
361
- if (denyRule)
362
- return applyDenyRuleToPolicy(policy, denyRule);
363
- if (policy.enforcement === "needs_approval" &&
364
- hasStoredApproval(policy.approval_memory.key)) {
365
- return evaluateOperationPolicy({
366
- ...policyInput,
367
- approvalSource: "memory",
368
- });
369
- }
370
- return policy;
371
- }
372
- catch (error) {
373
- if (!(error instanceof InvalidPermissionProfileError ||
374
- error instanceof PermissionRulesConfigError)) {
375
- throw error;
376
- }
377
- emitStderrAndExit(input.io, format([], input.command.columns, detectFormat(input.parsed.format), {
378
- command: `${input.site}.${input.commandName}`,
379
- duration_ms: Date.now() - input.startedAt,
380
- surface: input.targetSurface,
381
- error: {
382
- code: "invalid_input",
383
- message: error.message,
384
- adapter_path: input.adapterPath,
385
- step: 0,
386
- suggestion: error instanceof PermissionRulesConfigError
387
- ? error.suggestion
388
- : "use one of: open, confirm, locked",
389
- retryable: false,
390
- },
391
- next_actions: defaultErrorNextActions(input.site, input.commandName, "invalid_input"),
392
- }), ExitCode.USAGE_ERROR);
393
- return null;
394
- }
395
- }
396
- function hasStoredApproval(key) {
397
- const store = createApprovalStore();
398
- if (!existsSync(store.path))
399
- return false;
400
- try {
401
- const raw = readFileSync(store.path, "utf-8");
402
- const lines = raw.split(/\r?\n/);
403
- for (let i = lines.length - 1; i >= 0; i -= 1) {
404
- const line = lines[i];
405
- if (line.trim().length === 0)
406
- continue;
407
- try {
408
- const entry = JSON.parse(line);
409
- if (isStoredApproval(entry) && entry.key === key) {
410
- return entry.decision === "allow";
411
- }
412
- }
413
- catch {
414
- continue;
415
- }
416
- }
417
- return false;
418
- }
419
- catch {
420
- return false;
421
- }
422
- }
423
- function handleList(parsed, io) {
424
- const startedAt = Date.now();
425
- let siteFilter;
426
- let typeFilter;
427
- for (let i = 0; i < parsed.rest.length; i += 1) {
428
- const arg = parsed.rest[i];
429
- if (arg === "--site") {
430
- siteFilter = parsed.rest[i + 1];
431
- i += 1;
432
- continue;
433
- }
434
- if (arg.startsWith("--site=")) {
435
- siteFilter = arg.slice("--site=".length);
436
- continue;
437
- }
438
- if (arg === "--type") {
439
- typeFilter = parsed.rest[i + 1];
440
- i += 1;
441
- continue;
442
- }
443
- if (arg.startsWith("--type=")) {
444
- typeFilter = arg.slice("--type=".length);
445
- continue;
446
- }
447
- return false;
448
- }
449
- const manifest = readManifest();
450
- const rows = Object.entries(manifest.sites)
451
- .flatMap(([site, info]) => info.commands.map((command) => {
452
- const strategy = command.strategy ?? "public";
453
- const tags = [];
454
- if (strategy !== "public")
455
- tags.push("[auth]");
456
- if (command.quarantined === true)
457
- tags.push("[quarantined]");
458
- return {
459
- site,
460
- command: command.name,
461
- description: command.description ?? "",
462
- type: command.type ?? "web-api",
463
- auth: tags.join(" "),
464
- };
465
- }))
466
- .concat(dynamicListRows())
467
- .filter((row) => !siteFilter || row.site.includes(siteFilter))
468
- .filter((row) => !typeFilter || row.type === typeFilter)
469
- .sort((a, b) => a.site.localeCompare(b.site) || a.command.localeCompare(b.command));
470
- emit(io, rows, ["site", "command", "description", "type", "auth"], parsed.format, "core.list", startedAt);
471
- return true;
472
- }
473
- function dynamicListRows() {
474
- if (!dynamicMacosDiscoveryEnabled())
475
- return [];
476
- return Object.values(buildMacosDynamicCommands(discoverMacosDynamicData())).map((command) => ({
477
- site: "macos",
478
- command: command.name,
479
- description: command.description ?? "",
480
- type: "desktop",
481
- auth: "",
482
- }));
483
- }
484
- function handleSearch(parsed, io) {
485
- const startedAt = Date.now();
486
- let limit = 8;
487
- let category;
488
- const queryParts = [];
489
- for (let i = 0; i < parsed.rest.length; i += 1) {
490
- const arg = parsed.rest[i];
491
- if (arg === "-n" || arg === "--limit") {
492
- limit = parseInt(parsed.rest[i + 1] ?? "", 10) || 8;
493
- i += 1;
494
- continue;
495
- }
496
- if (arg.startsWith("--limit=")) {
497
- limit = parseInt(arg.slice("--limit=".length), 10) || 8;
498
- continue;
499
- }
500
- if (arg === "--category") {
501
- category = parsed.rest[i + 1];
502
- i += 1;
503
- continue;
504
- }
505
- if (arg.startsWith("--category=")) {
506
- category = arg.slice("--category=".length);
507
- continue;
508
- }
509
- queryParts.push(arg);
510
- }
511
- const query = queryParts.join(" ");
512
- if (!query && !category) {
513
- io.stderr("Usage: unicli search <query> or unicli search --category <cat>");
514
- process.exitCode = 2;
515
- return true;
516
- }
517
- const effectiveQuery = category ? `${category} ${query}`.trim() : query;
518
- const results = search(effectiveQuery, limit);
519
- if (results.length === 0) {
520
- io.stderr(`No commands found for: ${effectiveQuery}`);
521
- process.exitCode = 66;
522
- return true;
523
- }
524
- const rows = results.map((result) => ({
525
- command: `${result.site} ${result.command}`,
526
- description: result.description || `${result.command} for ${result.site}`,
527
- score: result.score,
528
- category: result.category,
529
- usage: result.usage,
530
- }));
531
- emit(io, rows, ["command", "description", "score", "usage"], parsed.format, "core.search", startedAt);
532
- return true;
533
- }
534
- function handleDescribe(parsed, io) {
535
- const startedAt = Date.now();
536
- const manifest = readManifest();
537
- const [site, cmdName] = parsed.rest;
538
- if (!site) {
539
- const sites = Object.entries(manifest.sites).map(([name, info]) => ({
540
- name,
541
- display_name: name,
542
- type: info.commands[0]?.type ?? "web-api",
543
- strategy: info.commands[0]?.strategy ?? "public",
544
- commands_count: info.commands.length,
545
- description: "",
546
- }));
547
- io.stdout(JSON.stringify({ sites, total: sites.length }, null, 2));
548
- return true;
549
- }
550
- const info = manifest.sites[site];
551
- if (!info) {
552
- io.stdout(JSON.stringify({ error: `unknown site: ${site}` }, null, 2));
553
- process.exitCode = 64;
554
- return true;
555
- }
556
- if (!cmdName) {
557
- const commands = info.commands.map((command) => ({
558
- name: command.name,
559
- description: command.description ?? "",
560
- quarantined: command.quarantined === true,
561
- strategy: command.strategy ?? "public",
562
- auth: (command.strategy ?? "public") !== "public",
563
- browser: command.browser === true,
564
- args: summarizeArgs(command.args),
565
- }));
566
- io.stdout(JSON.stringify({
567
- site,
568
- display_name: site,
569
- type: info.commands[0]?.type ?? "web-api",
570
- strategy: info.commands[0]?.strategy ?? "public",
571
- commands,
572
- }, null, 2));
573
- return true;
574
- }
575
- const command = info.commands.find((candidate) => candidate.name === cmdName);
576
- if (!command) {
577
- io.stdout(JSON.stringify({ error: `unknown command: ${site} ${cmdName}` }, null, 2));
578
- process.exitCode = 64;
579
- return true;
580
- }
581
- const adapterType = command.type ?? info.commands[0]?.type ?? "web-api";
582
- const targetSurface = resolveOperationTargetSurface({
583
- adapterType,
584
- targetSurface: command.target_surface,
585
- });
586
- const adapterPath = resolveOperationAdapterPath(site, cmdName, command.adapter_path);
587
- const operationPolicy = evaluateManifestOperationPolicy({
588
- parsed,
589
- io,
590
- site,
591
- commandName: cmdName,
592
- command,
593
- adapterType,
594
- targetSurface,
595
- adapterPath,
596
- startedAt,
597
- });
598
- if (!operationPolicy)
599
- return true;
600
- io.stdout(JSON.stringify({
601
- command: `unicli ${site} ${cmdName}`,
602
- description: command.description ?? "",
603
- quarantined: command.quarantined === true,
604
- strategy: command.strategy ?? "public",
605
- auth: (command.strategy ?? "public") !== "public",
606
- browser: command.browser === true,
607
- target_surface: targetSurface,
608
- adapter_path: adapterPath,
609
- operation_policy: operationPolicy,
610
- args_schema: argsToJsonSchema(command.args ?? []),
611
- example_stdin: buildExample(command.args ?? []),
612
- channels: buildChannels(site, cmdName, command.args ?? []),
613
- next_actions: [
614
- {
615
- command: `unicli ${site} ${cmdName} --dry-run`,
616
- description: "Preview the resolved argument bag and pipeline plan",
617
- },
618
- {
619
- command: `unicli ${site} ${cmdName}`,
620
- description: "Run the command (shell channel)",
621
- params: {
622
- note: {
623
- description: "For payloads with quotes/emoji/JSON, pipe stdin-JSON instead.",
624
- },
625
- },
626
- },
627
- {
628
- command: `unicli repair ${site} ${cmdName}`,
629
- description: "If the command fails due to upstream drift",
630
- },
631
- ],
632
- }, null, 2));
633
- return true;
634
- }
635
- function handleRepair(parsed, io) {
636
- const startedAt = Date.now();
637
- let dryRun = parsed.dryRun;
638
- let max = 20;
639
- let timeout = 90;
640
- const positionals = [];
641
- for (let i = 0; i < parsed.rest.length; i += 1) {
642
- const arg = parsed.rest[i];
643
- if (arg === "--dry-run") {
644
- dryRun = true;
645
- continue;
646
- }
647
- if (arg === "--max") {
648
- max = parseInt(parsed.rest[i + 1] ?? "", 10) || 20;
649
- i += 1;
650
- continue;
651
- }
652
- if (arg.startsWith("--max=")) {
653
- max = parseInt(arg.slice("--max=".length), 10) || 20;
654
- continue;
655
- }
656
- if (arg === "--timeout") {
657
- timeout = parseInt(parsed.rest[i + 1] ?? "", 10) || 90;
658
- i += 1;
659
- continue;
660
- }
661
- if (arg.startsWith("--timeout=")) {
662
- timeout = parseInt(arg.slice("--timeout=".length), 10) || 90;
663
- continue;
664
- }
665
- if (arg.startsWith("-"))
666
- return false;
667
- positionals.push(arg);
668
- }
669
- if (!dryRun)
670
- return false;
671
- const [site, command] = positionals;
672
- if (!site)
673
- return false;
674
- const config = buildDefaultConfig(site, command);
675
- config.maxIterations = max;
676
- config.timeout = timeout * 1000;
677
- emit(io, {
678
- mode: "dry-run",
679
- site,
680
- command: command ?? null,
681
- config: {
682
- ...config,
683
- metricPattern: config.metricPattern.source,
684
- },
685
- }, undefined, parsed.format, "repair.run", startedAt);
686
- return true;
687
- }
688
- function handleAdapterDryRun(parsed, io) {
689
- if (!parsed.command || !parsed.dryRun || parsed.rest.length === 0) {
690
- return false;
691
- }
692
- const manifest = readManifest();
693
- const info = manifest.sites[parsed.command];
694
- if (!info)
695
- return false;
696
- const [cmdName, ...tokens] = parsed.rest;
697
- if (!cmdName || cmdName === "help" || cmdName.startsWith("-"))
698
- return false;
699
- const command = info.commands.find((candidate) => candidate.name === cmdName);
700
- if (!command)
701
- return false;
702
- const startedAt = Date.now();
703
- const args = resolveDryRunArgs(command.args, tokens);
704
- if (!args)
705
- return false;
706
- const adapterType = command.type ?? info.commands[0]?.type ?? "web-api";
707
- const targetSurface = resolveOperationTargetSurface({
708
- adapterType,
709
- targetSurface: command.target_surface,
710
- });
711
- const adapterPath = resolveOperationAdapterPath(parsed.command, cmdName, command.adapter_path);
712
- const operationPolicy = evaluateManifestOperationPolicy({
713
- parsed,
714
- io,
715
- site: parsed.command,
716
- commandName: cmdName,
717
- command,
718
- adapterType,
719
- targetSurface,
720
- adapterPath,
721
- startedAt,
722
- });
723
- if (!operationPolicy)
724
- return true;
725
- io.stdout(JSON.stringify({
726
- command: `${parsed.command}.${cmdName}`,
727
- adapter_type: adapterType,
728
- strategy: command.strategy ?? "public",
729
- args,
730
- args_source: tokens.length > 0 ? "shell" : "defaults",
731
- operation_policy: operationPolicy,
732
- trace_id: `fast-${Date.now().toString(36)}`,
733
- surface: "cli",
734
- target_surface: targetSurface,
735
- pipeline_steps: command.pipeline_steps ?? 0,
736
- adapter_path: adapterPath,
737
- }, null, 2));
738
- return true;
739
- }
740
- function handleAdapterPolicyGate(parsed, io) {
741
- if (!parsed.command || parsed.dryRun || parsed.rest.length === 0) {
742
- return false;
743
- }
744
- const [cmdName] = parsed.rest;
745
- if (!cmdName || cmdName === "help" || cmdName.startsWith("-"))
746
- return false;
747
- if (parsed.rest.slice(1).some(isHelpToken))
748
- return false;
749
- const startedAt = Date.now();
750
- const manifest = readManifest();
751
- const info = manifest.sites[parsed.command];
752
- if (!info)
753
- return false;
754
- const command = info.commands.find((candidate) => candidate.name === cmdName);
755
- if (!command)
756
- return false;
757
- const adapterType = command.type ?? info.commands[0]?.type ?? "web-api";
758
- const targetSurface = resolveOperationTargetSurface({
759
- adapterType,
760
- targetSurface: command.target_surface,
761
- });
762
- const adapterPath = resolveOperationAdapterPath(parsed.command, cmdName, command.adapter_path);
763
- const operationPolicy = evaluateManifestOperationPolicy({
764
- parsed,
765
- io,
766
- site: parsed.command,
767
- commandName: cmdName,
768
- command,
769
- adapterType,
770
- targetSurface,
771
- adapterPath,
772
- startedAt,
773
- });
774
- if (!operationPolicy)
775
- return true;
776
- if (operationPolicy.enforcement !== "needs_approval" &&
777
- operationPolicy.enforcement !== "deny") {
778
- return false;
779
- }
780
- const isDenyRule = operationPolicy.enforcement === "deny";
781
- const ruleId = operationPolicy.deny_rule?.id ?? "unknown";
782
- const ruleReason = operationPolicy.deny_rule?.reason ?? "permission rule matched";
783
- emitStderrAndExit(io, format([], command.columns, detectFormat(parsed.format), {
784
- command: `${parsed.command}.${cmdName}`,
785
- duration_ms: Date.now() - startedAt,
786
- surface: targetSurface,
787
- error: {
788
- code: "permission_denied",
789
- message: isDenyRule
790
- ? `permission rule "${ruleId}" denies ${operationPolicy.effect}: ${ruleReason}`
791
- : `permission profile "${operationPolicy.profile}" requires approval for ${operationPolicy.effect}`,
792
- adapter_path: adapterPath,
793
- step: 0,
794
- suggestion: operationPolicy.approval_hint ??
795
- (isDenyRule
796
- ? "edit or remove the matching permission rule"
797
- : "rerun with --yes or use --permission-profile open"),
798
- retryable: false,
799
- alternatives: isDenyRule
800
- ? [
801
- `unicli --dry-run ${parsed.command} ${cmdName}`,
802
- "edit ~/.unicli/permission-rules.json",
803
- ]
804
- : [
805
- `unicli --dry-run ${parsed.command} ${cmdName}`,
806
- `unicli --yes --permission-profile ${operationPolicy.profile} ${parsed.command} ${cmdName}`,
807
- `unicli --permission-profile open ${parsed.command} ${cmdName}`,
808
- ],
809
- },
810
- next_actions: defaultErrorNextActions(parsed.command, cmdName, "permission_denied"),
811
- }), ExitCode.AUTH_REQUIRED);
812
- return true;
813
- }
814
- function handleSiteHelp(parsed, io) {
815
- const wantsHelp = parsed.rest.length === 0 || parsed.rest.every(isHelpToken);
816
- if (!parsed.command || !wantsHelp)
817
- return false;
818
- const manifest = readManifest();
819
- const info = manifest.sites[parsed.command];
820
- if (!info)
821
- return false;
822
- const commandWidth = Math.max(7, ...info.commands.map((command) => command.name.length));
823
- const lines = [
824
- `Usage: unicli ${parsed.command} [options] [command]`,
825
- "",
826
- `Commands for ${parsed.command}`,
827
- "",
828
- "Options:",
829
- " -h, --help".padEnd(commandWidth + 6) + "display help for command",
830
- "",
831
- "Commands:",
832
- ];
833
- for (const command of info.commands) {
834
- lines.push(` ${command.name.padEnd(commandWidth)} ${command.description ?? ""}`.trimEnd());
835
- }
836
- lines.push(` ${"help [command]".padEnd(commandWidth)} display help for command`);
837
- io.stdout(lines.join("\n"));
838
- return true;
839
- }
840
96
  export function tryRunFastPath(argv = process.argv, io = DEFAULT_IO) {
841
97
  const parsed = parseArgv(argv);
842
98
  try {