planmode 0.1.5 → 0.2.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.
@@ -0,0 +1,162 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import { readManifest, validateManifest, readPackageContent } from "./manifest.js";
4
+ import { renderTemplate, collectVariableValues } from "./template.js";
5
+ import { searchPackages } from "./registry.js";
6
+
7
+ export interface TestIssue {
8
+ severity: "error" | "warning";
9
+ check: string;
10
+ message: string;
11
+ }
12
+
13
+ export interface TestResult {
14
+ issues: TestIssue[];
15
+ passed: boolean;
16
+ checks: { name: string; passed: boolean }[];
17
+ }
18
+
19
+ export async function testPackage(projectDir: string = process.cwd()): Promise<TestResult> {
20
+ const issues: TestIssue[] = [];
21
+ const checks: { name: string; passed: boolean }[] = [];
22
+
23
+ // 1. Manifest exists and parses
24
+ let manifest;
25
+ try {
26
+ manifest = readManifest(projectDir);
27
+ checks.push({ name: "Manifest parses", passed: true });
28
+ } catch (err) {
29
+ issues.push({
30
+ severity: "error",
31
+ check: "Manifest parses",
32
+ message: (err as Error).message,
33
+ });
34
+ checks.push({ name: "Manifest parses", passed: false });
35
+ return { issues, passed: false, checks };
36
+ }
37
+
38
+ // 2. Manifest validates for publishing
39
+ const errors = validateManifest(manifest, true);
40
+ if (errors.length === 0) {
41
+ checks.push({ name: "Manifest valid for publishing", passed: true });
42
+ } else {
43
+ for (const err of errors) {
44
+ issues.push({
45
+ severity: "error",
46
+ check: "Manifest valid for publishing",
47
+ message: err,
48
+ });
49
+ }
50
+ checks.push({ name: "Manifest valid for publishing", passed: false });
51
+ }
52
+
53
+ // 3. Content file exists and is readable
54
+ let content: string | undefined;
55
+ try {
56
+ content = readPackageContent(projectDir, manifest);
57
+ if (content.trim().length === 0) {
58
+ issues.push({
59
+ severity: "warning",
60
+ check: "Content is non-empty",
61
+ message: "Content file is empty",
62
+ });
63
+ checks.push({ name: "Content is non-empty", passed: false });
64
+ } else {
65
+ checks.push({ name: "Content is non-empty", passed: true });
66
+ }
67
+ } catch (err) {
68
+ issues.push({
69
+ severity: "error",
70
+ check: "Content readable",
71
+ message: (err as Error).message,
72
+ });
73
+ checks.push({ name: "Content readable", passed: false });
74
+ }
75
+
76
+ // 4. Template renders with default values
77
+ if (content && manifest.variables && Object.keys(manifest.variables).length > 0) {
78
+ try {
79
+ // Check all required variables have defaults
80
+ const missingDefaults: string[] = [];
81
+ for (const [name, def] of Object.entries(manifest.variables)) {
82
+ if (def.required && def.default === undefined) {
83
+ missingDefaults.push(name);
84
+ }
85
+ }
86
+
87
+ if (missingDefaults.length > 0) {
88
+ issues.push({
89
+ severity: "warning",
90
+ check: "Required variables have defaults",
91
+ message: `Required variables without defaults: ${missingDefaults.join(", ")}. Users must provide these at install time.`,
92
+ });
93
+ checks.push({ name: "Required variables have defaults", passed: false });
94
+ } else {
95
+ checks.push({ name: "Required variables have defaults", passed: true });
96
+ }
97
+
98
+ // Try rendering with defaults only
99
+ const values = collectVariableValues(manifest.variables, {});
100
+ renderTemplate(content, values);
101
+ checks.push({ name: "Template renders with defaults", passed: true });
102
+ } catch (err) {
103
+ issues.push({
104
+ severity: "error",
105
+ check: "Template renders with defaults",
106
+ message: (err as Error).message,
107
+ });
108
+ checks.push({ name: "Template renders with defaults", passed: false });
109
+ }
110
+ }
111
+
112
+ // 5. Dependencies exist in registry
113
+ if (manifest.dependencies) {
114
+ const allDeps = [
115
+ ...(manifest.dependencies.rules ?? []),
116
+ ...(manifest.dependencies.plans ?? []),
117
+ ];
118
+
119
+ for (const dep of allDeps) {
120
+ const depName = dep.includes("@") ? dep.split("@")[0]! : dep;
121
+ try {
122
+ const results = await searchPackages(depName);
123
+ const found = results.some((r) => r.name === depName);
124
+ if (found) {
125
+ checks.push({ name: `Dependency "${depName}" exists`, passed: true });
126
+ } else {
127
+ issues.push({
128
+ severity: "warning",
129
+ check: `Dependency "${depName}" exists`,
130
+ message: `Dependency "${depName}" not found in registry. It may not be published yet.`,
131
+ });
132
+ checks.push({ name: `Dependency "${depName}" exists`, passed: false });
133
+ }
134
+ } catch {
135
+ issues.push({
136
+ severity: "warning",
137
+ check: `Dependency "${depName}" exists`,
138
+ message: `Could not check registry for "${depName}" (network error)`,
139
+ });
140
+ checks.push({ name: `Dependency "${depName}" exists`, passed: false });
141
+ }
142
+ }
143
+ }
144
+
145
+ // 6. Content file size check
146
+ if (content) {
147
+ const sizeKb = Buffer.byteLength(content, "utf-8") / 1024;
148
+ if (sizeKb > 100) {
149
+ issues.push({
150
+ severity: "warning",
151
+ check: "Content size reasonable",
152
+ message: `Content is ${sizeKb.toFixed(1)}KB. Large packages may hit token limits in AI models.`,
153
+ });
154
+ checks.push({ name: "Content size reasonable", passed: false });
155
+ } else {
156
+ checks.push({ name: "Content size reasonable", passed: true });
157
+ }
158
+ }
159
+
160
+ const hasErrors = issues.some((i) => i.severity === "error");
161
+ return { issues, passed: !hasErrors, checks };
162
+ }