everything-dev 1.3.7 → 1.4.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.
- package/dist/cli/init.cjs +34 -5
- package/dist/cli/init.cjs.map +1 -1
- package/dist/cli/init.d.cts +10 -7
- package/dist/cli/init.d.cts.map +1 -1
- package/dist/cli/init.d.mts +10 -7
- package/dist/cli/init.d.mts.map +1 -1
- package/dist/cli/init.mjs +34 -6
- package/dist/cli/init.mjs.map +1 -1
- package/dist/cli/prompts.cjs +19 -9
- package/dist/cli/prompts.cjs.map +1 -1
- package/dist/cli/prompts.mjs +19 -9
- package/dist/cli/prompts.mjs.map +1 -1
- package/dist/cli/snapshot.cjs +35 -0
- package/dist/cli/snapshot.cjs.map +1 -0
- package/dist/cli/snapshot.mjs +33 -0
- package/dist/cli/snapshot.mjs.map +1 -0
- package/dist/cli/status.cjs +80 -0
- package/dist/cli/status.cjs.map +1 -0
- package/dist/cli/status.mjs +79 -0
- package/dist/cli/status.mjs.map +1 -0
- package/dist/cli/sync.cjs +170 -0
- package/dist/cli/sync.cjs.map +1 -0
- package/dist/cli/sync.mjs +169 -0
- package/dist/cli/sync.mjs.map +1 -0
- package/dist/cli/upgrade.cjs +123 -0
- package/dist/cli/upgrade.cjs.map +1 -0
- package/dist/cli/upgrade.mjs +122 -0
- package/dist/cli/upgrade.mjs.map +1 -0
- package/dist/cli.cjs +101 -5
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.mjs +101 -5
- package/dist/cli.mjs.map +1 -1
- package/dist/contract.cjs +81 -8
- package/dist/contract.cjs.map +1 -1
- package/dist/contract.d.cts +162 -21
- package/dist/contract.d.cts.map +1 -1
- package/dist/contract.d.mts +162 -21
- package/dist/contract.d.mts.map +1 -1
- package/dist/contract.meta.cjs +32 -9
- package/dist/contract.meta.cjs.map +1 -1
- package/dist/contract.meta.d.cts +50 -11
- package/dist/contract.meta.d.mts +50 -11
- package/dist/contract.meta.mjs +32 -9
- package/dist/contract.meta.mjs.map +1 -1
- package/dist/contract.mjs +77 -9
- package/dist/contract.mjs.map +1 -1
- package/dist/index.cjs +5 -0
- package/dist/index.d.cts +2 -2
- package/dist/index.d.mts +2 -2
- package/dist/index.mjs +2 -2
- package/dist/plugin.cjs +123 -43
- package/dist/plugin.cjs.map +1 -1
- package/dist/plugin.d.cts +78 -11
- package/dist/plugin.d.cts.map +1 -1
- package/dist/plugin.d.mts +78 -11
- package/dist/plugin.d.mts.map +1 -1
- package/dist/plugin.mjs +126 -46
- package/dist/plugin.mjs.map +1 -1
- package/dist/types.d.cts +2 -2
- package/dist/types.d.mts +2 -2
- package/dist/utils/theme.cjs +1 -0
- package/dist/utils/theme.cjs.map +1 -1
- package/dist/utils/theme.mjs +1 -0
- package/dist/utils/theme.mjs.map +1 -1
- package/package.json +1 -1
- package/src/cli/init.ts +60 -11
- package/src/cli/prompts.ts +34 -16
- package/src/cli/snapshot.ts +46 -0
- package/src/cli/status.ts +85 -0
- package/src/cli/sync.ts +239 -0
- package/src/cli/upgrade.ts +165 -0
- package/src/cli.ts +152 -5
- package/src/contract.meta.ts +36 -6
- package/src/contract.ts +74 -7
- package/src/plugin.ts +156 -45
- package/src/utils/theme.ts +1 -0
package/src/cli/init.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { spawn } from "node:child_process";
|
|
2
|
+
import { createHash } from "node:crypto";
|
|
2
3
|
import {
|
|
3
4
|
createWriteStream,
|
|
4
5
|
existsSync,
|
|
@@ -19,6 +20,7 @@ import {
|
|
|
19
20
|
normalizePackageManifestsInTree,
|
|
20
21
|
} from "../internal/manifest-normalizer";
|
|
21
22
|
import type { BosConfig } from "../types";
|
|
23
|
+
import { writeSnapshot } from "./snapshot";
|
|
22
24
|
|
|
23
25
|
const require = createRequire(import.meta.url);
|
|
24
26
|
|
|
@@ -29,8 +31,8 @@ interface SourceResult {
|
|
|
29
31
|
}
|
|
30
32
|
|
|
31
33
|
export async function resolveSourceDir(opts: {
|
|
32
|
-
|
|
33
|
-
|
|
34
|
+
extendsAccount: string;
|
|
35
|
+
extendsGateway: string;
|
|
34
36
|
source?: string;
|
|
35
37
|
}): Promise<SourceResult> {
|
|
36
38
|
if (opts.source) {
|
|
@@ -44,7 +46,7 @@ export async function resolveSourceDir(opts: {
|
|
|
44
46
|
return { sourceDir, parentConfig, cleanup: async () => {} };
|
|
45
47
|
}
|
|
46
48
|
|
|
47
|
-
const parentConfig = await fetchParentConfig(opts.
|
|
49
|
+
const parentConfig = await fetchParentConfig(opts.extendsAccount, opts.extendsGateway);
|
|
48
50
|
|
|
49
51
|
if (!parentConfig.repository) {
|
|
50
52
|
throw new Error("Parent config has no repository field — cannot locate template source");
|
|
@@ -54,8 +56,11 @@ export async function resolveSourceDir(opts: {
|
|
|
54
56
|
return { sourceDir, parentConfig, cleanup };
|
|
55
57
|
}
|
|
56
58
|
|
|
57
|
-
export async function fetchParentConfig(
|
|
58
|
-
|
|
59
|
+
export async function fetchParentConfig(
|
|
60
|
+
extendsAccount: string,
|
|
61
|
+
extendsGateway: string,
|
|
62
|
+
): Promise<BosConfig> {
|
|
63
|
+
const bosUrl = `bos://${extendsAccount}/${extendsGateway}`;
|
|
59
64
|
return fetchBosConfigFromFastKv<BosConfig>(bosUrl);
|
|
60
65
|
}
|
|
61
66
|
|
|
@@ -187,9 +192,9 @@ export async function copyFilteredFiles(
|
|
|
187
192
|
export async function personalizeConfig(
|
|
188
193
|
destination: string,
|
|
189
194
|
opts: {
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
195
|
+
extendsAccount: string;
|
|
196
|
+
extendsGateway: string;
|
|
197
|
+
account?: string;
|
|
193
198
|
domain?: string;
|
|
194
199
|
workspaceOpts?: { localOverrides?: boolean; sourceDir?: string };
|
|
195
200
|
},
|
|
@@ -198,10 +203,10 @@ export async function personalizeConfig(
|
|
|
198
203
|
if (existsSync(configPath)) {
|
|
199
204
|
const config = JSON.parse(readFileSync(configPath, "utf-8")) as Record<string, unknown>;
|
|
200
205
|
|
|
201
|
-
config.extends = `bos://${opts.
|
|
206
|
+
config.extends = `bos://${opts.extendsAccount}/${opts.extendsGateway}`;
|
|
202
207
|
|
|
203
|
-
if (opts.
|
|
204
|
-
config.account = opts.
|
|
208
|
+
if (opts.account) {
|
|
209
|
+
config.account = opts.account;
|
|
205
210
|
}
|
|
206
211
|
if (opts.domain) {
|
|
207
212
|
config.domain = opts.domain;
|
|
@@ -352,6 +357,50 @@ async function resolveWorkspaceRefs(
|
|
|
352
357
|
}
|
|
353
358
|
}
|
|
354
359
|
|
|
360
|
+
export async function writeInitSnapshot(
|
|
361
|
+
destination: string,
|
|
362
|
+
extendsAccount: string,
|
|
363
|
+
extendsGateway: string,
|
|
364
|
+
sourceDir: string,
|
|
365
|
+
patterns: string[],
|
|
366
|
+
options: { withHost: boolean },
|
|
367
|
+
): Promise<void> {
|
|
368
|
+
const effectivePatterns = options.withHost
|
|
369
|
+
? [...patterns, "host/**"]
|
|
370
|
+
: patterns.filter((p) => !p.startsWith("host/") && p !== "host/**");
|
|
371
|
+
|
|
372
|
+
const allFiles = new Set<string>();
|
|
373
|
+
for (const pattern of effectivePatterns) {
|
|
374
|
+
const matches = await glob(pattern, {
|
|
375
|
+
cwd: sourceDir,
|
|
376
|
+
nodir: true,
|
|
377
|
+
dot: true,
|
|
378
|
+
absolute: false,
|
|
379
|
+
});
|
|
380
|
+
for (const match of matches) {
|
|
381
|
+
allFiles.add(match);
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
const fileHashes: Record<string, string> = {};
|
|
386
|
+
for (const filePath of allFiles) {
|
|
387
|
+
const src = join(sourceDir, filePath);
|
|
388
|
+
const stat = lstatSync(src);
|
|
389
|
+
if (!stat.isFile()) continue;
|
|
390
|
+
const content = readFileSync(src);
|
|
391
|
+
fileHashes[filePath] = computeHash(content);
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
await writeSnapshot(destination, {
|
|
395
|
+
parentRef: `bos://${extendsAccount}/${extendsGateway}`,
|
|
396
|
+
files: fileHashes,
|
|
397
|
+
});
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
function computeHash(data: Uint8Array): string {
|
|
401
|
+
return createHash("sha256").update(data).digest("hex").substring(0, 16);
|
|
402
|
+
}
|
|
403
|
+
|
|
355
404
|
function mkTmpDir(prefix: string): string {
|
|
356
405
|
const base = join(tmpdir(), prefix);
|
|
357
406
|
let attempt = 0;
|
package/src/cli/prompts.ts
CHANGED
|
@@ -25,39 +25,57 @@ export async function promptYesNo(question: string, defaultVal = false): Promise
|
|
|
25
25
|
return answer.toLowerCase() === "y" || answer.toLowerCase() === "yes";
|
|
26
26
|
}
|
|
27
27
|
|
|
28
|
+
function deriveDirectoryFromDomain(domain: string): string {
|
|
29
|
+
const firstSegment = domain.split(".")[0];
|
|
30
|
+
return firstSegment || domain;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function deriveAccountFromDomain(domain: string, extendsAccount: string): string {
|
|
34
|
+
const firstSegment = domain.split(".")[0];
|
|
35
|
+
if (!firstSegment) return "";
|
|
36
|
+
const suffix = extendsAccount.includes(".")
|
|
37
|
+
? extendsAccount.substring(extendsAccount.indexOf(".") + 1)
|
|
38
|
+
: extendsAccount;
|
|
39
|
+
return `${firstSegment}.${suffix}`;
|
|
40
|
+
}
|
|
41
|
+
|
|
28
42
|
export async function promptInitOptions(input: {
|
|
43
|
+
extendsAccount?: string;
|
|
44
|
+
extendsGateway?: string;
|
|
45
|
+
directory?: string;
|
|
29
46
|
account?: string;
|
|
30
|
-
gateway?: string;
|
|
31
|
-
destination?: string;
|
|
32
|
-
name?: string;
|
|
33
47
|
domain?: string;
|
|
34
48
|
withHost?: boolean;
|
|
35
49
|
}): Promise<{
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
50
|
+
extendsAccount: string;
|
|
51
|
+
extendsGateway: string;
|
|
52
|
+
directory: string;
|
|
53
|
+
account?: string;
|
|
40
54
|
domain?: string;
|
|
41
55
|
withHost: boolean;
|
|
42
56
|
}> {
|
|
43
|
-
const
|
|
57
|
+
const extendsAccount =
|
|
58
|
+
input.extendsAccount || (await prompt("Extends account", "dev.everything.near"));
|
|
44
59
|
|
|
45
|
-
const
|
|
60
|
+
const extendsGateway =
|
|
61
|
+
input.extendsGateway || (await prompt("Extends gateway", "everything.dev"));
|
|
46
62
|
|
|
47
|
-
const
|
|
63
|
+
const domain = input.domain || (await prompt("Project domain"));
|
|
48
64
|
|
|
49
|
-
const
|
|
65
|
+
const accountDefault = domain ? deriveAccountFromDomain(domain, extendsAccount) : "";
|
|
66
|
+
const account = input.account || (await prompt("Project NEAR account", accountDefault));
|
|
50
67
|
|
|
51
|
-
const
|
|
68
|
+
const directoryDefault = domain ? deriveDirectoryFromDomain(domain) : extendsGateway;
|
|
69
|
+
const directory = input.directory || (await prompt("Project directory", directoryDefault));
|
|
52
70
|
|
|
53
71
|
const withHost =
|
|
54
72
|
input.withHost !== undefined ? input.withHost : await promptYesNo("Include host?", false);
|
|
55
73
|
|
|
56
74
|
return {
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
75
|
+
extendsAccount,
|
|
76
|
+
extendsGateway,
|
|
77
|
+
directory,
|
|
78
|
+
account: account || undefined,
|
|
61
79
|
domain: domain || undefined,
|
|
62
80
|
withHost,
|
|
63
81
|
};
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
|
|
4
|
+
export interface SyncSnapshot {
|
|
5
|
+
parentRef: string;
|
|
6
|
+
timestamp: string;
|
|
7
|
+
files: Record<string, string>;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const SNAPSHOT_DIR = ".bos";
|
|
11
|
+
const SNAPSHOT_FILE = "sync-snapshot.json";
|
|
12
|
+
|
|
13
|
+
function snapshotPath(projectDir: string): string {
|
|
14
|
+
return join(projectDir, SNAPSHOT_DIR, SNAPSHOT_FILE);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export async function readSnapshot(projectDir: string): Promise<SyncSnapshot | null> {
|
|
18
|
+
const path = snapshotPath(projectDir);
|
|
19
|
+
if (!existsSync(path)) {
|
|
20
|
+
return null;
|
|
21
|
+
}
|
|
22
|
+
try {
|
|
23
|
+
const content = readFileSync(path, "utf-8");
|
|
24
|
+
return JSON.parse(content) as SyncSnapshot;
|
|
25
|
+
} catch {
|
|
26
|
+
return null;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export async function writeSnapshot(
|
|
31
|
+
projectDir: string,
|
|
32
|
+
data: { parentRef: string; files: Record<string, string> },
|
|
33
|
+
): Promise<void> {
|
|
34
|
+
const dir = join(projectDir, SNAPSHOT_DIR);
|
|
35
|
+
if (!existsSync(dir)) {
|
|
36
|
+
mkdirSync(dir, { recursive: true });
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const snapshot: SyncSnapshot = {
|
|
40
|
+
parentRef: data.parentRef,
|
|
41
|
+
timestamp: new Date().toISOString(),
|
|
42
|
+
files: data.files,
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
writeFileSync(snapshotPath(projectDir), `${JSON.stringify(snapshot, null, 2)}\n`);
|
|
46
|
+
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import type { StatusResult } from "../contract";
|
|
4
|
+
import { fetchBosConfigFromFastKv } from "../fastkv";
|
|
5
|
+
import { readSnapshot } from "./snapshot";
|
|
6
|
+
|
|
7
|
+
const FRAMEWORK_PACKAGES = ["everything-dev", "every-plugin"];
|
|
8
|
+
|
|
9
|
+
async function fetchLatestNpmVersion(packageName: string): Promise<string | null> {
|
|
10
|
+
try {
|
|
11
|
+
const response = await fetch(`https://registry.npmjs.org/${packageName}/latest`, {
|
|
12
|
+
headers: { Accept: "application/json" },
|
|
13
|
+
signal: AbortSignal.timeout(10_000),
|
|
14
|
+
});
|
|
15
|
+
if (!response.ok) return null;
|
|
16
|
+
const data = (await response.json()) as { version: string };
|
|
17
|
+
return data.version;
|
|
18
|
+
} catch {
|
|
19
|
+
return null;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function readInstalledVersion(projectDir: string, packageName: string): string | undefined {
|
|
24
|
+
const pkgPath = join(projectDir, "package.json");
|
|
25
|
+
if (!existsSync(pkgPath)) return undefined;
|
|
26
|
+
const pkg = JSON.parse(readFileSync(pkgPath, "utf-8")) as Record<string, unknown>;
|
|
27
|
+
const deps = (pkg.dependencies ?? {}) as Record<string, string>;
|
|
28
|
+
const devDeps = (pkg.devDependencies ?? {}) as Record<string, string>;
|
|
29
|
+
const version = deps[packageName] || devDeps[packageName];
|
|
30
|
+
if (!version) return undefined;
|
|
31
|
+
return version.replace(/^[\^~>=]+/, "");
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function checkEnvFile(projectDir: string): "found" | "missing" | "example-only" {
|
|
35
|
+
if (existsSync(join(projectDir, ".env"))) return "found";
|
|
36
|
+
if (existsSync(join(projectDir, ".env.example"))) return "example-only";
|
|
37
|
+
return "missing";
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
async function checkParentReachable(extendsRef: string | undefined): Promise<boolean | undefined> {
|
|
41
|
+
if (!extendsRef?.startsWith("bos://")) return undefined;
|
|
42
|
+
try {
|
|
43
|
+
const config = await fetchBosConfigFromFastKv(extendsRef);
|
|
44
|
+
return config !== null;
|
|
45
|
+
} catch {
|
|
46
|
+
return false;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export async function getStatus(projectDir: string): Promise<StatusResult> {
|
|
51
|
+
const configPath = join(projectDir, "bos.config.json");
|
|
52
|
+
if (!existsSync(configPath)) {
|
|
53
|
+
return {
|
|
54
|
+
status: "error",
|
|
55
|
+
error: "No bos.config.json found in current directory",
|
|
56
|
+
packages: [],
|
|
57
|
+
envFile: "missing",
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const config = JSON.parse(readFileSync(configPath, "utf-8")) as Record<string, unknown>;
|
|
62
|
+
|
|
63
|
+
const packages = [];
|
|
64
|
+
for (const name of FRAMEWORK_PACKAGES) {
|
|
65
|
+
const installed = readInstalledVersion(projectDir, name);
|
|
66
|
+
const latest = await fetchLatestNpmVersion(name);
|
|
67
|
+
packages.push({ name, installed, latest: latest ?? undefined });
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const snapshot = await readSnapshot(projectDir);
|
|
71
|
+
|
|
72
|
+
const extendsRef = config.extends as string | undefined;
|
|
73
|
+
const parentReachable = await checkParentReachable(extendsRef);
|
|
74
|
+
|
|
75
|
+
return {
|
|
76
|
+
status: "ok",
|
|
77
|
+
extends: extendsRef,
|
|
78
|
+
account: config.account as string | undefined,
|
|
79
|
+
domain: config.domain as string | undefined,
|
|
80
|
+
packages,
|
|
81
|
+
lastSync: snapshot?.timestamp,
|
|
82
|
+
envFile: checkEnvFile(projectDir),
|
|
83
|
+
parentReachable,
|
|
84
|
+
};
|
|
85
|
+
}
|
package/src/cli/sync.ts
ADDED
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import {
|
|
3
|
+
copyFileSync,
|
|
4
|
+
existsSync,
|
|
5
|
+
lstatSync,
|
|
6
|
+
mkdirSync,
|
|
7
|
+
readFileSync,
|
|
8
|
+
writeFileSync,
|
|
9
|
+
} from "node:fs";
|
|
10
|
+
import { dirname, join } from "node:path";
|
|
11
|
+
import { glob } from "glob";
|
|
12
|
+
import type { SyncOptions, SyncResult } from "../contract";
|
|
13
|
+
import { personalizeConfig, readTemplatekeep, resolveSourceDir, runBunInstall } from "./init";
|
|
14
|
+
import { readSnapshot, writeSnapshot } from "./snapshot";
|
|
15
|
+
|
|
16
|
+
function readExcludeFile(filePath: string): string[] {
|
|
17
|
+
if (!existsSync(filePath)) return [];
|
|
18
|
+
const content = readFileSync(filePath, "utf-8");
|
|
19
|
+
return content
|
|
20
|
+
.split("\n")
|
|
21
|
+
.map((line) => line.trim())
|
|
22
|
+
.filter((line) => line.length > 0 && !line.startsWith("#"));
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export async function readTemplatesyncExclude(sourceDir: string): Promise<string[]> {
|
|
26
|
+
return readExcludeFile(join(sourceDir, ".templatesync-exclude"));
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function readLocalSyncExcludes(projectDir: string): string[] {
|
|
30
|
+
return readExcludeFile(join(projectDir, ".bos", "sync-local-exclude"));
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function isExcluded(filePath: string, excludePatterns: string[]): boolean {
|
|
34
|
+
for (const pattern of excludePatterns) {
|
|
35
|
+
if (pattern.endsWith("/**")) {
|
|
36
|
+
const prefix = pattern.slice(0, -3);
|
|
37
|
+
if (filePath.startsWith(`${prefix}/`) || filePath === prefix) return true;
|
|
38
|
+
} else if (pattern.endsWith("/*")) {
|
|
39
|
+
const prefix = pattern.slice(0, -2);
|
|
40
|
+
const slashIdx = filePath.indexOf("/", prefix.length + 1);
|
|
41
|
+
if (filePath.startsWith(`${prefix}/`) && slashIdx === -1) return true;
|
|
42
|
+
} else if (filePath === pattern || filePath.startsWith(`${pattern}/`)) {
|
|
43
|
+
return true;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
return false;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function computeLocalHash(projectDir: string, filePath: string): string | null {
|
|
50
|
+
const fullPath = join(projectDir, filePath);
|
|
51
|
+
if (!existsSync(fullPath)) return null;
|
|
52
|
+
try {
|
|
53
|
+
const content = readFileSync(fullPath);
|
|
54
|
+
return createHash("sha256").update(content).digest("hex").substring(0, 16);
|
|
55
|
+
} catch {
|
|
56
|
+
return null;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function backupFiles(projectDir: string, filePaths: string[]): string | null {
|
|
61
|
+
const filesToBackup = filePaths.filter((f) => existsSync(join(projectDir, f)));
|
|
62
|
+
if (filesToBackup.length === 0) return null;
|
|
63
|
+
|
|
64
|
+
const timestamp = new Date().toISOString().replace(/[:.]/g, "-");
|
|
65
|
+
const backupDir = join(projectDir, ".bos", "sync-backup", timestamp);
|
|
66
|
+
|
|
67
|
+
for (const filePath of filesToBackup) {
|
|
68
|
+
const src = join(projectDir, filePath);
|
|
69
|
+
const dest = join(backupDir, filePath);
|
|
70
|
+
mkdirSync(dirname(dest), { recursive: true });
|
|
71
|
+
copyFileSync(src, dest);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
return backupDir;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export async function syncTemplate(projectDir: string, options: SyncOptions): Promise<SyncResult> {
|
|
78
|
+
const localConfig = JSON.parse(
|
|
79
|
+
readFileSync(join(projectDir, "bos.config.json"), "utf-8"),
|
|
80
|
+
) as Record<string, unknown>;
|
|
81
|
+
|
|
82
|
+
const extendsRef = localConfig.extends as string | undefined;
|
|
83
|
+
if (!extendsRef?.startsWith("bos://")) {
|
|
84
|
+
return {
|
|
85
|
+
status: "error",
|
|
86
|
+
updated: [],
|
|
87
|
+
skipped: [],
|
|
88
|
+
added: [],
|
|
89
|
+
error: "No extends field found in bos.config.json — cannot determine parent",
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const extendsMatch = extendsRef.match(/^bos:\/\/([^/]+)\/(.+)$/);
|
|
94
|
+
if (!extendsMatch) {
|
|
95
|
+
return {
|
|
96
|
+
status: "error",
|
|
97
|
+
updated: [],
|
|
98
|
+
skipped: [],
|
|
99
|
+
added: [],
|
|
100
|
+
error: `Invalid extends reference: ${extendsRef}`,
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const extendsAccount = extendsMatch[1];
|
|
105
|
+
const extendsGateway = extendsMatch[2];
|
|
106
|
+
|
|
107
|
+
const { sourceDir, cleanup } = await resolveSourceDir({ extendsAccount, extendsGateway });
|
|
108
|
+
|
|
109
|
+
try {
|
|
110
|
+
const patterns = await readTemplatekeep(sourceDir);
|
|
111
|
+
if (patterns.length === 0) {
|
|
112
|
+
return {
|
|
113
|
+
status: "error",
|
|
114
|
+
updated: [],
|
|
115
|
+
skipped: [],
|
|
116
|
+
added: [],
|
|
117
|
+
error: "No .templatekeep found in template source",
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const parentExcludes = await readTemplatesyncExclude(sourceDir);
|
|
122
|
+
const localExcludes = readLocalSyncExcludes(projectDir);
|
|
123
|
+
const excludePatterns = [...parentExcludes, ...localExcludes];
|
|
124
|
+
|
|
125
|
+
const allTemplateFiles = new Set<string>();
|
|
126
|
+
for (const pattern of patterns) {
|
|
127
|
+
const matches = await glob(pattern, {
|
|
128
|
+
cwd: sourceDir,
|
|
129
|
+
nodir: true,
|
|
130
|
+
dot: true,
|
|
131
|
+
absolute: false,
|
|
132
|
+
});
|
|
133
|
+
for (const match of matches) {
|
|
134
|
+
allTemplateFiles.add(match);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
const snapshot = await readSnapshot(projectDir);
|
|
139
|
+
|
|
140
|
+
const updated: string[] = [];
|
|
141
|
+
const skipped: string[] = [];
|
|
142
|
+
const added: string[] = [];
|
|
143
|
+
|
|
144
|
+
for (const filePath of allTemplateFiles) {
|
|
145
|
+
if (isExcluded(filePath, excludePatterns)) continue;
|
|
146
|
+
|
|
147
|
+
const localHash = computeLocalHash(projectDir, filePath);
|
|
148
|
+
const sourceContent = readFileSync(join(sourceDir, filePath));
|
|
149
|
+
const sourceHash = createHash("sha256").update(sourceContent).digest("hex").substring(0, 16);
|
|
150
|
+
|
|
151
|
+
if (localHash === null) {
|
|
152
|
+
added.push(filePath);
|
|
153
|
+
continue;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
if (localHash === sourceHash) continue;
|
|
157
|
+
|
|
158
|
+
const snapshotHash = snapshot?.files[filePath];
|
|
159
|
+
|
|
160
|
+
if (snapshotHash === undefined) {
|
|
161
|
+
updated.push(filePath);
|
|
162
|
+
continue;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
if (localHash === snapshotHash) {
|
|
166
|
+
updated.push(filePath);
|
|
167
|
+
} else {
|
|
168
|
+
if (options.force) {
|
|
169
|
+
updated.push(filePath);
|
|
170
|
+
} else {
|
|
171
|
+
skipped.push(filePath);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
if (options.dryRun) {
|
|
177
|
+
return {
|
|
178
|
+
status: "dry-run",
|
|
179
|
+
updated,
|
|
180
|
+
skipped,
|
|
181
|
+
added,
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
const filesToWrite = [...updated, ...added].filter((f) => !isExcluded(f, excludePatterns));
|
|
186
|
+
|
|
187
|
+
if (filesToWrite.length > 0) {
|
|
188
|
+
backupFiles(projectDir, filesToWrite);
|
|
189
|
+
|
|
190
|
+
for (const filePath of filesToWrite) {
|
|
191
|
+
const src = join(sourceDir, filePath);
|
|
192
|
+
const dest = join(projectDir, filePath);
|
|
193
|
+
mkdirSync(dirname(dest), { recursive: true });
|
|
194
|
+
writeFileSync(dest, readFileSync(src));
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
const newSnapshotFiles: Record<string, string> = {};
|
|
199
|
+
for (const filePath of allTemplateFiles) {
|
|
200
|
+
const src = join(sourceDir, filePath);
|
|
201
|
+
const stat = lstatSync(src);
|
|
202
|
+
if (!stat.isFile()) continue;
|
|
203
|
+
const content = readFileSync(src);
|
|
204
|
+
newSnapshotFiles[filePath] = createHash("sha256")
|
|
205
|
+
.update(content)
|
|
206
|
+
.digest("hex")
|
|
207
|
+
.substring(0, 16);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
await writeSnapshot(projectDir, {
|
|
211
|
+
parentRef: `bos://${extendsAccount}/${extendsGateway}`,
|
|
212
|
+
files: newSnapshotFiles,
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
const account = (localConfig.account as string) || extendsAccount;
|
|
216
|
+
const domain = (localConfig.domain as string) || extendsGateway;
|
|
217
|
+
|
|
218
|
+
await personalizeConfig(projectDir, {
|
|
219
|
+
extendsAccount,
|
|
220
|
+
extendsGateway,
|
|
221
|
+
account,
|
|
222
|
+
domain,
|
|
223
|
+
workspaceOpts: { sourceDir },
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
if (!options.noInstall) {
|
|
227
|
+
await runBunInstall(projectDir);
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
return {
|
|
231
|
+
status: "synced",
|
|
232
|
+
updated,
|
|
233
|
+
skipped,
|
|
234
|
+
added,
|
|
235
|
+
};
|
|
236
|
+
} finally {
|
|
237
|
+
await cleanup();
|
|
238
|
+
}
|
|
239
|
+
}
|