@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 +7 -0
- package/README.md +6 -4
- package/docs/INSTALLATION.md +9 -0
- package/docs/ROADMAP.md +48 -0
- package/docs/SAFETY_MODEL.md +1 -0
- package/package.json +1 -1
- package/schemas/install-manifest.schema.json +42 -0
- package/src/cli/main.js +254 -13
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.
|
|
125
|
+
Next hardening steps after the `0.3.0` install-manifest release:
|
|
123
126
|
|
|
124
|
-
1.
|
|
125
|
-
2.
|
|
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.
|
package/docs/INSTALLATION.md
CHANGED
|
@@ -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
|
package/docs/ROADMAP.md
ADDED
|
@@ -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.
|
package/docs/SAFETY_MODEL.md
CHANGED
|
@@ -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
|
@@ -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.
|
|
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
|
|
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
|
|
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}`);
|