@wefter/opencode 0.2.1 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -2,6 +2,13 @@
2
2
 
3
3
  All notable changes to this project will be documented in this file.
4
4
 
5
+ ## 0.3.0 - 2026-06-05
6
+
7
+ - Added `.wefter/install-manifest.json` generation during `init` and a safe `wefter uninstall` command.
8
+ - Added install manifest schema and documentation for previewing/removing Wefter-managed files.
9
+ - Added roadmap and internal release specs for `delivery-implementation-migration` and `technical-shaping-foundation`.
10
+ - Opted GitHub Actions workflows into Node 24 for JavaScript actions while preserving package test coverage across supported Node versions.
11
+
5
12
  ## 0.2.1 - 2026-06-04
6
13
 
7
14
  Stabilizes the `0.2.x` workflow contracts after the product-shaping release.
package/README.md CHANGED
@@ -13,6 +13,7 @@ package: @wefter/opencode
13
13
  repo: wefter
14
14
  cli: wefter
15
15
  config: wefter.config.json
16
+ install manifest: .wefter/install-manifest.json
16
17
  local workflow files: .wefter/
17
18
  runtime artifacts: .audit/wefter/ for legacy workflows; .wefter/runs/ for product-shaping
18
19
  ```
@@ -68,6 +69,7 @@ wefter docs audit --profile-path docs/audits/lenses.json --passes-per-lens 1 --m
68
69
  wefter profile import --source docs/audits/lenses.json --force
69
70
  wefter docs repair --audit-report .audit/wefter/documentation-audit/<run-id>/final/final-documentation-audit-report.md
70
71
  wefter new-run documentation-audit --passes-per-lens 1 --max-audits 12
72
+ wefter uninstall --dry-run
71
73
  ```
72
74
 
73
75
  ## Default Config
@@ -110,6 +112,7 @@ wefter new-run documentation-audit --passes-per-lens 1 --max-audits 12
110
112
  - `docs audit --profile-path` can use a repository-specific audit profile for one run without changing `wefter.config.json`.
111
113
  - `profile import` validates and copies an existing repository-relative audit profile into the configured Wefter profile path.
112
114
  - `docs repair` writes through a staging directory and requires an existing repository-relative audit report path.
115
+ - `init` writes `.wefter/install-manifest.json` with checksums for installed files; `uninstall` removes only unchanged manifest-recorded files unless `--force` is used.
113
116
  - Paths in `wefter.config.json` must be relative to the target repository and must not contain `..`.
114
117
  - Run names are plain directory names and cannot contain path separators.
115
118
  - `product-shaping` writes versioned product specs under `.wefter/specs/` and runtime runs under `.wefter/runs/product-shaping/` by default.
@@ -119,8 +122,7 @@ wefter new-run documentation-audit --passes-per-lens 1 --max-audits 12
119
122
 
120
123
  ## Product Direction
121
124
 
122
- Next hardening steps after the `0.2.1` stabilization release:
125
+ Next hardening steps after the `0.3.0` install-manifest release:
123
126
 
124
- 1. Add installation manifest/uninstall support.
125
- 2. Continue migration from legacy `work-unit-implementation` naming toward `delivery-implementation`.
126
- 3. Implement `technical-shaping` only after its contract, CLI behavior and OpenCode command are ready.
127
+ 1. Continue migration from legacy `work-unit-implementation` naming toward `delivery-implementation`.
128
+ 2. Implement `technical-shaping` only after its contract, CLI behavior and OpenCode command are ready.
@@ -28,6 +28,15 @@ Validate an installation with:
28
28
  wefter doctor
29
29
  ```
30
30
 
31
+ Preview and remove an installation with:
32
+
33
+ ```bash
34
+ wefter uninstall --dry-run
35
+ wefter uninstall --yes
36
+ ```
37
+
38
+ `uninstall` uses `.wefter/install-manifest.json` and removes only unchanged Wefter-managed files unless `--force` is used. It also removes Wefter commands and watcher ignores from `opencode.json` without deleting unrelated user configuration.
39
+
31
40
  Import an existing repository-specific documentation audit profile, such as a legacy `docs/audits/lenses.json`, with:
32
41
 
33
42
  ```bash
@@ -0,0 +1,48 @@
1
+ # Roadmap
2
+
3
+ Wefter development should move in small workflow releases. Each release must preserve installed-project compatibility unless a migration path is explicit.
4
+
5
+ ## Current Foundation
6
+
7
+ - `product-shaping` is available and produces `DELIVERABLES.md` as the product handoff.
8
+ - `work-unit-implementation` remains the executable implementation engine under legacy vocabulary.
9
+ - `technical-shaping` is registered as planned metadata only; it must not install commands until implemented.
10
+ - `init` records installed files in `.wefter/install-manifest.json`; `uninstall` removes manifest-recorded files safely.
11
+
12
+ ## Next Release Order
13
+
14
+ 1. `delivery-implementation-migration`
15
+ 2. `technical-shaping-foundation`
16
+ 3. CLI modularization and schema validation hardening
17
+
18
+ ## Delivery Implementation Migration
19
+
20
+ Goal: migrate vocabulary and defaults from `work-unit-implementation` toward `delivery-implementation` without breaking existing installations.
21
+
22
+ Required decisions:
23
+
24
+ - Whether `DELIVERABLES.md` becomes the default implementation source document.
25
+ - Which legacy command aliases remain and for how long.
26
+ - Whether schemas are renamed, aliased or versioned in place.
27
+ - How OpenCode agent names transition without invalidating existing configs.
28
+
29
+ Non-goals:
30
+
31
+ - Do not remove `work-unit` commands in the first migration release.
32
+ - Do not change product-shaping responsibilities.
33
+ - Do not create technical design artifacts in delivery implementation.
34
+
35
+ ## Technical Shaping Foundation
36
+
37
+ Goal: define a workflow between product-shaped deliverables and delivery implementation.
38
+
39
+ Required outputs:
40
+
41
+ - Technical decisions and constraints.
42
+ - Data contracts and interface expectations.
43
+ - Verification strategy for delivery implementation.
44
+ - Explicit human gates for architecture, security, persistence and migration decisions.
45
+
46
+ Activation rule:
47
+
48
+ `technical-shaping` commands stay in `plannedCommands` until the workflow has process docs, config/profile defaults, prompt templates, agents, schemas, CLI run generation and validation tests.
@@ -11,6 +11,7 @@ Core rules:
11
11
  - Versioned workflow configuration is written under `.wefter/` by default.
12
12
  - Paths are target-repository relative and must not contain `..`.
13
13
  - Run directories are staged before becoming visible as final runs.
14
+ - Installations write `.wefter/install-manifest.json`; uninstall removes manifest-recorded files only when unchanged unless `--force` is explicit.
14
15
  - OpenCode agent permissions restrict write access to configured artifact paths.
15
16
  - Implementation work must be task-level, reviewed and validated before moving to the next work unit.
16
17
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wefter/opencode",
3
- "version": "0.2.1",
3
+ "version": "0.3.0",
4
4
  "description": "Installable Wefter workflows for OpenCode projects.",
5
5
  "type": "module",
6
6
  "repository": {
@@ -0,0 +1,42 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "$id": "https://wefter.dev/schemas/install-manifest.schema.json",
4
+ "type": "object",
5
+ "required": ["version", "packageName", "packageVersion", "generatedAt", "files", "managedOpencode"],
6
+ "properties": {
7
+ "version": { "const": 1 },
8
+ "packageName": { "const": "@wefter/opencode" },
9
+ "packageVersion": { "type": "string", "minLength": 1 },
10
+ "generatedAt": { "type": "string", "format": "date-time" },
11
+ "files": {
12
+ "type": "array",
13
+ "items": {
14
+ "type": "object",
15
+ "required": ["path", "sha256"],
16
+ "properties": {
17
+ "path": { "$ref": "#/$defs/relativePath" },
18
+ "sha256": { "type": "string", "pattern": "^[a-f0-9]{64}$" }
19
+ },
20
+ "additionalProperties": false
21
+ }
22
+ },
23
+ "managedOpencode": {
24
+ "type": "object",
25
+ "required": ["commands", "skillsPath", "watcherIgnores"],
26
+ "properties": {
27
+ "commands": { "type": "array", "items": { "type": "string", "minLength": 1 } },
28
+ "skillsPath": { "$ref": "#/$defs/relativePath" },
29
+ "watcherIgnores": { "type": "array", "items": { "type": "string", "minLength": 1 } }
30
+ },
31
+ "additionalProperties": false
32
+ }
33
+ },
34
+ "additionalProperties": false,
35
+ "$defs": {
36
+ "relativePath": {
37
+ "type": "string",
38
+ "minLength": 1,
39
+ "pattern": "^(?![A-Za-z]:)(?![\\\\/])(?!.*(?:^|[\\\\/])\\.\\.(?:[\\\\/]|$))[^\r\n]+$"
40
+ }
41
+ }
42
+ }
package/src/cli/main.js CHANGED
@@ -1,12 +1,14 @@
1
1
  import fs from "node:fs";
2
+ import crypto from "node:crypto";
2
3
  import path from "node:path";
3
4
  import process from "node:process";
4
5
  import readline from "node:readline/promises";
5
6
  import { stdin as input, stdout as output } from "node:process";
6
7
  import { fileURLToPath } from "node:url";
7
8
 
8
- const VERSION = "0.2.1";
9
+ const VERSION = "0.3.0";
9
10
  const CONFIG_FILE = "wefter.config.json";
11
+ const INSTALL_MANIFEST_FILE = ".wefter/install-manifest.json";
10
12
  const PRODUCT_SHAPING_WORKFLOW_ID = "product-shaping";
11
13
  const DOCUMENTATION_REPAIR_WORKFLOW_ID = "documentation-repair";
12
14
  const WORK_UNIT_WORKFLOW_ID = "work-unit-implementation";
@@ -67,6 +69,7 @@ function printHelp() {
67
69
 
68
70
  Usage:
69
71
  wefter init [--yes] [--force] [--target <path>] [--profile-path <path>] [--artifact-root <path>] [--template-root <path>] [--process-doc-path <path>] [--runner-command <command>]
72
+ wefter uninstall [--target <path>] [--yes] [--force] [--dry-run]
70
73
  wefter product shape [--target <path>] [--release-id <id>] [--run-name <name>] [--spec-root <path>] [--run-root <path>] [--config-path <path>] [--profile-path <path>] [--dry-run]
71
74
  wefter product validate [--target <path>] [--release-id <id>] [--run-id <id> | --run-root <path>] [--config-path <path>] [--json]
72
75
  wefter docs audit [--target <path>] [--profile-path <path>] [--run-name <name>] [--passes-per-lens <n>] [--max-audits <n>] [--dry-run]
@@ -80,6 +83,7 @@ Usage:
80
83
 
81
84
  Commands:
82
85
  init Install opencode agents, skill, commands, templates and local config.
86
+ uninstall Remove files recorded in the Wefter install manifest.
83
87
  product shape Generate one product-shaping run skeleton.
84
88
  product validate Validate product-shaping specs against the completion gate.
85
89
  docs audit Generate one documentation audit run from the configured profile.
@@ -125,6 +129,9 @@ function allowedFlagsForCommand(command, subcommand) {
125
129
  if (command === "init") {
126
130
  return ["yes", "force", "target", "profile-path", "artifact-root", "template-root", "process-doc-path", "runner-command"];
127
131
  }
132
+ if (command === "uninstall") {
133
+ return ["target", "yes", "force", "dry-run"];
134
+ }
128
135
  if (command === "new-run") {
129
136
  return ["target", "profile-path", "run-name", "passes-per-lens", "max-audits", "dry-run"];
130
137
  }
@@ -398,6 +405,129 @@ function writeJson(filePath, value) {
398
405
  fs.writeFileSync(filePath, `${JSON.stringify(value, null, 2)}\n`, "utf8");
399
406
  }
400
407
 
408
+ function sha256File(filePath) {
409
+ return crypto.createHash("sha256").update(fs.readFileSync(filePath)).digest("hex");
410
+ }
411
+
412
+ function listFilesRecursive(root) {
413
+ if (!fs.existsSync(root)) {
414
+ return [];
415
+ }
416
+ const files = [];
417
+ for (const entry of fs.readdirSync(root, { withFileTypes: true })) {
418
+ const fullPath = path.join(root, entry.name);
419
+ if (entry.isDirectory()) {
420
+ files.push(...listFilesRecursive(fullPath));
421
+ continue;
422
+ }
423
+ if (entry.isFile()) {
424
+ files.push(fullPath);
425
+ }
426
+ }
427
+ return files;
428
+ }
429
+
430
+ function addFileIfExists(targetRoot, files, fullPath) {
431
+ if (!fs.existsSync(fullPath) || !fs.statSync(fullPath).isFile()) {
432
+ return;
433
+ }
434
+ ensureInside(targetRoot, fullPath, "installed file");
435
+ files.add(toDisplayPath(targetRoot, fullPath));
436
+ }
437
+
438
+ function addDirectoryFilesIfExists(targetRoot, files, fullPath) {
439
+ if (!fs.existsSync(fullPath)) {
440
+ return;
441
+ }
442
+ ensureInside(targetRoot, fullPath, "installed directory");
443
+ for (const file of listFilesRecursive(fullPath)) {
444
+ addFileIfExists(targetRoot, files, file);
445
+ }
446
+ }
447
+
448
+ function knownOpencodeCommandNames() {
449
+ return [
450
+ "wefter-audit-docs",
451
+ "wefter-generate-doc-audit-profile",
452
+ "wefter-repair-docs",
453
+ "wefter-shape-product",
454
+ "wefter-run-work-unit"
455
+ ];
456
+ }
457
+
458
+ function configuredWatcherIgnores(targetRoot, config) {
459
+ const workUnitConfig = readJsonIfExists(path.join(targetRoot, workUnitConfigPath(config)), "work-unit config");
460
+ const productSettings = workflowSettings(config, PRODUCT_SHAPING_WORKFLOW_ID);
461
+ return [config.artifactRoot, config.templateRoot, documentationRepairArtifactRoot(), workUnitConfig?.runArtifactsRoot, productSettings.enabled ? productShapingRunRoot(config) : null]
462
+ .filter(Boolean)
463
+ .map((ignored) => `${ignored.replace(/\/$/, "")}/**`);
464
+ }
465
+
466
+ function collectInstallManifestFiles(targetRoot, config) {
467
+ const files = new Set();
468
+ addFileIfExists(targetRoot, files, path.join(targetRoot, CONFIG_FILE));
469
+ addDirectoryFilesIfExists(targetRoot, files, path.join(targetRoot, config.workflowRoot));
470
+ addDirectoryFilesIfExists(targetRoot, files, path.join(targetRoot, config.templateRoot));
471
+ addFileIfExists(targetRoot, files, path.join(targetRoot, config.profilePath));
472
+ addFileIfExists(targetRoot, files, path.join(targetRoot, config.processDocPath));
473
+ addFileIfExists(targetRoot, files, path.join(targetRoot, productShapingConfigPath(config)));
474
+ addFileIfExists(targetRoot, files, path.join(targetRoot, productShapingProfilePath(config)));
475
+ addFileIfExists(targetRoot, files, path.join(targetRoot, workUnitConfigPath(config)));
476
+ addFileIfExists(targetRoot, files, path.join(targetRoot, workUnitProfilePath(config)));
477
+
478
+ const opencodeAgentRoot = path.join(targetRoot, ".opencode/agent");
479
+ if (fs.existsSync(opencodeAgentRoot)) {
480
+ for (const file of fs.readdirSync(opencodeAgentRoot)) {
481
+ if (file.startsWith("wefter-") && file.endsWith(".md")) {
482
+ addFileIfExists(targetRoot, files, path.join(opencodeAgentRoot, file));
483
+ }
484
+ }
485
+ }
486
+ for (const skill of ["documentation-audit", "documentation-repair", "product-shaping", "work-unit-implementation"]) {
487
+ addDirectoryFilesIfExists(targetRoot, files, path.join(targetRoot, ".opencode/skills", skill));
488
+ }
489
+
490
+ return [...files]
491
+ .filter((relativePath) => relativePath !== INSTALL_MANIFEST_FILE && relativePath !== "opencode.json")
492
+ .sort();
493
+ }
494
+
495
+ function writeInstallManifest(targetRoot, config) {
496
+ const manifestPath = path.join(targetRoot, INSTALL_MANIFEST_FILE);
497
+ const files = collectInstallManifestFiles(targetRoot, config).map((relativePath) => ({
498
+ path: relativePath,
499
+ sha256: sha256File(path.join(targetRoot, relativePath))
500
+ }));
501
+ writeJson(manifestPath, {
502
+ version: 1,
503
+ packageName: "@wefter/opencode",
504
+ packageVersion: VERSION,
505
+ generatedAt: new Date().toISOString(),
506
+ files,
507
+ managedOpencode: {
508
+ commands: knownOpencodeCommandNames(),
509
+ skillsPath: ".opencode/skills",
510
+ watcherIgnores: configuredWatcherIgnores(targetRoot, config)
511
+ }
512
+ });
513
+ }
514
+
515
+ function removeEmptyParents(targetRoot, startDir) {
516
+ let current = startDir;
517
+ while (current && current !== targetRoot && isInsideDirectory(targetRoot, current)) {
518
+ if (!fs.existsSync(current)) {
519
+ current = path.dirname(current);
520
+ continue;
521
+ }
522
+ const entries = fs.readdirSync(current);
523
+ if (entries.length > 0) {
524
+ return;
525
+ }
526
+ fs.rmdirSync(current);
527
+ current = path.dirname(current);
528
+ }
529
+ }
530
+
401
531
  function writeTextIfSafe(filePath, content, force) {
402
532
  fs.mkdirSync(path.dirname(filePath), { recursive: true });
403
533
  if (fs.existsSync(filePath)) {
@@ -570,17 +700,12 @@ function copyDirectory(sourceRoot, destinationRoot, force) {
570
700
  function mergeOpencodeConfig(targetRoot, config, force) {
571
701
  const opencodePath = path.join(targetRoot, "opencode.json");
572
702
  const existing = fs.existsSync(opencodePath) ? readJson(opencodePath, "opencode.json") : { "$schema": "https://opencode.ai/config.json" };
573
- const workUnitConfig = readJsonIfExists(path.join(targetRoot, workUnitConfigPath(config)), "work-unit config");
574
703
  const productSettings = workflowSettings(config, PRODUCT_SHAPING_WORKFLOW_ID);
575
704
 
576
705
  existing["$schema"] = existing["$schema"] || "https://opencode.ai/config.json";
577
706
  existing.watcher = existing.watcher || {};
578
707
  existing.watcher.ignore = Array.isArray(existing.watcher.ignore) ? existing.watcher.ignore : [];
579
- for (const ignored of [config.artifactRoot, config.templateRoot, documentationRepairArtifactRoot(), workUnitConfig?.runArtifactsRoot, productSettings.enabled ? productShapingRunRoot(config) : null]) {
580
- if (!ignored) {
581
- continue;
582
- }
583
- const pattern = `${ignored.replace(/\/$/, "")}/**`;
708
+ for (const pattern of configuredWatcherIgnores(targetRoot, config)) {
584
709
  if (!existing.watcher.ignore.includes(pattern)) {
585
710
  existing.watcher.ignore.push(pattern);
586
711
  }
@@ -1040,6 +1165,7 @@ async function commandInit(flags) {
1040
1165
  if (!fs.existsSync(profileFullPath)) {
1041
1166
  writeJson(profileFullPath, defaultProfile(config));
1042
1167
  }
1168
+ writeInstallManifest(targetRoot, config);
1043
1169
 
1044
1170
  console.log(`Installed Wefter for OpenCode into ${targetRoot}`);
1045
1171
  console.log(`Profile: ${config.profilePath}`);
@@ -1049,6 +1175,122 @@ async function commandInit(flags) {
1049
1175
  console.log("Restart opencode before using /wefter-shape-product, /wefter-audit-docs, /wefter-generate-doc-audit-profile, /wefter-repair-docs, or /wefter-run-work-unit.");
1050
1176
  }
1051
1177
 
1178
+ function readInstallManifest(targetRoot) {
1179
+ const manifestPath = path.join(targetRoot, INSTALL_MANIFEST_FILE);
1180
+ if (!fs.existsSync(manifestPath)) {
1181
+ throw new Error(`Missing ${INSTALL_MANIFEST_FILE}. Re-run wefter init with the current version before uninstalling, or remove Wefter files manually.`);
1182
+ }
1183
+ const manifest = readJson(manifestPath, "install manifest");
1184
+ assertObject(manifest, "Install manifest");
1185
+ if (manifest.version !== 1) {
1186
+ throw new Error("Install manifest must have version: 1.");
1187
+ }
1188
+ if (!Array.isArray(manifest.files)) {
1189
+ throw new Error("Install manifest files must be an array.");
1190
+ }
1191
+ return manifest;
1192
+ }
1193
+
1194
+ function updateOpencodeForUninstall(targetRoot, manifest, dryRun) {
1195
+ const opencodePath = path.join(targetRoot, "opencode.json");
1196
+ if (!fs.existsSync(opencodePath)) {
1197
+ return false;
1198
+ }
1199
+ const opencode = readJson(opencodePath, "opencode.json");
1200
+ let changed = false;
1201
+ for (const commandName of manifest.managedOpencode?.commands || knownOpencodeCommandNames()) {
1202
+ if (opencode.command?.[commandName]) {
1203
+ delete opencode.command[commandName];
1204
+ changed = true;
1205
+ }
1206
+ }
1207
+ const watcherIgnore = opencode.watcher?.ignore;
1208
+ if (Array.isArray(watcherIgnore)) {
1209
+ const remove = new Set(manifest.managedOpencode?.watcherIgnores || []);
1210
+ const nextIgnore = watcherIgnore.filter((item) => !remove.has(item));
1211
+ if (nextIgnore.length !== watcherIgnore.length) {
1212
+ opencode.watcher.ignore = nextIgnore;
1213
+ changed = true;
1214
+ }
1215
+ }
1216
+ const skillsPath = manifest.managedOpencode?.skillsPath || ".opencode/skills";
1217
+ if (Array.isArray(opencode.skills?.paths) && opencode.skills.paths.includes(skillsPath)) {
1218
+ const skillsRoot = path.join(targetRoot, skillsPath);
1219
+ const hasRemainingSkills = fs.existsSync(skillsRoot) && listFilesRecursive(skillsRoot).length > 0;
1220
+ if (!hasRemainingSkills) {
1221
+ opencode.skills.paths = opencode.skills.paths.filter((item) => item !== skillsPath);
1222
+ changed = true;
1223
+ }
1224
+ }
1225
+ if (changed && !dryRun) {
1226
+ writeJson(opencodePath, opencode);
1227
+ }
1228
+ return changed;
1229
+ }
1230
+
1231
+ async function confirmUninstall(flags, targetRoot, manifest) {
1232
+ if (flags.yes || flags["dry-run"] || !process.stdin.isTTY) {
1233
+ return;
1234
+ }
1235
+ const rl = readline.createInterface({ input, output });
1236
+ const answer = await rl.question(`Remove ${manifest.files.length} Wefter-managed files from ${targetRoot}? Type 'yes' to continue: `);
1237
+ rl.close();
1238
+ if (answer.trim().toLowerCase() !== "yes") {
1239
+ throw new Error("Uninstall cancelled.");
1240
+ }
1241
+ }
1242
+
1243
+ async function commandUninstall(flags) {
1244
+ const targetRoot = resolveTarget(flags);
1245
+ const manifest = readInstallManifest(targetRoot);
1246
+ await confirmUninstall(flags, targetRoot, manifest);
1247
+
1248
+ const removed = [];
1249
+ const skipped = [];
1250
+ let skippedModified = false;
1251
+ for (const item of [...manifest.files].sort((a, b) => b.path.length - a.path.length)) {
1252
+ const relativePath = normalizeRelativePath(item.path, "install manifest file path");
1253
+ const fullPath = path.join(targetRoot, relativePath);
1254
+ ensureInside(targetRoot, fullPath, "install manifest file");
1255
+ if (!fs.existsSync(fullPath)) {
1256
+ skipped.push(`${relativePath} (missing)`);
1257
+ continue;
1258
+ }
1259
+ const currentHash = sha256File(fullPath);
1260
+ if (currentHash !== item.sha256 && !flags.force) {
1261
+ skipped.push(`${relativePath} (modified; use --force to remove)`);
1262
+ skippedModified = true;
1263
+ continue;
1264
+ }
1265
+ if (!flags["dry-run"]) {
1266
+ fs.unlinkSync(fullPath);
1267
+ removeEmptyParents(targetRoot, path.dirname(fullPath));
1268
+ }
1269
+ removed.push(relativePath);
1270
+ }
1271
+
1272
+ const opencodeChanged = updateOpencodeForUninstall(targetRoot, manifest, flags["dry-run"]);
1273
+ const manifestPath = path.join(targetRoot, INSTALL_MANIFEST_FILE);
1274
+ if (fs.existsSync(manifestPath) && !flags["dry-run"] && !skippedModified) {
1275
+ fs.unlinkSync(manifestPath);
1276
+ removeEmptyParents(targetRoot, path.dirname(manifestPath));
1277
+ }
1278
+
1279
+ console.log(`${flags["dry-run"] ? "Would remove" : "Removed"} Wefter-managed files: ${removed.length}`);
1280
+ if (skipped.length > 0) {
1281
+ console.log(`Skipped files: ${skipped.length}`);
1282
+ for (const item of skipped) {
1283
+ console.log(`- ${item}`);
1284
+ }
1285
+ }
1286
+ if (opencodeChanged) {
1287
+ console.log(`${flags["dry-run"] ? "Would update" : "Updated"} opencode.json Wefter entries.`);
1288
+ }
1289
+ if (skippedModified && !flags["dry-run"]) {
1290
+ console.log(`Kept ${INSTALL_MANIFEST_FILE} so skipped files can be reviewed or removed with --force.`);
1291
+ }
1292
+ }
1293
+
1052
1294
  function readTextRequired(filePath) {
1053
1295
  if (!fs.existsSync(filePath)) {
1054
1296
  throw new Error(`Missing ${filePath}`);
@@ -2603,12 +2845,7 @@ function commandDoctor(flags) {
2603
2845
  throw new Error("Missing .opencode/skills in opencode skills.paths.");
2604
2846
  }
2605
2847
  const watcherIgnore = Array.isArray(opencode.watcher?.ignore) ? opencode.watcher.ignore : [];
2606
- const workUnitConfig = readJson(path.join(targetRoot, workUnitConfigPath(config)), "work-unit config");
2607
- for (const ignored of [config.artifactRoot, config.templateRoot, documentationRepairArtifactRoot(), workUnitConfig.runArtifactsRoot, productSettings.enabled ? productShapingRunRoot(config) : null]) {
2608
- if (!ignored) {
2609
- continue;
2610
- }
2611
- const pattern = `${ignored.replace(/\/$/, "")}/**`;
2848
+ for (const pattern of configuredWatcherIgnores(targetRoot, config)) {
2612
2849
  if (!watcherIgnore.includes(pattern)) {
2613
2850
  throw new Error(`Missing opencode watcher ignore '${pattern}'.`);
2614
2851
  }
@@ -2648,6 +2885,10 @@ export async function main(argv = process.argv.slice(2)) {
2648
2885
  await commandInit(flags);
2649
2886
  return;
2650
2887
  }
2888
+ if (command === "uninstall") {
2889
+ await commandUninstall(flags);
2890
+ return;
2891
+ }
2651
2892
  if (command === "new-run") {
2652
2893
  if (subcommand && subcommand !== "documentation-audit") {
2653
2894
  throw new Error(`Unsupported workflow for new-run: ${subcommand}`);