ework-aio 0.2.2 → 0.2.4

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ework-aio",
3
- "version": "0.2.2",
3
+ "version": "0.2.4",
4
4
  "description": "All-in-one installer for ework (issue tracker) + ework-daemon (AI bridge) + opencode-ework (plugin). One command: npm i -g ework-aio && ework-aio install.",
5
5
  "type": "module",
6
6
  "license": "MIT",
package/src/cli.ts CHANGED
@@ -35,7 +35,7 @@ import {
35
35
  type ConfigArgs,
36
36
  } from "./commands/config.ts";
37
37
 
38
- const VERSION = "0.2.2-dev";
38
+ const VERSION = "0.2.4-dev";
39
39
 
40
40
  const USAGE = `ework-aio <command> [options]
41
41
 
@@ -592,14 +592,18 @@ async function bootstrapBot(opts: BootstrapBotOpts): Promise<string> {
592
592
  }
593
593
 
594
594
  // 3. Mint PAT via /me/tokens/create. Response is HTML containing the
595
- // clear-text token in `<input id="t" value="<40-hex>">`. We only get one
596
- // chance to see the clear-text must scrape it now.
595
+ // clear-text token. ework-web renders it inside `<code id="t">VALUE</code>`
596
+ // (verified at src/views/tokens.ts:141 in ework-web at time of writing);
597
+ // the legacy install.sh-era HTML used `<input id="t" value="VALUE">`. We
598
+ // scrape both shapes so an ework-web template change doesn't silently
599
+ // break install.
597
600
  //
598
601
  // S-3: pin redirect:"manual" so a PRG-style 303→GET doesn't land us on
599
602
  // a "token list" page where the clear-text value isn't present.
600
603
  //
601
- // S-2: scrape regex is attribute-order-independent accepts both
602
- // `<input id="t" value="...">` and `<input value="..." id="t">`.
604
+ // Token shape: 40 lowercase hex chars (ework-web createPat uses
605
+ // randomHex(20) 20 bytes 40 hex). Case-insensitive `i` flag for
606
+ // XHTML-style `<INPUT VALUE="...">` and uppercase hex (defensive).
603
607
  const patRes = await fetchImpl(`${opts.baseUrl}/me/tokens/create`, {
604
608
  method: "POST",
605
609
  headers: { Cookie: botCookie, "Content-Type": "application/x-www-form-urlencoded" },
@@ -612,14 +616,14 @@ async function bootstrapBot(opts: BootstrapBotOpts): Promise<string> {
612
616
  );
613
617
  }
614
618
  const patBody = await patRes.text();
615
- // S-2: scrape regex is attribute-order-independent — accepts both
616
- // `<input id="t" value="...">` and `<input value="..." id="t">`.
617
- // Case-insensitive (i flag) so XHTML-style `<INPUT VALUE="...">` from
618
- // ework-web template tweaks doesn't silently break scraping.
619
- const match = patBody.match(/<input[^>]*id="t"[^>]*value="([a-f0-9]{40})"/i)
620
- ?? patBody.match(/<input[^>]*value="([a-f0-9]{40})"[^>]*id="t"/i);
619
+ const match =
620
+ patBody.match(/<code[^>]*id="t"[^>]*>\s*([a-f0-9]{40})\s*<\/code>/i) ??
621
+ patBody.match(/<input[^>]*id="t"[^>]*value="([a-f0-9]{40})"/i) ??
622
+ patBody.match(/<input[^>]*value="([a-f0-9]{40})"[^>]*id="t"/i);
621
623
  if (!match || !match[1]) {
622
- throw new InstallError(`could not extract PAT from token-create response`);
624
+ throw new InstallError(
625
+ `could not extract PAT from token-create response (first 300 chars): ${patBody.slice(0, 300)}`,
626
+ );
623
627
  }
624
628
  return match[1];
625
629
  }
@@ -6,10 +6,30 @@ import fs from "node:fs";
6
6
  import path from "node:path";
7
7
  import { InstallError } from "./log.ts";
8
8
 
9
+ // Per https://opencode.ai/config.json (verified v1.14.x), the top-level
10
+ // schema key is "plugin" (singular), an array. Each entry is either a
11
+ // string ("opencode-ework@latest") or a 2-tuple [name, options]. The
12
+ // schema sets additionalProperties: false at top level, so any other key
13
+ // name — including the plausible-looking "plugins" plural — is rejected
14
+ // with "Unrecognized key" and breaks every opencode command that parses
15
+ // config (export, session list, etc).
16
+ //
17
+ // v0.2.0-v0.2.3 wrote the WRONG key ("plugins"). On the user's machine
18
+ // the result was: install completes, but clicking any session in
19
+ // awork-web fails because awork-web calls `opencode export ses_xxx`,
20
+ // opencode parses its config first, and aborts with "Unrecognized key:
21
+ // plugins". This file now writes the correct key, and the reader
22
+ // transparently migrates any legacy "plugins" array into "plugin" so
23
+ // reinstall fixes existing broken configs without manual editing.
9
24
  export interface OpencodeConfig {
10
- // Unknown-key passthrough — preserves fields we don't know about.
11
25
  [key: string]: unknown;
12
- plugins?: Array<string | { name: string; [k: string]: unknown }>;
26
+ // Accepted entry shapes (we are lenient on read):
27
+ // - "name@version" (opencode schema — canonical)
28
+ // - ["name", {...opts}] (opencode schema — 2-tuple form)
29
+ // - { name: "..." } (legacy from earlier ework-aio; opencode
30
+ // rejects this, but we preserve through
31
+ // read+write so users don't lose data)
32
+ plugin?: Array<string | [string, unknown] | { name: string; [k: string]: unknown }>;
13
33
  }
14
34
 
15
35
  export interface ReadConfigResult {
@@ -43,7 +63,7 @@ export async function readConfig(configPath: string): Promise<ReadConfigResult>
43
63
  );
44
64
  }
45
65
 
46
- return { config: parsed as OpencodeConfig, existed: true };
66
+ return { config: migrateLegacyPluginsKey(parsed as OpencodeConfig), existed: true };
47
67
  }
48
68
 
49
69
  export async function writeConfig(configPath: string, config: OpencodeConfig): Promise<void> {
@@ -60,9 +80,42 @@ export async function writeConfig(configPath: string, config: OpencodeConfig): P
60
80
  }
61
81
  }
62
82
 
83
+ // migrateLegacyPluginsKey: if a config has the v0.2.0-v0.2.3 "plugins"
84
+ // (plural) key from our earlier broken installer, merge its contents
85
+ // into the correct "plugin" key and drop the bad key. Idempotent — if
86
+ // the user re-runs install with the fixed version, their config gets
87
+ // healed automatically. Returns the original config object untouched if
88
+ // no migration was needed.
89
+ function migrateLegacyPluginsKey(config: OpencodeConfig): OpencodeConfig {
90
+ const legacy = config.plugins;
91
+ if (!Array.isArray(legacy)) {
92
+ // Either no legacy key, or it's malformed (not an array). Delete
93
+ // either way so we don't persist an unrecognized key back to disk.
94
+ if ("plugins" in config) {
95
+ const next = { ...config };
96
+ delete next.plugins;
97
+ return next;
98
+ }
99
+ return config;
100
+ }
101
+ const next: OpencodeConfig = { ...config };
102
+ delete next.plugins;
103
+ const canonical = Array.isArray(config.plugin) ? [...config.plugin] : [];
104
+ for (const entry of legacy) {
105
+ const name = normalizePluginEntry(entry);
106
+ if (!name) continue;
107
+ const already = canonical.some((e) => normalizePluginEntry(e) === name);
108
+ if (!already) canonical.push(entry);
109
+ }
110
+ next.plugin = canonical;
111
+ return next;
112
+ }
113
+
63
114
  function normalizePluginEntry(entry: unknown): string | null {
64
- // Plugin entries can be either "name" (string) or { name: "...", ...opts }.
65
115
  if (typeof entry === "string") return entry;
116
+ if (Array.isArray(entry) && entry.length >= 1 && typeof entry[0] === "string") {
117
+ return entry[0];
118
+ }
66
119
  if (entry !== null && typeof entry === "object") {
67
120
  const name = (entry as { name?: unknown }).name;
68
121
  if (typeof name === "string") return name;
@@ -71,9 +124,9 @@ function normalizePluginEntry(entry: unknown): string | null {
71
124
  }
72
125
 
73
126
  export function hasPlugin(config: OpencodeConfig, pluginName: string): boolean {
74
- const plugins = config.plugins;
75
- if (!Array.isArray(plugins)) return false;
76
- return plugins.some((p) => normalizePluginEntry(p) === pluginName);
127
+ const list = config.plugin;
128
+ if (!Array.isArray(list)) return false;
129
+ return list.some((p) => normalizePluginEntry(p) === pluginName);
77
130
  }
78
131
 
79
132
  export function ensurePlugin(config: OpencodeConfig, pluginName: string): { config: OpencodeConfig; added: boolean } {
@@ -81,7 +134,7 @@ export function ensurePlugin(config: OpencodeConfig, pluginName: string): { conf
81
134
  return { config, added: false };
82
135
  }
83
136
  const next: OpencodeConfig = { ...config };
84
- next.plugins = [...(Array.isArray(config.plugins) ? config.plugins : []), pluginName];
137
+ next.plugin = [...(Array.isArray(config.plugin) ? config.plugin : []), pluginName];
85
138
  return { config: next, added: true };
86
139
  }
87
140
 
@@ -90,15 +143,17 @@ export function removePlugin(config: OpencodeConfig, pluginName: string): { conf
90
143
  return { config, removed: false };
91
144
  }
92
145
  const next: OpencodeConfig = { ...config };
93
- const plugins = Array.isArray(config.plugins) ? config.plugins : [];
94
- next.plugins = plugins.filter((p) => normalizePluginEntry(p) !== pluginName);
146
+ const list = Array.isArray(config.plugin) ? config.plugin : [];
147
+ next.plugin = list.filter((p) => normalizePluginEntry(p) !== pluginName);
95
148
  return { config: next, removed: true };
96
149
  }
97
150
 
98
151
  export async function ensurePluginInFile(configPath: string, pluginName: string): Promise<{ added: boolean }> {
99
152
  const { config } = await readConfig(configPath);
100
153
  const result = ensurePlugin(config, pluginName);
101
- if (!result.added) return { added: false };
154
+ // Always write readConfig may have migrated a legacy "plugins" key,
155
+ // and we want to persist the corrected shape to disk even if the
156
+ // plugin was already present under the (now-renamed) "plugin" key.
102
157
  await writeConfig(configPath, result.config);
103
- return { added: true };
158
+ return { added: result.added };
104
159
  }