@tera-system/pro 0.1.1 → 0.1.3
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/MANIFEST.json +1 -1
- package/package.json +2 -2
- package/scripts/install.js +419 -406
package/MANIFEST.json
CHANGED
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tera-system/pro",
|
|
3
|
-
"version": "0.1.
|
|
4
|
-
"description": "Tera System Pro
|
|
3
|
+
"version": "0.1.3",
|
|
4
|
+
"description": "Tera System Pro أ¢â‚¬â€ commercial edition with enforced license gate, server sync, and heartbeat verification",
|
|
5
5
|
"license": "SEE LICENSE IN LICENSE.md",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"engines": {
|
package/scripts/install.js
CHANGED
|
@@ -1,406 +1,419 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
/**
|
|
3
|
-
* @tera/core installer
|
|
4
|
-
* --------------------------------------------------------------
|
|
5
|
-
* NON-DESTRUCTIVE + OWNERSHIP-AWARE installer for the Tera System
|
|
6
|
-
* OpenCode Plugin component.
|
|
7
|
-
*
|
|
8
|
-
* Rules:
|
|
9
|
-
* - Never deletes anything.
|
|
10
|
-
* - Never overwrites a file it does not own.
|
|
11
|
-
* - Tracks ownership via `.opencode/tera-core.manifest.json`.
|
|
12
|
-
* - Only updates files it owns, always keeping a backup first.
|
|
13
|
-
* - Idempotent: re-running is safe; identical files are skipped.
|
|
14
|
-
* - Never touches an existing user `opencode.json`.
|
|
15
|
-
*
|
|
16
|
-
* Target resolution (first match wins):
|
|
17
|
-
* 1. `--target <dir>` argument
|
|
18
|
-
* 2. `TERA_TARGET_DIR` environment variable
|
|
19
|
-
* 3. `INIT_CWD` (set by npm for lifecycle scripts)
|
|
20
|
-
* 4. current working directory
|
|
21
|
-
*/
|
|
22
|
-
import { createHash } from "node:crypto";
|
|
23
|
-
import fs from "node:fs";
|
|
24
|
-
import path from "node:path";
|
|
25
|
-
import { fileURLToPath } from "node:url";
|
|
26
|
-
import { assessLicenseState, readLicenseState, writeLicenseState } from "./lib/license.mjs";
|
|
27
|
-
import { createIntegrityManifest, saveIntegrityManifest } from "./lib/integrity.mjs";
|
|
28
|
-
import { verifyBundle, applyBundle } from "./lib/bundle.mjs";
|
|
29
|
-
|
|
30
|
-
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
31
|
-
const PKG_ROOT = path.resolve(__dirname, "..");
|
|
32
|
-
const MANIFEST_NAME = "tera-core.manifest.json";
|
|
33
|
-
const CONFIG_EXAMPLE = "opencode.tera.example.json";
|
|
34
|
-
|
|
35
|
-
/* ---------------------------------- helpers ---------------------------------- */
|
|
36
|
-
|
|
37
|
-
const sha256 = (buf) => createHash("sha256").update(buf).digest("hex");
|
|
38
|
-
|
|
39
|
-
const toPosix = (p) => p.split(path.sep).join("/");
|
|
40
|
-
|
|
41
|
-
function resolveTarget() {
|
|
42
|
-
const args = process.argv.slice(2);
|
|
43
|
-
for (let i = 0; i < args.length; i++) {
|
|
44
|
-
if (args[i] === "--help") {
|
|
45
|
-
console.log("Usage: node scripts/install.js [--target <workspaceRoot>]");
|
|
46
|
-
process.exit(0);
|
|
47
|
-
}
|
|
48
|
-
if (args[i] === "--target" && args[i + 1]) {
|
|
49
|
-
return path.resolve(args[i + 1]);
|
|
50
|
-
}
|
|
51
|
-
}
|
|
52
|
-
if (process.env.TERA_TARGET_DIR) return path.resolve(process.env.TERA_TARGET_DIR);
|
|
53
|
-
if (process.env.INIT_CWD) return path.resolve(process.env.INIT_CWD);
|
|
54
|
-
return path.resolve(process.cwd());
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
function walkFiles(dir) {
|
|
58
|
-
const out = [];
|
|
59
|
-
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
60
|
-
const full = path.join(dir, entry.name);
|
|
61
|
-
if (entry.isDirectory()) out.push(...walkFiles(full));
|
|
62
|
-
else if (entry.isFile()) out.push(full);
|
|
63
|
-
}
|
|
64
|
-
return out;
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
function copyFile(src, dest) {
|
|
68
|
-
fs.mkdirSync(path.dirname(dest), { recursive: true });
|
|
69
|
-
fs.copyFileSync(src, dest);
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
function loadOwnership(target) {
|
|
73
|
-
const p = path.join(target, ".opencode", MANIFEST_NAME);
|
|
74
|
-
if (!fs.existsSync(p)) return { files: {} };
|
|
75
|
-
try {
|
|
76
|
-
const parsed = JSON.parse(fs.readFileSync(p, "utf8"));
|
|
77
|
-
return { files: parsed.files && typeof parsed.files === "object" ? parsed.files : {} };
|
|
78
|
-
} catch {
|
|
79
|
-
return { files: {} };
|
|
80
|
-
}
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
/* ---------------------------------- state ---------------------------------- */
|
|
84
|
-
|
|
85
|
-
const target = resolveTarget();
|
|
86
|
-
const pkg = JSON.parse(fs.readFileSync(path.join(PKG_ROOT, "package.json"), "utf8"));
|
|
87
|
-
const ownership = loadOwnership(target);
|
|
88
|
-
|
|
89
|
-
const stats = { installed: 0, updated: 0, identical: 0, skippedGroups: 0, conflicts: [] };
|
|
90
|
-
|
|
91
|
-
/* ------------------------------- group install ------------------------------- */
|
|
92
|
-
|
|
93
|
-
/**
|
|
94
|
-
* @param {{name:string, src:string, dest:string}} group
|
|
95
|
-
*/
|
|
96
|
-
function installGroup(group) {
|
|
97
|
-
if (!fs.existsSync(group.src)) return;
|
|
98
|
-
for (const srcFile of walkFiles(group.src)) {
|
|
99
|
-
const relSrc = path.relative(group.src, srcFile);
|
|
100
|
-
const destFile = path.join(group.dest, relSrc);
|
|
101
|
-
const destRel = toPosix(path.relative(target, destFile));
|
|
102
|
-
const srcHash = sha256(fs.readFileSync(srcFile));
|
|
103
|
-
|
|
104
|
-
if (fs.existsSync(destFile)) {
|
|
105
|
-
const destHash = sha256(fs.readFileSync(destFile));
|
|
106
|
-
if (destHash === srcHash) {
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
}
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
const
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
*
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
*
|
|
237
|
-
*
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
state:
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
}
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
// Step
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
const
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
}
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
// Step
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
const
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* @tera/core installer
|
|
4
|
+
* --------------------------------------------------------------
|
|
5
|
+
* NON-DESTRUCTIVE + OWNERSHIP-AWARE installer for the Tera System
|
|
6
|
+
* OpenCode Plugin component.
|
|
7
|
+
*
|
|
8
|
+
* Rules:
|
|
9
|
+
* - Never deletes anything.
|
|
10
|
+
* - Never overwrites a file it does not own.
|
|
11
|
+
* - Tracks ownership via `.opencode/tera-core.manifest.json`.
|
|
12
|
+
* - Only updates files it owns, always keeping a backup first.
|
|
13
|
+
* - Idempotent: re-running is safe; identical files are skipped.
|
|
14
|
+
* - Never touches an existing user `opencode.json`.
|
|
15
|
+
*
|
|
16
|
+
* Target resolution (first match wins):
|
|
17
|
+
* 1. `--target <dir>` argument
|
|
18
|
+
* 2. `TERA_TARGET_DIR` environment variable
|
|
19
|
+
* 3. `INIT_CWD` (set by npm for lifecycle scripts)
|
|
20
|
+
* 4. current working directory
|
|
21
|
+
*/
|
|
22
|
+
import { createHash } from "node:crypto";
|
|
23
|
+
import fs from "node:fs";
|
|
24
|
+
import path from "node:path";
|
|
25
|
+
import { fileURLToPath } from "node:url";
|
|
26
|
+
import { assessLicenseState, readLicenseState, writeLicenseState } from "./lib/license.mjs";
|
|
27
|
+
import { createIntegrityManifest, saveIntegrityManifest } from "./lib/integrity.mjs";
|
|
28
|
+
import { verifyBundle, applyBundle } from "./lib/bundle.mjs";
|
|
29
|
+
|
|
30
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
31
|
+
const PKG_ROOT = path.resolve(__dirname, "..");
|
|
32
|
+
const MANIFEST_NAME = "tera-core.manifest.json";
|
|
33
|
+
const CONFIG_EXAMPLE = "opencode.tera.example.json";
|
|
34
|
+
|
|
35
|
+
/* ---------------------------------- helpers ---------------------------------- */
|
|
36
|
+
|
|
37
|
+
const sha256 = (buf) => createHash("sha256").update(buf).digest("hex");
|
|
38
|
+
|
|
39
|
+
const toPosix = (p) => p.split(path.sep).join("/");
|
|
40
|
+
|
|
41
|
+
function resolveTarget() {
|
|
42
|
+
const args = process.argv.slice(2);
|
|
43
|
+
for (let i = 0; i < args.length; i++) {
|
|
44
|
+
if (args[i] === "--help") {
|
|
45
|
+
console.log("Usage: node scripts/install.js [--target <workspaceRoot>]");
|
|
46
|
+
process.exit(0);
|
|
47
|
+
}
|
|
48
|
+
if (args[i] === "--target" && args[i + 1]) {
|
|
49
|
+
return path.resolve(args[i + 1]);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
if (process.env.TERA_TARGET_DIR) return path.resolve(process.env.TERA_TARGET_DIR);
|
|
53
|
+
if (process.env.INIT_CWD) return path.resolve(process.env.INIT_CWD);
|
|
54
|
+
return path.resolve(process.cwd());
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function walkFiles(dir) {
|
|
58
|
+
const out = [];
|
|
59
|
+
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
60
|
+
const full = path.join(dir, entry.name);
|
|
61
|
+
if (entry.isDirectory()) out.push(...walkFiles(full));
|
|
62
|
+
else if (entry.isFile()) out.push(full);
|
|
63
|
+
}
|
|
64
|
+
return out;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function copyFile(src, dest) {
|
|
68
|
+
fs.mkdirSync(path.dirname(dest), { recursive: true });
|
|
69
|
+
fs.copyFileSync(src, dest);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function loadOwnership(target) {
|
|
73
|
+
const p = path.join(target, ".opencode", MANIFEST_NAME);
|
|
74
|
+
if (!fs.existsSync(p)) return { files: {} };
|
|
75
|
+
try {
|
|
76
|
+
const parsed = JSON.parse(fs.readFileSync(p, "utf8"));
|
|
77
|
+
return { files: parsed.files && typeof parsed.files === "object" ? parsed.files : {} };
|
|
78
|
+
} catch {
|
|
79
|
+
return { files: {} };
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/* ---------------------------------- state ---------------------------------- */
|
|
84
|
+
|
|
85
|
+
const target = resolveTarget();
|
|
86
|
+
const pkg = JSON.parse(fs.readFileSync(path.join(PKG_ROOT, "package.json"), "utf8"));
|
|
87
|
+
const ownership = loadOwnership(target);
|
|
88
|
+
|
|
89
|
+
const stats = { installed: 0, updated: 0, identical: 0, adopted: 0, skippedGroups: 0, conflicts: [] };
|
|
90
|
+
|
|
91
|
+
/* ------------------------------- group install ------------------------------- */
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* @param {{name:string, src:string, dest:string}} group
|
|
95
|
+
*/
|
|
96
|
+
function installGroup(group) {
|
|
97
|
+
if (!fs.existsSync(group.src)) return;
|
|
98
|
+
for (const srcFile of walkFiles(group.src)) {
|
|
99
|
+
const relSrc = path.relative(group.src, srcFile);
|
|
100
|
+
const destFile = path.join(group.dest, relSrc);
|
|
101
|
+
const destRel = toPosix(path.relative(target, destFile));
|
|
102
|
+
const srcHash = sha256(fs.readFileSync(srcFile));
|
|
103
|
+
|
|
104
|
+
if (fs.existsSync(destFile)) {
|
|
105
|
+
const destHash = sha256(fs.readFileSync(destFile));
|
|
106
|
+
if (destHash === srcHash) {
|
|
107
|
+
// Identical content — adopt ownership if not already owned.
|
|
108
|
+
// (byte-identical file = package content, so it is safe to claim it.)
|
|
109
|
+
if (!Object.prototype.hasOwnProperty.call(ownership.files, destRel)) {
|
|
110
|
+
ownership.files[destRel] = srcHash;
|
|
111
|
+
stats.adopted++;
|
|
112
|
+
console.log(` [adopt ] ${destRel} — identical, now owned by package`);
|
|
113
|
+
} else {
|
|
114
|
+
stats.identical++;
|
|
115
|
+
}
|
|
116
|
+
continue;
|
|
117
|
+
}
|
|
118
|
+
const owned = Object.prototype.hasOwnProperty.call(ownership.files, destRel);
|
|
119
|
+
if (owned) {
|
|
120
|
+
const bak = `${destFile}.tera-bak-${Date.now()}`;
|
|
121
|
+
fs.copyFileSync(destFile, bak);
|
|
122
|
+
fs.copyFileSync(srcFile, destFile);
|
|
123
|
+
ownership.files[destRel] = srcHash;
|
|
124
|
+
stats.updated++;
|
|
125
|
+
console.log(` [updated ] ${destRel} (backup: ${path.basename(bak)})`);
|
|
126
|
+
} else {
|
|
127
|
+
stats.conflicts.push(destRel);
|
|
128
|
+
console.log(` [CONFLICT] ${destRel} — kept your file (unowned, not overwritten).`);
|
|
129
|
+
}
|
|
130
|
+
} else {
|
|
131
|
+
copyFile(srcFile, destFile);
|
|
132
|
+
ownership.files[destRel] = srcHash;
|
|
133
|
+
stats.installed++;
|
|
134
|
+
console.log(` [install ] ${destRel}`);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/* ---------------------------------- config ---------------------------------- */
|
|
140
|
+
|
|
141
|
+
function installConfig() {
|
|
142
|
+
const configSrc = path.join(PKG_ROOT, CONFIG_EXAMPLE);
|
|
143
|
+
if (!fs.existsSync(configSrc)) return;
|
|
144
|
+
const configDest = path.join(target, ".opencode", "opencode.json");
|
|
145
|
+
const configRel = ".opencode/opencode.json";
|
|
146
|
+
|
|
147
|
+
if (fs.existsSync(configDest)) {
|
|
148
|
+
const a = sha256(fs.readFileSync(configDest));
|
|
149
|
+
const b = sha256(fs.readFileSync(configSrc));
|
|
150
|
+
if (a === b) {
|
|
151
|
+
stats.identical++;
|
|
152
|
+
console.log(` [same ] ${configRel} — already identical.`);
|
|
153
|
+
} else {
|
|
154
|
+
stats.conflicts.push(configRel);
|
|
155
|
+
console.log(` [CONFLICT] ${configRel} — kept yours. Reference copy provided below.`);
|
|
156
|
+
}
|
|
157
|
+
} else {
|
|
158
|
+
copyFile(configSrc, configDest);
|
|
159
|
+
ownership.files[configRel] = sha256(fs.readFileSync(configDest));
|
|
160
|
+
stats.installed++;
|
|
161
|
+
console.log(` [install ] ${configRel}`);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// Additive reference copy — always written, never destructive.
|
|
165
|
+
const exampleDest = path.join(target, ".opencode", CONFIG_EXAMPLE);
|
|
166
|
+
copyFile(configSrc, exampleDest);
|
|
167
|
+
console.log(` [reference] .opencode/${CONFIG_EXAMPLE}`);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/* ------------------------------ project-control ------------------------------ */
|
|
171
|
+
|
|
172
|
+
function installProjectControl() {
|
|
173
|
+
const pcSrc = path.join(PKG_ROOT, "core", "project-control");
|
|
174
|
+
if (!fs.existsSync(pcSrc)) return;
|
|
175
|
+
const pcDest = path.join(target, "project-control");
|
|
176
|
+
if (fs.existsSync(pcDest)) {
|
|
177
|
+
stats.skippedGroups++;
|
|
178
|
+
console.log(" [SKIP ] project-control/ — exists (user-owned state). Skeleton not installed.");
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
181
|
+
for (const f of walkFiles(pcSrc)) {
|
|
182
|
+
const relF = toPosix(path.relative(pcSrc, f));
|
|
183
|
+
const d = path.join(pcDest, relF);
|
|
184
|
+
copyFile(f, d);
|
|
185
|
+
ownership.files[`project-control/${relF}`] = sha256(fs.readFileSync(f));
|
|
186
|
+
stats.installed++;
|
|
187
|
+
console.log(` [install ] project-control/${relF}`);
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/* ----------------------------------- run ----------------------------------- */
|
|
192
|
+
|
|
193
|
+
console.log(`\n@tera/core v${pkg.version} — install`);
|
|
194
|
+
console.log(` target workspace : ${target}\n`);
|
|
195
|
+
|
|
196
|
+
installGroup({ name: "agents", src: path.join(PKG_ROOT, "agents"), dest: path.join(target, ".opencode", "agents") });
|
|
197
|
+
installGroup({ name: "commands", src: path.join(PKG_ROOT, "commands"), dest: path.join(target, ".opencode", "commands") });
|
|
198
|
+
installGroup({ name: "tera-system", src: path.join(PKG_ROOT, "core", "tera-system"), dest: path.join(target, "tera-system") });
|
|
199
|
+
installGroup({ name: "tools", src: path.join(PKG_ROOT, "tools"), dest: path.join(target, "tools") });
|
|
200
|
+
installGroup({ name: "scripts", src: path.join(PKG_ROOT, "scripts"), dest: path.join(target, "scripts") });
|
|
201
|
+
installConfig();
|
|
202
|
+
installProjectControl();
|
|
203
|
+
|
|
204
|
+
/* -------------------------------- persist log -------------------------------- */
|
|
205
|
+
|
|
206
|
+
const manifest = {
|
|
207
|
+
name: pkg.name,
|
|
208
|
+
version: pkg.version,
|
|
209
|
+
installedAt: new Date().toISOString(),
|
|
210
|
+
target,
|
|
211
|
+
files: ownership.files,
|
|
212
|
+
conflicts: stats.conflicts,
|
|
213
|
+
summary: {
|
|
214
|
+
installed: stats.installed,
|
|
215
|
+
updated: stats.updated,
|
|
216
|
+
identical: stats.identical,
|
|
217
|
+
adopted: stats.adopted,
|
|
218
|
+
skippedGroups: stats.skippedGroups,
|
|
219
|
+
conflicts: stats.conflicts.length,
|
|
220
|
+
},
|
|
221
|
+
};
|
|
222
|
+
|
|
223
|
+
const manifestPath = path.join(target, ".opencode", MANIFEST_NAME);
|
|
224
|
+
fs.mkdirSync(path.dirname(manifestPath), { recursive: true });
|
|
225
|
+
fs.writeFileSync(manifestPath, JSON.stringify(manifest, null, 2));
|
|
226
|
+
|
|
227
|
+
/* ------------------- license state (non-blocking, never breaks install) ------------------- */
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* License verification with API-first strategy + offline fallback.
|
|
231
|
+
*
|
|
232
|
+
* Flow:
|
|
233
|
+
* 1. Try online API verification (if API URL is configured)
|
|
234
|
+
* 2. On API failure → fallback to offline Ed25519 verification
|
|
235
|
+
* 3. Never blocks install — always continues
|
|
236
|
+
*
|
|
237
|
+
* Environment variables:
|
|
238
|
+
* TERA_LICENSE_API — API base URL (e.g. https://teranoo.com/api/license)
|
|
239
|
+
* TERA_LICENSE_KEY — Manual license key override
|
|
240
|
+
*/
|
|
241
|
+
const LICENSE_API_URL = process.env.TERA_LICENSE_API || "https://teranoo.com/api/license";
|
|
242
|
+
const LICENSE_TIMEOUT_MS = 5000; // 5 seconds max for API call
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
* Verify license via remote API (online).
|
|
246
|
+
* Returns: { ok: boolean, state: string, reason: string, expiresAt?: string, source: "api" }
|
|
247
|
+
*/
|
|
248
|
+
async function verifyViaAPI(licenseKey) {
|
|
249
|
+
if (!licenseKey) return { ok: false, state: "Unlicensed", reason: "no-key", source: "api" };
|
|
250
|
+
|
|
251
|
+
try {
|
|
252
|
+
const controller = new AbortController();
|
|
253
|
+
const timeout = setTimeout(() => controller.abort(), LICENSE_TIMEOUT_MS);
|
|
254
|
+
|
|
255
|
+
const res = await fetch(`${LICENSE_API_URL}/verify`, {
|
|
256
|
+
method: "POST",
|
|
257
|
+
headers: { "Content-Type": "application/json" },
|
|
258
|
+
body: JSON.stringify({ licenseKey }),
|
|
259
|
+
signal: controller.signal,
|
|
260
|
+
});
|
|
261
|
+
clearTimeout(timeout);
|
|
262
|
+
|
|
263
|
+
if (!res.ok) {
|
|
264
|
+
// Server responded with an error status (e.g. 404 while the endpoint is
|
|
265
|
+
// not implemented yet, or 5xx). This is NOT a definitive license verdict —
|
|
266
|
+
// treat it as "API unavailable" so the offline Ed25519 fallback runs.
|
|
267
|
+
return { ok: false, state: "Unknown", reason: `api-http-${res.status}`, source: "api-fallback" };
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
const data = await res.json();
|
|
271
|
+
return {
|
|
272
|
+
ok: data.valid === true,
|
|
273
|
+
state: data.state || "Invalid",
|
|
274
|
+
reason: data.reason || "api-response",
|
|
275
|
+
expiresAt: data.expiresAt || null,
|
|
276
|
+
source: "api",
|
|
277
|
+
};
|
|
278
|
+
} catch (err) {
|
|
279
|
+
// Network error, timeout, abort — fall through to offline
|
|
280
|
+
return { ok: false, state: "Unknown", reason: `api-error: ${err.message}`, source: "api-fallback" };
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
/**
|
|
285
|
+
* Verify license offline (Ed25519 signature check).
|
|
286
|
+
* Returns: { ok: boolean, state: string, reason: string, expiresAt?: string, source: "offline" }
|
|
287
|
+
*/
|
|
288
|
+
function verifyOffline(licenseKey) {
|
|
289
|
+
const state = assessLicenseState(licenseKey);
|
|
290
|
+
return {
|
|
291
|
+
ok: state.state === "Active" || state.state === "Grace",
|
|
292
|
+
state: state.state,
|
|
293
|
+
reason: state.reason,
|
|
294
|
+
expiresAt: state.expiresAt || null,
|
|
295
|
+
graceUntil: state.graceUntil || null,
|
|
296
|
+
source: "offline",
|
|
297
|
+
};
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
/**
|
|
301
|
+
* Combined verification: API-first, offline fallback.
|
|
302
|
+
* Always returns a result — never throws.
|
|
303
|
+
*/
|
|
304
|
+
async function verifyLicense(licenseKey) {
|
|
305
|
+
// Step 1: Try API (unless explicitly disabled)
|
|
306
|
+
if (process.env.TERA_OFFLINE_ONLY === "1") {
|
|
307
|
+
return verifyOffline(licenseKey);
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
const apiResult = await verifyViaAPI(licenseKey);
|
|
311
|
+
|
|
312
|
+
// If API succeeded and gave a definitive answer, use it
|
|
313
|
+
if (apiResult.source === "api" && apiResult.ok !== undefined) {
|
|
314
|
+
return apiResult;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
// Step 2: Fallback to offline
|
|
318
|
+
const offlineResult = verifyOffline(licenseKey);
|
|
319
|
+
|
|
320
|
+
// If offline confirms API's failure, use offline (more trusted)
|
|
321
|
+
if (!apiResult.ok && !offlineResult.ok) {
|
|
322
|
+
return offlineResult;
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
// If API failed but offline says active, trust offline (API might be down)
|
|
326
|
+
if (!apiResult.ok && offlineResult.ok) {
|
|
327
|
+
return { ...offlineResult, reason: `offline-verified (api-unavailable: ${apiResult.reason})` };
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
// Default: trust API
|
|
331
|
+
return apiResult;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
/* ------------------- server sync → integrity → license ------------------- */
|
|
335
|
+
const SYNC_URL = process.env.TERA_SYNC_URL || "https://teranoo.com/api/tera-system/bundle";
|
|
336
|
+
|
|
337
|
+
/**
|
|
338
|
+
* Attempt server sync (best-effort, never breaks install).
|
|
339
|
+
* Downloads the signed bundle, verifies signature + hashes, and applies.
|
|
340
|
+
* If unreachable/rejected → bundled content remains (thick mode).
|
|
341
|
+
*/
|
|
342
|
+
async function tryServerSync() {
|
|
343
|
+
try {
|
|
344
|
+
const controller = new AbortController();
|
|
345
|
+
const timeout = setTimeout(() => controller.abort(), 8000);
|
|
346
|
+
const res = await fetch(SYNC_URL, { signal: controller.signal });
|
|
347
|
+
clearTimeout(timeout);
|
|
348
|
+
if (!res.ok) {
|
|
349
|
+
console.log(` [sync ] server HTTP ${res.status} — using bundled content`);
|
|
350
|
+
return false;
|
|
351
|
+
}
|
|
352
|
+
const bundle = await res.json();
|
|
353
|
+
const v = verifyBundle(bundle);
|
|
354
|
+
if (!v.ok) {
|
|
355
|
+
console.log(` [sync ] bundle rejected (${v.reason}) — using bundled content`);
|
|
356
|
+
return false;
|
|
357
|
+
}
|
|
358
|
+
const r = applyBundle(bundle, target);
|
|
359
|
+
console.log(` [sync ] server bundle applied — ${r.applied} files (signed & verified)`);
|
|
360
|
+
return true;
|
|
361
|
+
} catch {
|
|
362
|
+
console.log(` [sync ] server unreachable — using bundled content`);
|
|
363
|
+
return false;
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
// Run: sync → integrity manifest → license check (async wrapper, non-blocking)
|
|
368
|
+
(async () => {
|
|
369
|
+
try {
|
|
370
|
+
// Step 1: best-effort server sync (self-healing / fresh content)
|
|
371
|
+
await tryServerSync();
|
|
372
|
+
|
|
373
|
+
// Step 2: integrity manifest (after sync, covers server files too)
|
|
374
|
+
try {
|
|
375
|
+
const integrityManifest = createIntegrityManifest(target);
|
|
376
|
+
saveIntegrityManifest(target, integrityManifest);
|
|
377
|
+
console.log(` [integrity] manifest created (${integrityManifest.fileCount} files tracked)`);
|
|
378
|
+
} catch (err) {
|
|
379
|
+
console.log(` [integrity] skipped (${err.message}) — install continues`);
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
// Step 3: license verification (API-first, offline fallback)
|
|
383
|
+
const stateFile = path.join(target, ".tera", "license.state.json");
|
|
384
|
+
let licenseKey = (readLicenseState(stateFile) || {}).licenseKey;
|
|
385
|
+
if (!licenseKey) licenseKey = process.env.TERA_LICENSE_KEY || undefined;
|
|
386
|
+
|
|
387
|
+
const result = await verifyLicense(licenseKey);
|
|
388
|
+
|
|
389
|
+
writeLicenseState(stateFile, {
|
|
390
|
+
state: result.state,
|
|
391
|
+
reason: result.reason,
|
|
392
|
+
licenseKey: licenseKey || null,
|
|
393
|
+
expiresAt: result.expiresAt || null,
|
|
394
|
+
graceUntil: result.graceUntil || null,
|
|
395
|
+
source: result.source,
|
|
396
|
+
checkedAt: new Date().toISOString(),
|
|
397
|
+
});
|
|
398
|
+
|
|
399
|
+
const sourceTag = result.source === "api" ? "☁️" : result.source === "offline" ? "🔒" : "🔄";
|
|
400
|
+
console.log(` [license ] ${sourceTag} state=${result.state} (${result.reason})`);
|
|
401
|
+
} catch (err) {
|
|
402
|
+
console.log(` [license ] skipped (${err.message}) — install continues`);
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
// final summary (printed after sync/integrity/license complete)
|
|
406
|
+
console.log("\n summary:");
|
|
407
|
+
console.log(` installed : ${stats.installed}`);
|
|
408
|
+
console.log(` updated : ${stats.updated}`);
|
|
409
|
+
console.log(` identical : ${stats.identical}`);
|
|
410
|
+
console.log(` adopted : ${stats.adopted} (pre-existing identical files now owned)`);
|
|
411
|
+
console.log(` conflicts : ${stats.conflicts.length} (your files kept untouched)`);
|
|
412
|
+
console.log(` skipped : ${stats.skippedGroups} group(s) — user-owned state`);
|
|
413
|
+
console.log(` ownership log : ${path.join(".opencode", MANIFEST_NAME)}\n`);
|
|
414
|
+
|
|
415
|
+
if (stats.conflicts.length > 0) {
|
|
416
|
+
console.log(" ⚠ conflicts — your files were kept. Review them before relying on Tera content.");
|
|
417
|
+
process.exitCode = 0;
|
|
418
|
+
}
|
|
419
|
+
})();
|