@waelio/cli 0.1.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/LICENSE +21 -0
- package/README.md +155 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +52 -0
- package/dist/localRepos.d.ts +29 -0
- package/dist/localRepos.js +137 -0
- package/dist/localRepos.test.d.ts +1 -0
- package/dist/localRepos.test.js +51 -0
- package/dist/server.d.ts +4 -0
- package/dist/server.js +366 -0
- package/dist/siteforge.d.ts +44 -0
- package/dist/siteforge.js +281 -0
- package/dist/siteforge.test.d.ts +1 -0
- package/dist/siteforge.test.js +53 -0
- package/package.json +52 -0
- package/ui/dist/assets/index-BJ1Dzrgp.css +1 -0
- package/ui/dist/assets/index-DS_pYwiX.js +18 -0
- package/ui/dist/favicon.svg +4 -0
- package/ui/dist/index.html +44 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Waelio
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
# waelio CLI + UI
|
|
2
|
+
|
|
3
|
+
TypeScript toolkit for building [`waelio/siteforge`](https://github.com/waelio/siteforge) from either the terminal or a browser UI.
|
|
4
|
+
|
|
5
|
+
## Recommended UI stack
|
|
6
|
+
|
|
7
|
+
This repo now uses **Vite + TypeScript + Vue** for the on-screen UI.
|
|
8
|
+
|
|
9
|
+
Why this stack fits well here:
|
|
10
|
+
|
|
11
|
+
- **Vite** keeps development fast
|
|
12
|
+
- **TypeScript** matches the rest of the repo and the build pipeline
|
|
13
|
+
- **Vue** fits naturally with the broader waelio ecosystem and is quick to iterate on
|
|
14
|
+
|
|
15
|
+
## What it does
|
|
16
|
+
|
|
17
|
+
- checks that required local tools are installed
|
|
18
|
+
- previews the build plan before running it
|
|
19
|
+
- clones `waelio/siteforge` into a working directory
|
|
20
|
+
- installs dependencies with `npm ci`
|
|
21
|
+
- runs `npm run build`
|
|
22
|
+
- shows the full workflow in a browser UI with live logs
|
|
23
|
+
|
|
24
|
+
## Requirements
|
|
25
|
+
|
|
26
|
+
- Node.js 20+
|
|
27
|
+
- npm
|
|
28
|
+
- git
|
|
29
|
+
- Go
|
|
30
|
+
|
|
31
|
+
## Install dependencies
|
|
32
|
+
|
|
33
|
+
```sh
|
|
34
|
+
npm install
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
## Run the UI in development
|
|
38
|
+
|
|
39
|
+
```sh
|
|
40
|
+
npm run dev
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
That starts:
|
|
44
|
+
|
|
45
|
+
- the API/build server on `http://localhost:3000`
|
|
46
|
+
- the Vite UI on `http://localhost:5173`
|
|
47
|
+
|
|
48
|
+
## Build everything
|
|
49
|
+
|
|
50
|
+
```sh
|
|
51
|
+
npm run build
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
## Run the built UI + API server
|
|
55
|
+
|
|
56
|
+
```sh
|
|
57
|
+
npm start
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
## CLI commands
|
|
61
|
+
|
|
62
|
+
### Check prerequisites
|
|
63
|
+
|
|
64
|
+
```sh
|
|
65
|
+
node dist/index.js doctor
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
### Build `siteforge`
|
|
69
|
+
|
|
70
|
+
```sh
|
|
71
|
+
node dist/index.js build
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
Options:
|
|
75
|
+
|
|
76
|
+
- `--repo <url>` — repository URL to build (defaults to `waelio/siteforge`)
|
|
77
|
+
- `--ref <ref>` — branch, tag, or commit to checkout before building
|
|
78
|
+
- `--source <path>` — use an existing local checkout instead of cloning
|
|
79
|
+
- `--workdir <path>` — directory to clone into when `--source` is not provided
|
|
80
|
+
- `--dry-run` — print the build plan without executing it
|
|
81
|
+
|
|
82
|
+
### Start the local UI/API server from the CLI
|
|
83
|
+
|
|
84
|
+
```sh
|
|
85
|
+
node dist/index.js ui --port 3000
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
### Scaffold a Next.js + NestJS project from a blueprint
|
|
89
|
+
|
|
90
|
+
```sh
|
|
91
|
+
node dist/index.js scaffold ./blueprint.json --out ./sites
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
Options:
|
|
95
|
+
|
|
96
|
+
- `--out <dir>` — output root (defaults to `siteforge/sites`)
|
|
97
|
+
- `--no-git` — skip `git init` and the initial commit
|
|
98
|
+
|
|
99
|
+
The blueprint is a JSON file produced by siteforge that describes the project
|
|
100
|
+
name, slug, and selections (pages, features, integrations, locales, roles,
|
|
101
|
+
brand tones, visual styles, content models, and SEO focuses). The scaffolder
|
|
102
|
+
generates a frontend (Next.js) and backend (NestJS) workspace from those
|
|
103
|
+
selections.
|
|
104
|
+
|
|
105
|
+
## Helper repos surfaced in the UI
|
|
106
|
+
|
|
107
|
+
- `waelio/ustore` — storage and state patterns
|
|
108
|
+
- `waelio/utils` — shared utilities and UI-friendly helpers
|
|
109
|
+
- `waelio/waelio-messaging` — future real-time collaboration ideas
|
|
110
|
+
|
|
111
|
+
## Localization
|
|
112
|
+
|
|
113
|
+
UI strings are stored as per-locale JSON files under `src/locals/<lang>/<lang>.json`,
|
|
114
|
+
with a top-level `src/locals/manifest.json` summarizing the set. The following
|
|
115
|
+
locales ship by default:
|
|
116
|
+
|
|
117
|
+
`ar`, `de`, `en`, `es`, `fr`, `he`, `id`, `it`, `ru`, `sv`, `tr`, `zh`
|
|
118
|
+
|
|
119
|
+
RTL locales (`ar`, `he`) should be considered when adding or editing UI copy.
|
|
120
|
+
|
|
121
|
+
## Scripts
|
|
122
|
+
|
|
123
|
+
- `npm run dev` — run the API server and Vite UI together
|
|
124
|
+
- `npm run build` — build the CLI (`tsc`) and the UI (`vite build`)
|
|
125
|
+
- `npm start` — run the built server (`dist/server.js`)
|
|
126
|
+
- `npm run typecheck` — `tsc --noEmit` plus `vue-tsc` for the UI
|
|
127
|
+
- `npm test` — run `*.test.ts` via `tsx --test`
|
|
128
|
+
- `npm run check` — typecheck + tests
|
|
129
|
+
|
|
130
|
+
## Local repository discovery
|
|
131
|
+
|
|
132
|
+
The UI and API now scan your local GitHub workspace and build a sanitized repository index.
|
|
133
|
+
|
|
134
|
+
- default local root: `/Users/waelio/Code/GitHub`
|
|
135
|
+
- override with `WAELIO_LOCAL_ROOT`
|
|
136
|
+
- top-level repositories are included
|
|
137
|
+
- nested build/checkouts such as `.build`, `node_modules`, `dist`, and `.git` internals are excluded from discovery
|
|
138
|
+
|
|
139
|
+
### Local repo API
|
|
140
|
+
|
|
141
|
+
- `GET /api/local-repos` — returns the compiled local repository list
|
|
142
|
+
- `GET /api/local-repos/tree?repoId=...&path=...` — returns a sanitized physical folder listing for a selected local repository
|
|
143
|
+
|
|
144
|
+
### Safety rules
|
|
145
|
+
|
|
146
|
+
- repository IDs map to scanned local repos only
|
|
147
|
+
- folder browsing is restricted to paths inside the selected repository
|
|
148
|
+
- path traversal such as `..` is rejected
|
|
149
|
+
- `.git` directories are hidden from the served listing
|
|
150
|
+
|
|
151
|
+
## Notes
|
|
152
|
+
|
|
153
|
+
- The default repository URL is `https://github.com/waelio/siteforge.git`.
|
|
154
|
+
- A custom workdir can be used when you want a persistent local checkout.
|
|
155
|
+
- If `source` is provided, cloning is skipped and the existing checkout is built directly.
|
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { Command } from "commander";
|
|
3
|
+
import { DEFAULT_SITEFORGE_REPO, runBuild, runDoctor } from "./siteforge.js";
|
|
4
|
+
async function main() {
|
|
5
|
+
const program = new Command();
|
|
6
|
+
program
|
|
7
|
+
.name("waelio")
|
|
8
|
+
.description("Build the waelio/siteforge website from a local checkout or fresh clone")
|
|
9
|
+
.version("0.1.0")
|
|
10
|
+
.showHelpAfterError();
|
|
11
|
+
program
|
|
12
|
+
.command("doctor")
|
|
13
|
+
.description("Check whether git, npm, and Go are installed")
|
|
14
|
+
.action(async () => {
|
|
15
|
+
await runDoctor();
|
|
16
|
+
});
|
|
17
|
+
program
|
|
18
|
+
.command("build")
|
|
19
|
+
.description("Clone and build the waelio/siteforge website")
|
|
20
|
+
.option("--repo <url>", "repository URL to build", DEFAULT_SITEFORGE_REPO)
|
|
21
|
+
.option("--ref <ref>", "branch, tag, or commit to checkout before building")
|
|
22
|
+
.option("--source <path>", "use an existing local siteforge checkout instead of cloning")
|
|
23
|
+
.option("--workdir <path>", "directory to clone into when --source is not provided")
|
|
24
|
+
.option("--dry-run", "print the build plan without executing it", false)
|
|
25
|
+
.action(async (options) => {
|
|
26
|
+
await runBuild({
|
|
27
|
+
repoUrl: options.repo,
|
|
28
|
+
ref: options.ref,
|
|
29
|
+
source: options.source,
|
|
30
|
+
workdir: options.workdir,
|
|
31
|
+
dryRun: options.dryRun,
|
|
32
|
+
});
|
|
33
|
+
});
|
|
34
|
+
program
|
|
35
|
+
.command("ui")
|
|
36
|
+
.description("Start the local web UI and API server")
|
|
37
|
+
.option("--port <number>", "port for the local server", "3000")
|
|
38
|
+
.action(async (options) => {
|
|
39
|
+
const { startServer } = await import("./server.js");
|
|
40
|
+
const port = Number(options.port);
|
|
41
|
+
if (!Number.isInteger(port) || port <= 0) {
|
|
42
|
+
throw new Error(`Invalid port: ${options.port}`);
|
|
43
|
+
}
|
|
44
|
+
await startServer({ port });
|
|
45
|
+
});
|
|
46
|
+
await program.parseAsync(process.argv);
|
|
47
|
+
}
|
|
48
|
+
main().catch((error) => {
|
|
49
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
50
|
+
console.error(`\nError: ${message}`);
|
|
51
|
+
process.exitCode = 1;
|
|
52
|
+
});
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
export declare const DEFAULT_LOCAL_REPOS_ROOT: string;
|
|
2
|
+
export interface LocalRepositorySummary {
|
|
3
|
+
id: string;
|
|
4
|
+
name: string;
|
|
5
|
+
owner: string;
|
|
6
|
+
relativePath: string;
|
|
7
|
+
absolutePath: string;
|
|
8
|
+
}
|
|
9
|
+
export interface LocalRepositoriesSnapshot {
|
|
10
|
+
root: string;
|
|
11
|
+
repositories: LocalRepositorySummary[];
|
|
12
|
+
}
|
|
13
|
+
export interface LocalDirectoryEntry {
|
|
14
|
+
name: string;
|
|
15
|
+
kind: "directory" | "file";
|
|
16
|
+
relativePath: string;
|
|
17
|
+
absolutePath: string;
|
|
18
|
+
hidden: boolean;
|
|
19
|
+
}
|
|
20
|
+
export interface LocalDirectoryListing {
|
|
21
|
+
repo: LocalRepositorySummary;
|
|
22
|
+
requestedPath: string;
|
|
23
|
+
absolutePath: string;
|
|
24
|
+
entries: LocalDirectoryEntry[];
|
|
25
|
+
}
|
|
26
|
+
export declare function scanLocalRepositories(root?: string): Promise<LocalRepositoriesSnapshot>;
|
|
27
|
+
export declare function listLocalRepositoryDirectory(snapshot: LocalRepositoriesSnapshot, repositoryId: string, requestedPath?: string): Promise<LocalDirectoryListing>;
|
|
28
|
+
export declare function sanitizeRepositoryRelativePath(value?: string): string;
|
|
29
|
+
export declare function createLocalRepositoryId(relativePath: string): string;
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import { readdir, stat } from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
const configuredLocalReposRoot = process.env.WAELIO_LOCAL_ROOT?.trim();
|
|
4
|
+
export const DEFAULT_LOCAL_REPOS_ROOT = path.resolve(configuredLocalReposRoot && configuredLocalReposRoot.length > 0
|
|
5
|
+
? configuredLocalReposRoot
|
|
6
|
+
: "/Users/waelio/Code/GitHub");
|
|
7
|
+
const IGNORED_TOP_LEVEL_NAMES = new Set([".DS_Store"]);
|
|
8
|
+
const IGNORED_SECOND_LEVEL_NAMES = new Set([".build", ".git", "build", "dist", "node_modules", "tmp"]);
|
|
9
|
+
const IGNORED_DIRECTORY_NAMES = new Set([".git"]);
|
|
10
|
+
export async function scanLocalRepositories(root = DEFAULT_LOCAL_REPOS_ROOT) {
|
|
11
|
+
const resolvedRoot = path.resolve(root);
|
|
12
|
+
const repositories = new Map();
|
|
13
|
+
for (const candidate of await readDirectoryCandidates(resolvedRoot, IGNORED_TOP_LEVEL_NAMES)) {
|
|
14
|
+
if (await hasGitMarker(candidate.absolutePath)) {
|
|
15
|
+
const repository = createRepositorySummary(resolvedRoot, candidate.absolutePath);
|
|
16
|
+
repositories.set(repository.relativePath, repository);
|
|
17
|
+
continue;
|
|
18
|
+
}
|
|
19
|
+
for (const nestedCandidate of await readDirectoryCandidates(candidate.absolutePath, IGNORED_SECOND_LEVEL_NAMES)) {
|
|
20
|
+
if (!(await hasGitMarker(nestedCandidate.absolutePath))) {
|
|
21
|
+
continue;
|
|
22
|
+
}
|
|
23
|
+
const repository = createRepositorySummary(resolvedRoot, nestedCandidate.absolutePath);
|
|
24
|
+
repositories.set(repository.relativePath, repository);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
return {
|
|
28
|
+
root: resolvedRoot,
|
|
29
|
+
repositories: [...repositories.values()].sort((left, right) => left.relativePath.localeCompare(right.relativePath)),
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
export async function listLocalRepositoryDirectory(snapshot, repositoryId, requestedPath) {
|
|
33
|
+
const repository = snapshot.repositories.find((entry) => entry.id === repositoryId);
|
|
34
|
+
if (!repository) {
|
|
35
|
+
throw new Error(`Unknown local repository id: ${repositoryId}`);
|
|
36
|
+
}
|
|
37
|
+
const sanitizedPath = sanitizeRepositoryRelativePath(requestedPath);
|
|
38
|
+
const targetPath = path.resolve(repository.absolutePath, sanitizedPath);
|
|
39
|
+
if (!isPathInsideRepository(targetPath, repository.absolutePath)) {
|
|
40
|
+
throw new Error(`Invalid path outside repository: ${requestedPath ?? ""}`);
|
|
41
|
+
}
|
|
42
|
+
const details = await stat(targetPath);
|
|
43
|
+
if (!details.isDirectory()) {
|
|
44
|
+
throw new Error(`Requested path is not a directory: ${sanitizedPath}`);
|
|
45
|
+
}
|
|
46
|
+
const dirents = await readdir(targetPath, { withFileTypes: true });
|
|
47
|
+
const entries = dirents
|
|
48
|
+
.filter((entry) => !IGNORED_DIRECTORY_NAMES.has(entry.name))
|
|
49
|
+
.map((entry) => {
|
|
50
|
+
const childRelativePath = sanitizedPath.length > 0 ? path.posix.join(sanitizedPath, entry.name) : entry.name;
|
|
51
|
+
const childAbsolutePath = path.join(targetPath, entry.name);
|
|
52
|
+
return {
|
|
53
|
+
name: entry.name,
|
|
54
|
+
kind: entry.isDirectory() ? "directory" : "file",
|
|
55
|
+
relativePath: childRelativePath,
|
|
56
|
+
absolutePath: childAbsolutePath,
|
|
57
|
+
hidden: entry.name.startsWith("."),
|
|
58
|
+
};
|
|
59
|
+
})
|
|
60
|
+
.sort((left, right) => {
|
|
61
|
+
if (left.kind !== right.kind) {
|
|
62
|
+
return left.kind === "directory" ? -1 : 1;
|
|
63
|
+
}
|
|
64
|
+
return left.name.localeCompare(right.name);
|
|
65
|
+
});
|
|
66
|
+
return {
|
|
67
|
+
repo: repository,
|
|
68
|
+
requestedPath: sanitizedPath,
|
|
69
|
+
absolutePath: targetPath,
|
|
70
|
+
entries,
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
export function sanitizeRepositoryRelativePath(value) {
|
|
74
|
+
if (typeof value !== "string") {
|
|
75
|
+
return "";
|
|
76
|
+
}
|
|
77
|
+
const trimmed = value.trim();
|
|
78
|
+
if (trimmed.length === 0) {
|
|
79
|
+
return "";
|
|
80
|
+
}
|
|
81
|
+
const normalized = path.posix.normalize(trimmed.replace(/\\/g, "/")).replace(/^\/+/, "");
|
|
82
|
+
if (normalized === ".") {
|
|
83
|
+
return "";
|
|
84
|
+
}
|
|
85
|
+
if (normalized === ".." || normalized.startsWith("../") || normalized.includes("/../")) {
|
|
86
|
+
throw new Error(`Invalid path outside repository: ${value}`);
|
|
87
|
+
}
|
|
88
|
+
return normalized;
|
|
89
|
+
}
|
|
90
|
+
export function createLocalRepositoryId(relativePath) {
|
|
91
|
+
return Buffer.from(relativePath, "utf8").toString("base64url");
|
|
92
|
+
}
|
|
93
|
+
async function readDirectoryCandidates(directoryPath, ignoredNames) {
|
|
94
|
+
try {
|
|
95
|
+
const dirents = await readdir(directoryPath, { withFileTypes: true });
|
|
96
|
+
return dirents
|
|
97
|
+
.filter((entry) => entry.isDirectory())
|
|
98
|
+
.filter((entry) => !entry.name.startsWith("."))
|
|
99
|
+
.filter((entry) => !ignoredNames.has(entry.name))
|
|
100
|
+
.map((entry) => ({
|
|
101
|
+
name: entry.name,
|
|
102
|
+
absolutePath: path.join(directoryPath, entry.name),
|
|
103
|
+
}));
|
|
104
|
+
}
|
|
105
|
+
catch {
|
|
106
|
+
return [];
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
async function hasGitMarker(directoryPath) {
|
|
110
|
+
return pathExists(path.join(directoryPath, ".git"));
|
|
111
|
+
}
|
|
112
|
+
function createRepositorySummary(root, absolutePath) {
|
|
113
|
+
const relativePath = path.relative(root, absolutePath);
|
|
114
|
+
const parts = relativePath.split(path.sep).filter(Boolean);
|
|
115
|
+
const owner = parts.length > 1 ? parts[0] ?? "" : "local";
|
|
116
|
+
const name = parts[parts.length - 1] ?? relativePath;
|
|
117
|
+
return {
|
|
118
|
+
id: createLocalRepositoryId(relativePath),
|
|
119
|
+
name,
|
|
120
|
+
owner,
|
|
121
|
+
relativePath,
|
|
122
|
+
absolutePath,
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
function isPathInsideRepository(targetPath, repositoryPath) {
|
|
126
|
+
const relative = path.relative(repositoryPath, targetPath);
|
|
127
|
+
return relative === "" || (!relative.startsWith("..") && !path.isAbsolute(relative));
|
|
128
|
+
}
|
|
129
|
+
async function pathExists(targetPath) {
|
|
130
|
+
try {
|
|
131
|
+
await stat(targetPath);
|
|
132
|
+
return true;
|
|
133
|
+
}
|
|
134
|
+
catch {
|
|
135
|
+
return false;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { mkdir, mkdtemp, rm, writeFile } from "node:fs/promises";
|
|
3
|
+
import os from "node:os";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import test from "node:test";
|
|
6
|
+
import { createLocalRepositoryId, listLocalRepositoryDirectory, sanitizeRepositoryRelativePath, scanLocalRepositories, } from "./localRepos.js";
|
|
7
|
+
test("scanLocalRepositories returns top-level local repositories only", async () => {
|
|
8
|
+
const root = await mkdtemp(path.join(os.tmpdir(), "waelio-local-repos-"));
|
|
9
|
+
try {
|
|
10
|
+
await mkdir(path.join(root, "waelio", "cli", ".git"), { recursive: true });
|
|
11
|
+
await mkdir(path.join(root, "waelio", "siteforge", ".git"), { recursive: true });
|
|
12
|
+
await mkdir(path.join(root, "waelio", "siteforge", ".build", "checkouts", "nested", ".git"), {
|
|
13
|
+
recursive: true,
|
|
14
|
+
});
|
|
15
|
+
await mkdir(path.join(root, "peace2074", "peace2074.com", ".git"), { recursive: true });
|
|
16
|
+
const snapshot = await scanLocalRepositories(root);
|
|
17
|
+
assert.deepEqual(snapshot.repositories.map((repository) => repository.relativePath), ["peace2074/peace2074.com", "waelio/cli", "waelio/siteforge"]);
|
|
18
|
+
}
|
|
19
|
+
finally {
|
|
20
|
+
await rm(root, { recursive: true, force: true });
|
|
21
|
+
}
|
|
22
|
+
});
|
|
23
|
+
test("listLocalRepositoryDirectory returns sanitized directory entries", async () => {
|
|
24
|
+
const root = await mkdtemp(path.join(os.tmpdir(), "waelio-local-repos-"));
|
|
25
|
+
try {
|
|
26
|
+
const repositoryRoot = path.join(root, "waelio", "cli");
|
|
27
|
+
await mkdir(path.join(repositoryRoot, ".git"), { recursive: true });
|
|
28
|
+
await mkdir(path.join(repositoryRoot, "src"), { recursive: true });
|
|
29
|
+
await writeFile(path.join(repositoryRoot, "README.md"), "# demo\n");
|
|
30
|
+
await writeFile(path.join(repositoryRoot, "src", "index.ts"), "export {};\n");
|
|
31
|
+
const snapshot = await scanLocalRepositories(root);
|
|
32
|
+
const repository = snapshot.repositories[0];
|
|
33
|
+
assert.ok(repository);
|
|
34
|
+
assert.equal(repository?.id, createLocalRepositoryId("waelio/cli"));
|
|
35
|
+
const rootListing = await listLocalRepositoryDirectory(snapshot, repository.id);
|
|
36
|
+
assert.equal(rootListing.entries[0]?.name, "src");
|
|
37
|
+
assert.equal(rootListing.entries[1]?.name, "README.md");
|
|
38
|
+
const nestedListing = await listLocalRepositoryDirectory(snapshot, repository.id, "src");
|
|
39
|
+
assert.equal(nestedListing.requestedPath, "src");
|
|
40
|
+
assert.equal(nestedListing.entries[0]?.relativePath, "src/index.ts");
|
|
41
|
+
await assert.rejects(() => listLocalRepositoryDirectory(snapshot, repository.id, "../outside"), /Invalid path outside repository/);
|
|
42
|
+
}
|
|
43
|
+
finally {
|
|
44
|
+
await rm(root, { recursive: true, force: true });
|
|
45
|
+
}
|
|
46
|
+
});
|
|
47
|
+
test("sanitizeRepositoryRelativePath normalizes safe paths", () => {
|
|
48
|
+
assert.equal(sanitizeRepositoryRelativePath("src/components"), "src/components");
|
|
49
|
+
assert.equal(sanitizeRepositoryRelativePath("./src/../src/views"), "src/views");
|
|
50
|
+
assert.equal(sanitizeRepositoryRelativePath(""), "");
|
|
51
|
+
});
|
package/dist/server.d.ts
ADDED