@iamsaroj/replicax 0.0.4 → 0.0.6
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/README.md +102 -82
- package/dist/index.js +325 -115
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -5,23 +5,25 @@
|
|
|
5
5
|
<h3><em>Copy the setup, not the code.</em></h3>
|
|
6
6
|
|
|
7
7
|
<p>
|
|
8
|
-
Extract a project's entire development environment
|
|
8
|
+
Extract a project's entire development environment - <strong>tooling, folder structure, and conventions</strong>
|
|
9
9
|
into a portable profile, then recreate it anywhere in seconds.<br/>
|
|
10
10
|
None of your business code. None of your secrets. Just the setup.
|
|
11
11
|
</p>
|
|
12
12
|
|
|
13
13
|
[](https://www.npmjs.com/package/@iamsaroj/replicax)
|
|
14
|
+
[](https://github.com/khanalsaroj/replicaX/actions/workflows/ci.yml)
|
|
15
|
+
[](https://github.com/khanalsaroj/replicaX/actions/workflows/release.yml)
|
|
14
16
|
[](https://nodejs.org)
|
|
15
17
|
[](https://www.typescriptlang.org)
|
|
16
18
|

|
|
17
19
|
[](LICENSE)
|
|
18
20
|
|
|
19
21
|
<sub>
|
|
20
|
-
<a href="#quick-start"><b>Quick start</b></a>  
|
|
21
|
-
<a href="#commands"><b>Commands</b></a>  
|
|
22
|
-
<a href="#what-gets-captured"><b>What gets captured</b></a>  
|
|
23
|
-
<a href="#security"><b>Security</b></a>  
|
|
24
|
-
<a href="#how-it-works"><b>How it works</b></a>  
|
|
22
|
+
<a href="#quick-start"><b>Quick start</b></a> |
|
|
23
|
+
<a href="#commands"><b>Commands</b></a> |
|
|
24
|
+
<a href="#what-gets-captured"><b>What gets captured</b></a> |
|
|
25
|
+
<a href="#security"><b>Security</b></a> |
|
|
26
|
+
<a href="#how-it-works"><b>How it works</b></a> |
|
|
25
27
|
<a href="#faq"><b>FAQ</b></a>
|
|
26
28
|
</sub>
|
|
27
29
|
|
|
@@ -29,14 +31,14 @@
|
|
|
29
31
|
|
|
30
32
|
---
|
|
31
33
|
|
|
32
|
-
> **ReplicaX captures the _setup_ of a project
|
|
33
|
-
the _implementation_ behind.**
|
|
34
|
+
> **ReplicaX captures the _setup_ of a project - the parts you copy-paste between every new repo - and leaves
|
|
35
|
+
> the _implementation_ behind.**
|
|
34
36
|
|
|
35
37
|
Every new project starts the same way: copy `tsconfig.json`, port the ESLint and Prettier config, recreate the
|
|
36
|
-
Dockerfiles, re
|
|
38
|
+
Dockerfiles, re-add the CI workflow, rebuild `src/`'s folder layout. It's slow, error-prone, and quietly drifts out of
|
|
37
39
|
sync across a team.
|
|
38
40
|
|
|
39
|
-
ReplicaX captures that ritual **once** and replays it **on demand**
|
|
41
|
+
ReplicaX captures that ritual **once** and replays it **on demand** - locally, or straight from any GitHub repo.
|
|
40
42
|
|
|
41
43
|
> It is **not** a code generator, a project cloner, or a backup tool.
|
|
42
44
|
|
|
@@ -46,7 +48,7 @@ ReplicaX captures that ritual **once** and replays it **on demand** — locally,
|
|
|
46
48
|
|
|
47
49
|
| | Without ReplicaX | With ReplicaX |
|
|
48
50
|
|-----------------------|--------------------------------------------|---------------------------------|
|
|
49
|
-
| **New project setup** | 30+ minutes of copy
|
|
51
|
+
| **New project setup** | 30+ minutes of copy-paste from an old repo | `replicax create my-app` |
|
|
50
52
|
| **What you copy** | Whatever you remember to grab | A complete, validated profile |
|
|
51
53
|
| **Secrets** | One stray `.env` away from a leak | Blocked unconditionally |
|
|
52
54
|
| **Team consistency** | Drifts repo to repo | One shareable `.tar.gz` profile |
|
|
@@ -58,16 +60,16 @@ ReplicaX captures that ritual **once** and replays it **on demand** — locally,
|
|
|
58
60
|
|
|
59
61
|
| | |
|
|
60
62
|
|-------------------------------|------------------------------------------------------------------------------------------------------------------|
|
|
61
|
-
| **One
|
|
62
|
-
| **Capture any GitHub repo** | `extract owner/repo` profiles a remote repo
|
|
63
|
-
| **One
|
|
64
|
-
| **AI assistant skills** | `init-skill` uses your own AI (Claude
|
|
65
|
-
| **`.ts` _and_ `.js` configs** | Copied byte
|
|
66
|
-
| **Secret
|
|
63
|
+
| **One-command capture** | `init` scans the current project into a reusable profile. |
|
|
64
|
+
| **Capture any GitHub repo** | `extract owner/repo` profiles a remote repo - no clone, no `git` required. |
|
|
65
|
+
| **One-command scaffold** | `create` reproduces the setup in a fresh directory. |
|
|
66
|
+
| **AI assistant skills** | `init-skill` uses your own AI (Claude / Codex / Gemini) to author a ready-to-use skill for agentic coding tools. |
|
|
67
|
+
| **`.ts` _and_ `.js` configs** | Copied byte-for-byte - never compiled, never executed. |
|
|
68
|
+
| **Secret-safe by design** | `.env`, keys, and certs can never enter a profile; `.npmrc` tokens are stripped automatically. |
|
|
67
69
|
| **No business code** | Folders are recreated empty; source files are never read. |
|
|
68
|
-
| **Stays in sync** | `sync --diff` shows what drifted; `validate` verifies integrity via SHA
|
|
70
|
+
| **Stays in sync** | `sync --diff` shows what drifted; `validate` verifies integrity via SHA-256. |
|
|
69
71
|
| **Portable & shareable** | `export` / `import` a whole profile as a single `.tar.gz`. |
|
|
70
|
-
| **`.replicaxignore`** | gitignore
|
|
72
|
+
| **`.replicaxignore`** | gitignore-style control over exactly what gets captured. |
|
|
71
73
|
|
|
72
74
|
---
|
|
73
75
|
|
|
@@ -86,15 +88,15 @@ pnpm add -g @iamsaroj/replicax
|
|
|
86
88
|
## Quick start
|
|
87
89
|
|
|
88
90
|
```bash
|
|
89
|
-
# 1
|
|
91
|
+
# 1 In an existing, well-configured project
|
|
90
92
|
cd my-project
|
|
91
|
-
replicax init #
|
|
93
|
+
replicax init # -> writes a profile to .replicax/
|
|
92
94
|
|
|
93
|
-
# 2
|
|
94
|
-
replicax create my-new-app #
|
|
95
|
+
# 2 Anywhere the profile lives, scaffold a fresh project
|
|
96
|
+
replicax create my-new-app # -> same setup, none of the code
|
|
95
97
|
```
|
|
96
98
|
|
|
97
|
-
|
|
99
|
+
...and here's what `init` actually shows you:
|
|
98
100
|
|
|
99
101
|
```console
|
|
100
102
|
$ replicax init
|
|
@@ -136,14 +138,14 @@ my-app/
|
|
|
136
138
|
Create a project from it with: replicax create <project-name>
|
|
137
139
|
```
|
|
138
140
|
|
|
139
|
-
> 💡 **Tip:** run `replicax init --dry-run` first to preview exactly what would be captured
|
|
141
|
+
> 💡 **Tip:** run `replicax init --dry-run` first to preview exactly what would be captured - nothing is written.
|
|
140
142
|
|
|
141
143
|
---
|
|
142
144
|
|
|
143
145
|
## Before & After
|
|
144
146
|
|
|
145
147
|
Starting from a typical **Vite + React + TypeScript** project, `replicax init && replicax create my-new-app` produces a
|
|
146
|
-
clean skeleton
|
|
148
|
+
clean skeleton - same tooling, zero application code, zero secrets.
|
|
147
149
|
|
|
148
150
|
<table>
|
|
149
151
|
<tr>
|
|
@@ -164,14 +166,14 @@ my-project/
|
|
|
164
166
|
├── docker-compose.yml
|
|
165
167
|
├── .github/workflows/ci.yml
|
|
166
168
|
├── .husky/pre-commit
|
|
167
|
-
├── .env
|
|
168
|
-
├── package.json
|
|
169
|
+
├── .env <- secret
|
|
170
|
+
├── package.json <- runtime deps
|
|
169
171
|
└── src/
|
|
170
172
|
├── components/
|
|
171
|
-
│ └── Button.tsx
|
|
173
|
+
│ └── Button.tsx <- business code
|
|
172
174
|
├── hooks/
|
|
173
175
|
├── services/
|
|
174
|
-
│ └── UserService.ts
|
|
176
|
+
│ └── UserService.ts <- business code
|
|
175
177
|
└── pages/
|
|
176
178
|
</pre>
|
|
177
179
|
|
|
@@ -211,16 +213,16 @@ No `.env`. No `Button.tsx`. No `UserService.ts`. No `react` runtime dependency.
|
|
|
211
213
|
|
|
212
214
|
| Command | What it does |
|
|
213
215
|
|--------------------------------------------------------------------------------------|----------------------------------------------------|
|
|
214
|
-
| [`replicax init`](#replicax-init) | Scan the current project
|
|
216
|
+
| [`replicax init`](#replicax-init) | Scan the current project -> profile in `.replicax/` |
|
|
215
217
|
| [`replicax extract <repo>`](#replicax-extract-repo) | Profile a **remote GitHub repo** (no clone) |
|
|
216
218
|
| [`replicax create <name>`](#replicax-create-project-name) | Scaffold a new project from a profile |
|
|
217
219
|
| [`replicax sync`](#replicax-sync) | Update the profile from the current project |
|
|
218
220
|
| [`replicax inspect`](#replicax-inspect) | Display captured config & structure |
|
|
219
|
-
| [`replicax validate`](#replicax-validate) | Schema + integrity (SHA
|
|
221
|
+
| [`replicax validate`](#replicax-validate) | Schema + integrity (SHA-256) check CI-friendly |
|
|
220
222
|
| [`replicax export`](#replicax-export--import) / [`import`](#replicax-export--import) | Portable `.tar.gz` profile in/out |
|
|
221
|
-
| [`replicax init-skill`](#replicax-init-skill) | Author an AI
|
|
223
|
+
| [`replicax init-skill`](#replicax-init-skill) | Author an AI-assistant skill from your stack |
|
|
222
224
|
| [`replicax doctor`](#replicax-doctor) | Check which dev tools are installed locally |
|
|
223
|
-
| [`replicax compare <a> <b>`](#replicax-compare-source-target) | Diff two profiles/projects: tooling, config,
|
|
225
|
+
| [`replicax compare <a> <b>`](#replicax-compare-source-target) | Diff two profiles/projects: tooling, config, ... |
|
|
224
226
|
| [`replicax audit`](#replicax-audit) | Score a setup vs best practices + recommendations |
|
|
225
227
|
|
|
226
228
|
> Every write operation accepts `--dry-run` (preview, touch nothing) and `--verbose` (list every file).
|
|
@@ -237,7 +239,7 @@ replicax init --verbose # list every detected file
|
|
|
237
239
|
|
|
238
240
|
### `replicax extract <repo>`
|
|
239
241
|
|
|
240
|
-
Capture a profile from a **remote GitHub repository** instead of the current directory
|
|
242
|
+
Capture a profile from a **remote GitHub repository** instead of the current directory - the same scan as `init`,
|
|
241
243
|
pointed at a repo you don't even have checked out. The repo is downloaded as a tarball over the GitHub API (no `git`
|
|
242
244
|
required) into a temp directory, scanned, then discarded; only the profile is kept.
|
|
243
245
|
|
|
@@ -254,27 +256,36 @@ replicax extract owner/repo --dry-run # preview, write not
|
|
|
254
256
|
Accepts `owner/repo`, a `github.com` URL (including `/tree/<branch>` links), an ssh remote, or a `#branch` / `@tag`
|
|
255
257
|
suffix.
|
|
256
258
|
|
|
257
|
-
> **Private repos & rate limits:** set `GITHUB_TOKEN` (or `GH_TOKEN`) in your environment
|
|
259
|
+
> **Private repos & rate limits:** set `GITHUB_TOKEN` (or `GH_TOKEN`) in your environment - it's read from the
|
|
258
260
|
> environment, used for the one request, and **never stored**. The same secret guard applies, so a remote repo's
|
|
259
261
|
`.env` /
|
|
260
262
|
> keys are never captured.
|
|
261
263
|
|
|
262
264
|
### `replicax create <project-name>`
|
|
263
265
|
|
|
264
|
-
Create a new project from a profile. Existing files trigger an interactive overwrite/skip prompt (auto
|
|
265
|
-
non
|
|
266
|
+
Create a new project from a profile. Existing files trigger an interactive overwrite/skip prompt (auto-skips when
|
|
267
|
+
non-interactive).
|
|
266
268
|
|
|
267
269
|
```bash
|
|
268
270
|
replicax create my-app
|
|
269
271
|
replicax create my-app --profile ./shared/.replicax # use a profile elsewhere
|
|
270
|
-
replicax create my-app --skip-install #
|
|
272
|
+
replicax create my-app --skip-install # never run the package manager
|
|
273
|
+
replicax create my-app --install # install even for untrusted profiles
|
|
271
274
|
replicax create my-app --force # overwrite conflicts, no prompt
|
|
272
275
|
replicax create my-app --dry-run # preview the file plan
|
|
273
276
|
```
|
|
274
277
|
|
|
278
|
+
> **Dependency install is a trust boundary.** Running the package manager executes
|
|
279
|
+
> any `preinstall`/`postinstall` lifecycle scripts declared by the captured
|
|
280
|
+
> `devDependencies`. Profiles you made locally (`init`/`sync`) install by default.
|
|
281
|
+
> Profiles from **`extract` (remote)** or **`import` (an archive someone sent you)**
|
|
282
|
+
> are marked untrusted: `create` prints the package manager and the exact
|
|
283
|
+
> dependency list, then **stops** without installing. Review it, then run the
|
|
284
|
+
> install yourself or re-run with `--install`.
|
|
285
|
+
|
|
275
286
|
### `replicax sync`
|
|
276
287
|
|
|
277
|
-
Re
|
|
288
|
+
Re-scan and update the profile to match the current project.
|
|
278
289
|
|
|
279
290
|
```bash
|
|
280
291
|
replicax sync # update, print a change summary
|
|
@@ -300,13 +311,13 @@ Tooling (14 file(s))
|
|
|
300
311
|
│ Language & Type Checking │ tsconfig.json │ json │ 226 B │
|
|
301
312
|
│ Build Tools │ vite.config.ts │ ts │ 98 B │
|
|
302
313
|
│ Formatting │ .prettierrc │ other │ 63 B │
|
|
303
|
-
│
|
|
314
|
+
│ ... │ ... │ ... │ ... │
|
|
304
315
|
└────────────────────────────────┴──────────────────────────┴─────────┴──────────┘
|
|
305
316
|
```
|
|
306
317
|
|
|
307
318
|
### `replicax validate`
|
|
308
319
|
|
|
309
|
-
Check the profile's schema and integrity (SHA
|
|
320
|
+
Check the profile's schema and integrity (SHA-256 checksums + path safety). Exits non-zero on failure - handy in CI.
|
|
310
321
|
|
|
311
322
|
```bash
|
|
312
323
|
replicax validate
|
|
@@ -324,12 +335,12 @@ replicax import ./react-enterprise.tar.gz --force # overwrite an existing profi
|
|
|
324
335
|
|
|
325
336
|
### `replicax init-skill`
|
|
326
337
|
|
|
327
|
-
Generate an AI
|
|
338
|
+
Generate an AI-assistant **skill** from the current project - a ready-to-use bundle (an entry `SKILL.md` plus optional
|
|
328
339
|
`references/`) that teaches an assistant the tech stack, the install/build/test/lint commands, the tooling, and the
|
|
329
340
|
folder layout, written where your assistant looks for skills.
|
|
330
341
|
|
|
331
342
|
It uses **whatever AI you already have configured**. ReplicaX prefers a locally installed CLI (reusing its login) and
|
|
332
|
-
falls back to a provider API key from your environment
|
|
343
|
+
falls back to a provider API key from your environment - it never stores credentials:
|
|
333
344
|
|
|
334
345
|
| Provider | CLI (preferred) | API key (fallback) | API model default |
|
|
335
346
|
|----------|-----------------|-------------------------------------|--------------------|
|
|
@@ -346,22 +357,22 @@ replicax init-skill --target claude --dry-run # preview (no AI
|
|
|
346
357
|
replicax init-skill --target claude --force # overwrite existing skill files
|
|
347
358
|
```
|
|
348
359
|
|
|
349
|
-
**Targets** (`--target`, required) control the on
|
|
350
|
-
`codex`
|
|
351
|
-
or forced with `--provider`) is the AI that _authors_ it
|
|
360
|
+
**Targets** (`--target`, required) control the on-disk _format/location_: `claude` -> `.claude/skills/<name>/SKILL.md`,
|
|
361
|
+
`codex` -> `.codex/skills/<name>/SKILL.md`, `antigravity` -> `.agents/skills/<name>.md`. The **provider** (auto-detected,
|
|
362
|
+
or forced with `--provider`) is the AI that _authors_ it - the two are independent.
|
|
352
363
|
|
|
353
364
|
> **Bring your own template.** If the project root has a `SKILL.md`, `init-skill` hands it to the AI as the **base** to
|
|
354
|
-
> refine
|
|
365
|
+
> refine - the model preserves your headings, structure, and instructions and fills them in from the detected setup,
|
|
355
366
|
> instead of starting from scratch.
|
|
356
367
|
|
|
357
|
-
> **Privacy:** only the project's _setup_ is sent to the provider
|
|
368
|
+
> **Privacy:** only the project's _setup_ is sent to the provider - the same safe surface ReplicaX captures (config
|
|
358
369
|
> files, structure, `package.json` scripts/deps). Source code and secrets are never sent. With `--no-ai` (or no provider
|
|
359
370
|
> configured), ReplicaX falls back to a deterministic, fully offline template.
|
|
360
371
|
|
|
361
372
|
### `replicax doctor`
|
|
362
373
|
|
|
363
|
-
Report which developer tools are installed locally
|
|
364
|
-
Cross
|
|
374
|
+
Report which developer tools are installed locally - runtimes, package managers, Docker, and editors - with versions.
|
|
375
|
+
Cross-platform and read-only; a missing tool is a finding, not an error.
|
|
365
376
|
|
|
366
377
|
```bash
|
|
367
378
|
replicax doctor
|
|
@@ -383,7 +394,7 @@ Developer environment
|
|
|
383
394
|
|
|
384
395
|
### `replicax compare <source> <target>`
|
|
385
396
|
|
|
386
|
-
Diff two profiles
|
|
397
|
+
Diff two profiles - or two project directories - across tooling, configuration files, `package.json`, structure, and
|
|
387
398
|
metadata. Each argument may be a `.replicax` profile, a directory containing one, or a plain project folder (scanned on
|
|
388
399
|
the fly), so you can compare anything against anything.
|
|
389
400
|
|
|
@@ -400,12 +411,12 @@ Removed:
|
|
|
400
411
|
- Jest (Tooling)
|
|
401
412
|
Changed:
|
|
402
413
|
~ eslint.config.js (Configuration files)
|
|
403
|
-
~ language: javascript
|
|
414
|
+
~ language: javascript -> typescript (Metadata)
|
|
404
415
|
```
|
|
405
416
|
|
|
406
417
|
### `replicax audit`
|
|
407
418
|
|
|
408
|
-
Score a project's setup against best
|
|
419
|
+
Score a project's setup against best-practice rules - linting, formatting, testing, git hooks, CI/CD, containerization
|
|
409
420
|
and get concrete recommendations for what's missing. Scans the current directory by default, or evaluates a stored
|
|
410
421
|
profile with `--profile`.
|
|
411
422
|
|
|
@@ -433,7 +444,7 @@ Recommendations:
|
|
|
433
444
|
```
|
|
434
445
|
|
|
435
446
|
> `compare` and `audit` build on the detection engine: every scan now reports the **detected stack** (React, TypeScript,
|
|
436
|
-
> Docker, GitHub Actions,
|
|
447
|
+
> Docker, GitHub Actions, ...) with a confidence score, persisted in the profile and viewable via `inspect --section
|
|
437
448
|
> detections`.
|
|
438
449
|
|
|
439
450
|
---
|
|
@@ -445,10 +456,10 @@ Recommendations:
|
|
|
445
456
|
| TS/JS configs, ESLint, Prettier | Application source (components, services, controllers) |
|
|
446
457
|
| Vite / Webpack / Rollup / esbuild | Runtime `dependencies` in `package.json` |
|
|
447
458
|
| Tailwind / PostCSS | `.env*`, `*.pem`, `*.key`, certificates |
|
|
448
|
-
| Docker, CI/CD (Actions, GitLab, CircleCI, Jenkins) | `node_modules/`, `dist/`, `build/`, `coverage/`, `.next/`,
|
|
459
|
+
| Docker, CI/CD (Actions, GitLab, CircleCI, Jenkins) | `node_modules/`, `dist/`, `build/`, `coverage/`, `.next/`, ... |
|
|
449
460
|
| `.editorconfig`, Husky hooks | IDE folders (`.vscode/`, `.idea/`, `.vs/`, `.fleet/`, `.zed/`) |
|
|
450
461
|
| Test configs (Vitest/Jest/Playwright/Cypress) | Anything matched by `.replicaxignore` |
|
|
451
|
-
| Monorepo files, commitlint/lint
|
|
462
|
+
| Monorepo files, commitlint/lint-staged/release/knip | |
|
|
452
463
|
| JVM build (Maven/Gradle) + Spring `application.*` | Compiled output (`target/`, `*.class`), the gradle wrapper JAR |
|
|
453
464
|
| Folder hierarchy (directories only) | Folder _contents_ |
|
|
454
465
|
|
|
@@ -456,7 +467,7 @@ Recommendations:
|
|
|
456
467
|
config blocks like `lint-staged` are kept. Runtime `dependencies` are deliberately dropped (that's your application),
|
|
457
468
|
and the new project's name is stamped in on `create`.
|
|
458
469
|
|
|
459
|
-
Both `.ts` and `.js` config variants work because ReplicaX copies them **verbatim**
|
|
470
|
+
Both `.ts` and `.js` config variants work because ReplicaX copies them **verbatim** it never needs to compile or
|
|
460
471
|
execute a config to capture it.
|
|
461
472
|
|
|
462
473
|
---
|
|
@@ -466,23 +477,32 @@ execute a config to capture it.
|
|
|
466
477
|
ReplicaX treats secret exclusion as a **hard guarantee, not a best effort.**
|
|
467
478
|
|
|
468
479
|
> 🛡️ **Secrets are never captured.** `.env`, `.env.*`, `*.pem`, `*.key`, `*.crt`, SSH keys, and friends are blocked *
|
|
469
|
-
*unconditionally**
|
|
480
|
+
*unconditionally** - this cannot be overridden by configuration.
|
|
470
481
|
|
|
471
|
-
> 🧼 **`.npmrc` is sanitized.** It's a legitimate setup file, but auth tokens (`_authToken`, `_password`,
|
|
482
|
+
> 🧼 **`.npmrc` is sanitized.** It's a legitimate setup file, but auth tokens (`_authToken`, `_password`, ...) are stripped
|
|
472
483
|
> out before it enters a profile.
|
|
473
484
|
|
|
474
485
|
> 🚧 **No path escapes.** Every path read from a profile (or from an AI response) is validated against traversal (`..`)
|
|
475
486
|
> and absolute paths before anything is written, so a malicious profile can never write outside its target directory.
|
|
476
|
-
`validate` re
|
|
487
|
+
> `validate` re-checks this.
|
|
488
|
+
|
|
489
|
+
> 📦 **Archive extraction is a trust boundary.** An imported `.tar.gz` is validated before a single byte is written:
|
|
490
|
+
> entries that escape the target (`..`, absolute paths), symlinks/hardlinks, and device entries are rejected, and the
|
|
491
|
+
> archive is capped on compressed size, uncompressed size, file count, and per-file size, so a tar bomb is aborted up
|
|
492
|
+
> front. The same guard (with links skipped) protects the GitHub tarball that `extract` downloads.
|
|
493
|
+
|
|
494
|
+
> 🧯 **Install is opt-in for untrusted profiles.** Because `npm install` runs dependency lifecycle scripts, `create`
|
|
495
|
+
> will not auto-install for an `extract`ed or `import`ed profile - it prints the dependency list and waits for you to
|
|
496
|
+
> review and pass `--install`. See [`replicax create`](#replicax-create-project-name).
|
|
477
497
|
|
|
478
|
-
The same guarantees apply to `extract`
|
|
498
|
+
The same guarantees apply to `extract` - a remote repo's secrets are filtered exactly as a local project's are.
|
|
479
499
|
|
|
480
500
|
---
|
|
481
501
|
|
|
482
|
-
## Configuration
|
|
502
|
+
## Configuration - `.replicaxignore` and `.replicaxinclude`
|
|
483
503
|
|
|
484
504
|
**`.replicaxignore`** controls what gets *excluded*, with **gitignore syntax**. `init` can scaffold a starter for you.
|
|
485
|
-
Ignored files are excluded from the profile
|
|
505
|
+
Ignored files are excluded from the profile - but their parent directories are still captured for structure.
|
|
486
506
|
|
|
487
507
|
```gitignore
|
|
488
508
|
# Business logic (folders kept, contents dropped)
|
|
@@ -496,7 +516,7 @@ src/**/*.ts
|
|
|
496
516
|
|
|
497
517
|
**`.replicaxinclude`** is the opposite: a list of **glob patterns** (one per line, `#` comments) for *extra* files to
|
|
498
518
|
capture verbatim, on top of the auto-detected catalogue. Use it for config ReplicaX doesn't recognize by default
|
|
499
|
-
(`*.toml`, a `config/` directory, an IDE file you do want shared,
|
|
519
|
+
(`*.toml`, a `config/` directory, an IDE file you do want shared, ...). A trailing `/` means "the whole directory".
|
|
500
520
|
|
|
501
521
|
```gitignore
|
|
502
522
|
# Capture these in addition to the auto-detected setup
|
|
@@ -505,9 +525,9 @@ config/**
|
|
|
505
525
|
.vscode/extensions.json
|
|
506
526
|
```
|
|
507
527
|
|
|
508
|
-
**Precedence** (highest first): the **secret guard** always wins (a secret can never be included)
|
|
509
|
-
**`.replicaxignore`** (your excludes beat your includes)
|
|
510
|
-
lists, so it can reach normally-skipped locations)
|
|
528
|
+
**Precedence** (highest first): the **secret guard** always wins (a secret can never be included) ->
|
|
529
|
+
**`.replicaxignore`** (your excludes beat your includes) -> **`.replicaxinclude`** (overrides the default prune/ignore
|
|
530
|
+
lists, so it can reach normally-skipped locations) -> the **built-in catalogue**.
|
|
511
531
|
|
|
512
532
|
---
|
|
513
533
|
|
|
@@ -525,10 +545,10 @@ A profile is five required JSON files under `.replicax/`, plus an optional manif
|
|
|
525
545
|
└── manifest.json # content-free index of artifacts (path, category, size, hash)
|
|
526
546
|
```
|
|
527
547
|
|
|
528
|
-
All files are schema
|
|
548
|
+
All files are schema-validated (zod) on load; `validate` additionally re-checks checksums and rejects unsafe paths.
|
|
529
549
|
|
|
530
550
|
**Schema version 2.1.0** added the detected stack (`metadata.detections`), an optional `registry` block (for future
|
|
531
|
-
registry support), and `manifest.json`
|
|
551
|
+
registry support), and `manifest.json` - all backward-compatible. Profiles written by older ReplicaX (2.0.0) are
|
|
532
552
|
**migrated automatically on load** and the manifest is synthesized when absent, so existing profiles keep working.
|
|
533
553
|
|
|
534
554
|
---
|
|
@@ -552,7 +572,7 @@ flowchart LR
|
|
|
552
572
|
|
|
553
573
|
- **Scanner** detects config files (via a glob catalogue), the folder hierarchy, and project metadata (package manager,
|
|
554
574
|
framework, language).
|
|
555
|
-
- **Ignore engine** layers default ignores + `.replicaxignore`, with a separate, **non
|
|
575
|
+
- **Ignore engine** layers default ignores + `.replicaxignore`, with a separate, **non-overridable secret guard**.
|
|
556
576
|
- **Profile generator** assembles the bundle and computes checksums.
|
|
557
577
|
- **Project generator** reproduces the setup, adapting names/paths, with a **conflict resolver** for existing files.
|
|
558
578
|
|
|
@@ -562,7 +582,7 @@ flowchart LR
|
|
|
562
582
|
|
|
563
583
|
```bash
|
|
564
584
|
npm install
|
|
565
|
-
npm run build # tsup
|
|
585
|
+
npm run build # tsup -> dist/index.js (single ESM file, with shebang)
|
|
566
586
|
npm run typecheck # tsc --noEmit (covers src AND tests)
|
|
567
587
|
npm test # vitest (real temp-dir fixtures, no mocks)
|
|
568
588
|
npm run format # prettier --write .
|
|
@@ -581,15 +601,15 @@ npx vitest run -t "sanitizes a captured .npmrc"
|
|
|
581
601
|
<br/>
|
|
582
602
|
|
|
583
603
|
**Lockfile.** `package-lock.json` is maintained with **npm 10** (what CI's Node 20/22 ship with). On npm 11+, regenerate
|
|
584
|
-
with `npx npm@10 install` when changing dependencies
|
|
604
|
+
with `npx npm@10 install` when changing dependencies - npm 11 resolves a different tree and will desync `npm ci`.
|
|
585
605
|
|
|
586
|
-
**Path alias.** Imports use a `@/*`
|
|
606
|
+
**Path alias.** Imports use a `@/*` -> `src/*` alias resolved by tsc, tsup/esbuild, and the `vite-tsconfig-paths` vitest
|
|
587
607
|
plugin. The build bundles to one file, so the alias never reaches the published output.
|
|
588
608
|
|
|
589
|
-
**Stack.** TypeScript 5
|
|
590
|
-
@inquirer/prompts
|
|
609
|
+
**Stack.** TypeScript 5 / Node 20+ / ESM / commander / fast-glob / fs-extra / ignore / zod / tar / picocolors / ora /
|
|
610
|
+
@inquirer/prompts / cli-table3 / vitest / tsup / prettier.
|
|
591
611
|
|
|
592
|
-
**Audit note.** `npm audit` flags `esbuild` (a build
|
|
612
|
+
**Audit note.** `npm audit` flags `esbuild` (a build-time transitive of `tsup`). The advisory concerns esbuild's dev
|
|
593
613
|
server, which ReplicaX never runs, and esbuild is not part of the published runtime (`dist/`). Fixing it requires a
|
|
594
614
|
breaking tsup downgrade, so the toolchain is left intact.
|
|
595
615
|
|
|
@@ -608,7 +628,7 @@ No. Only configuration files and the empty folder hierarchy. Application code is
|
|
|
608
628
|
<details>
|
|
609
629
|
<summary><b>What about <code>.ts</code> config files like <code>vite.config.ts</code>?</b></summary>
|
|
610
630
|
<br/>
|
|
611
|
-
Fully supported
|
|
631
|
+
Fully supported - they're copied as text, so both <code>.ts</code> and <code>.js</code> variants work without any compile step.
|
|
612
632
|
</details>
|
|
613
633
|
|
|
614
634
|
<details>
|
|
@@ -626,22 +646,22 @@ They're part of your application, not your setup. <code>devDependencies</code>,
|
|
|
626
646
|
<details>
|
|
627
647
|
<summary><b>Does <code>extract</code> need <code>git</code> installed?</b></summary>
|
|
628
648
|
<br/>
|
|
629
|
-
No. It downloads the repo as a tarball over the GitHub API using Node's built-in <code>fetch</code>
|
|
649
|
+
No. It downloads the repo as a tarball over the GitHub API using Node's built-in <code>fetch</code> no <code>git</code> binary, no full clone.
|
|
630
650
|
</details>
|
|
631
651
|
|
|
632
652
|
<details>
|
|
633
653
|
<summary><b>Is it cross-platform?</b></summary>
|
|
634
654
|
<br/>
|
|
635
|
-
Yes
|
|
655
|
+
Yes Windows (native + WSL), macOS, and Linux.
|
|
636
656
|
</details>
|
|
637
657
|
|
|
638
658
|
---
|
|
639
659
|
|
|
640
660
|
## License
|
|
641
661
|
|
|
642
|
-
**MIT**
|
|
662
|
+
**MIT** see [LICENSE](LICENSE).
|
|
643
663
|
|
|
644
664
|
<div align="center">
|
|
645
665
|
<br/>
|
|
646
|
-
<sub><i>ReplicaX
|
|
666
|
+
<sub><i>ReplicaX - copy the setup, not the code.</i></sub>
|
|
647
667
|
</div>
|
package/dist/index.js
CHANGED
|
@@ -70,7 +70,7 @@ var REPLICAX_DIR = ".replicax";
|
|
|
70
70
|
var IGNORE_FILE = ".replicaxignore";
|
|
71
71
|
var INCLUDE_FILE = ".replicaxinclude";
|
|
72
72
|
var ROOT_SKILL_FILE = "SKILL.md";
|
|
73
|
-
var REPLICAX_VERSION = "2.
|
|
73
|
+
var REPLICAX_VERSION = "2.2.0";
|
|
74
74
|
var PROFILE_FILES = {
|
|
75
75
|
profile: "profile.json",
|
|
76
76
|
tooling: "tooling.json",
|
|
@@ -1273,13 +1273,16 @@ function buildBundle(args) {
|
|
|
1273
1273
|
name: args.name,
|
|
1274
1274
|
description: args.description ?? args.existing.description,
|
|
1275
1275
|
replicaxVersion: REPLICAX_VERSION,
|
|
1276
|
-
updatedAt: now
|
|
1276
|
+
updatedAt: now,
|
|
1277
|
+
// Preserve the original provenance on sync unless explicitly overridden.
|
|
1278
|
+
...args.source ?? args.existing.source ? { source: args.source ?? args.existing.source } : {}
|
|
1277
1279
|
} : {
|
|
1278
1280
|
name: args.name,
|
|
1279
1281
|
version: "1.0.0",
|
|
1280
1282
|
createdAt: now,
|
|
1281
1283
|
replicaxVersion: REPLICAX_VERSION,
|
|
1282
|
-
...args.description ? { description: args.description } : {}
|
|
1284
|
+
...args.description ? { description: args.description } : {},
|
|
1285
|
+
...args.source ? { source: args.source } : {}
|
|
1283
1286
|
};
|
|
1284
1287
|
const checksum = computeChecksum(args.tooling);
|
|
1285
1288
|
return {
|
|
@@ -1308,6 +1311,7 @@ var RegistrySchema = z.object({
|
|
|
1308
1311
|
/** Where the profile originated (URL, registry name, …). */
|
|
1309
1312
|
source: z.string().optional()
|
|
1310
1313
|
});
|
|
1314
|
+
var ProfileSourceSchema = z.enum(["local", "github", "import"]);
|
|
1311
1315
|
var ProfileSchema = z.object({
|
|
1312
1316
|
name: z.string().min(1),
|
|
1313
1317
|
version: z.string().min(1),
|
|
@@ -1315,6 +1319,8 @@ var ProfileSchema = z.object({
|
|
|
1315
1319
|
updatedAt: z.string().optional(),
|
|
1316
1320
|
replicaxVersion: z.string().min(1),
|
|
1317
1321
|
description: z.string().optional(),
|
|
1322
|
+
/** Provenance of the captured setup (added in schema 2.2.0). */
|
|
1323
|
+
source: ProfileSourceSchema.optional(),
|
|
1318
1324
|
/** Optional registry metadata (future registry compatibility). */
|
|
1319
1325
|
registry: RegistrySchema.optional()
|
|
1320
1326
|
});
|
|
@@ -1417,6 +1423,13 @@ var MIGRATIONS = [
|
|
|
1417
1423
|
}
|
|
1418
1424
|
return raw;
|
|
1419
1425
|
}
|
|
1426
|
+
},
|
|
1427
|
+
{
|
|
1428
|
+
from: "2.1.0",
|
|
1429
|
+
to: "2.2.0",
|
|
1430
|
+
apply(raw) {
|
|
1431
|
+
return raw;
|
|
1432
|
+
}
|
|
1420
1433
|
}
|
|
1421
1434
|
];
|
|
1422
1435
|
var KNOWN_VERSIONS = /* @__PURE__ */ new Set([
|
|
@@ -1619,7 +1632,8 @@ async function initCommand(options) {
|
|
|
1619
1632
|
name,
|
|
1620
1633
|
tooling: scan.tooling,
|
|
1621
1634
|
structure: scan.structure,
|
|
1622
|
-
metadata: scan.metadata
|
|
1635
|
+
metadata: scan.metadata,
|
|
1636
|
+
source: "local"
|
|
1623
1637
|
});
|
|
1624
1638
|
reportSkippedSecrets(scan.skippedSecrets);
|
|
1625
1639
|
printScanSummary(bundle);
|
|
@@ -1973,6 +1987,14 @@ async function runWithStdin(bin, args, input, timeoutMs = 12e4) {
|
|
|
1973
1987
|
}
|
|
1974
1988
|
|
|
1975
1989
|
// src/core/ai/providers.ts
|
|
1990
|
+
var ApiHttpError = class extends Error {
|
|
1991
|
+
constructor(status, message) {
|
|
1992
|
+
super(message);
|
|
1993
|
+
this.status = status;
|
|
1994
|
+
this.name = "ApiHttpError";
|
|
1995
|
+
}
|
|
1996
|
+
status;
|
|
1997
|
+
};
|
|
1976
1998
|
async function postJson(url, headers, body) {
|
|
1977
1999
|
const res = await fetch(url, {
|
|
1978
2000
|
method: "POST",
|
|
@@ -1981,10 +2003,33 @@ async function postJson(url, headers, body) {
|
|
|
1981
2003
|
});
|
|
1982
2004
|
if (!res.ok) {
|
|
1983
2005
|
const text = await res.text().catch(() => "");
|
|
1984
|
-
throw new
|
|
2006
|
+
throw new ApiHttpError(res.status, text.slice(0, 300) || res.statusText);
|
|
1985
2007
|
}
|
|
1986
2008
|
return res.json();
|
|
1987
2009
|
}
|
|
2010
|
+
function enrichProviderError(err, def, model) {
|
|
2011
|
+
const status = err instanceof ApiHttpError ? err.status : void 0;
|
|
2012
|
+
const detail = err instanceof Error ? err.message : String(err);
|
|
2013
|
+
const overrideHint = `Override the model with --model <id> or ${def.modelEnvVar}=<id>.`;
|
|
2014
|
+
if (status === 404 || status === 400) {
|
|
2015
|
+
return new ReplicaxError(
|
|
2016
|
+
`${def.label} API rejected model "${model}" (HTTP ${status}). ${overrideHint}`,
|
|
2017
|
+
[`Provider response: ${detail}`]
|
|
2018
|
+
);
|
|
2019
|
+
}
|
|
2020
|
+
if (status === 401 || status === 403) {
|
|
2021
|
+
return new ReplicaxError(`${def.label} API rejected the credentials (HTTP ${status}).`, [
|
|
2022
|
+
`Check ${def.apiEnvVars[0]} holds a valid API key.`,
|
|
2023
|
+
`Provider response: ${detail}`
|
|
2024
|
+
]);
|
|
2025
|
+
}
|
|
2026
|
+
if (status === 429) {
|
|
2027
|
+
return new ReplicaxError(`${def.label} API rate limit hit (HTTP 429).`, [
|
|
2028
|
+
"Wait a moment and try again."
|
|
2029
|
+
]);
|
|
2030
|
+
}
|
|
2031
|
+
return new ReplicaxError(`${def.label} API request failed: ${detail}`);
|
|
2032
|
+
}
|
|
1988
2033
|
async function callAnthropic(prompt, apiKey, model) {
|
|
1989
2034
|
const data = await postJson(
|
|
1990
2035
|
"https://api.anthropic.com/v1/messages",
|
|
@@ -2061,7 +2106,13 @@ function apiInvoker(def, apiKey, modelOverride) {
|
|
|
2061
2106
|
return {
|
|
2062
2107
|
id: def.id,
|
|
2063
2108
|
via: `${def.label} API (${model})`,
|
|
2064
|
-
run: (prompt) =>
|
|
2109
|
+
run: async (prompt) => {
|
|
2110
|
+
try {
|
|
2111
|
+
return await def.callApi(prompt, apiKey, model);
|
|
2112
|
+
} catch (err) {
|
|
2113
|
+
throw enrichProviderError(err, def, model);
|
|
2114
|
+
}
|
|
2115
|
+
}
|
|
2065
2116
|
};
|
|
2066
2117
|
}
|
|
2067
2118
|
async function resolveProvider(preference, modelOverride) {
|
|
@@ -2234,7 +2285,9 @@ async function initSkillCommand(options) {
|
|
|
2234
2285
|
}
|
|
2235
2286
|
} catch (err) {
|
|
2236
2287
|
aiSpinner.fail("AI generation failed");
|
|
2237
|
-
logger.warn(`${err.message}
|
|
2288
|
+
logger.warn(`${err.message}`);
|
|
2289
|
+
if (err instanceof ReplicaxError) for (const hint of err.hints) logger.hint(hint);
|
|
2290
|
+
logger.warn("Falling back to the built-in template.");
|
|
2238
2291
|
}
|
|
2239
2292
|
} else {
|
|
2240
2293
|
logger.info("No configured AI provider found \u2014 using the built-in template.");
|
|
@@ -2278,14 +2331,180 @@ async function initSkillCommand(options) {
|
|
|
2278
2331
|
}
|
|
2279
2332
|
|
|
2280
2333
|
// src/commands/extract.ts
|
|
2281
|
-
import
|
|
2334
|
+
import path11 from "path";
|
|
2282
2335
|
import ora3 from "ora";
|
|
2283
2336
|
|
|
2284
2337
|
// src/core/github.ts
|
|
2338
|
+
import os2 from "os";
|
|
2339
|
+
import path10 from "path";
|
|
2340
|
+
import fs9 from "fs-extra";
|
|
2341
|
+
|
|
2342
|
+
// src/core/archive.ts
|
|
2343
|
+
import { createReadStream } from "fs";
|
|
2285
2344
|
import os from "os";
|
|
2286
2345
|
import path9 from "path";
|
|
2287
2346
|
import fs8 from "fs-extra";
|
|
2288
|
-
import { extract as tarExtract } from "tar";
|
|
2347
|
+
import { create as tarCreate, extract as tarExtract, Parser } from "tar";
|
|
2348
|
+
async function exportProfile(profileDirectory, outPath) {
|
|
2349
|
+
const resolvedOut = path9.resolve(outPath);
|
|
2350
|
+
await fs8.ensureDir(path9.dirname(resolvedOut));
|
|
2351
|
+
const parent = path9.dirname(profileDirectory);
|
|
2352
|
+
const base = path9.basename(profileDirectory);
|
|
2353
|
+
await tarCreate(
|
|
2354
|
+
{
|
|
2355
|
+
gzip: true,
|
|
2356
|
+
file: resolvedOut,
|
|
2357
|
+
cwd: parent,
|
|
2358
|
+
// tar strips leading "/" and ".." by default, so extraction stays scoped.
|
|
2359
|
+
portable: true
|
|
2360
|
+
},
|
|
2361
|
+
[base]
|
|
2362
|
+
);
|
|
2363
|
+
}
|
|
2364
|
+
var PROFILE_ARCHIVE_LIMITS = {
|
|
2365
|
+
maxCompressedBytes: 50 * 1024 * 1024,
|
|
2366
|
+
// 50 MB
|
|
2367
|
+
maxTotalBytes: 200 * 1024 * 1024,
|
|
2368
|
+
// 200 MB uncompressed
|
|
2369
|
+
maxEntries: 2e4,
|
|
2370
|
+
maxEntryBytes: 50 * 1024 * 1024,
|
|
2371
|
+
// 50 MB per file
|
|
2372
|
+
allowSymlinks: false
|
|
2373
|
+
};
|
|
2374
|
+
var REPO_ARCHIVE_LIMITS = {
|
|
2375
|
+
maxCompressedBytes: 250 * 1024 * 1024,
|
|
2376
|
+
// 250 MB
|
|
2377
|
+
maxTotalBytes: 1024 * 1024 * 1024,
|
|
2378
|
+
// 1 GB uncompressed
|
|
2379
|
+
maxEntries: 2e5,
|
|
2380
|
+
maxEntryBytes: 200 * 1024 * 1024,
|
|
2381
|
+
// 200 MB per file
|
|
2382
|
+
allowSymlinks: true
|
|
2383
|
+
};
|
|
2384
|
+
function inspectEntry(entry, limits) {
|
|
2385
|
+
const type = String(entry.type);
|
|
2386
|
+
const entryPath = entry.path;
|
|
2387
|
+
if (type === "File" || type === "Directory" || type === "GNUDumpDir") {
|
|
2388
|
+
if (safeJoinable(entryPath) === null) {
|
|
2389
|
+
throw new ReplicaxError(`Refusing to extract unsafe path from archive: "${entryPath}".`, [
|
|
2390
|
+
"The archive may be malicious (path traversal)."
|
|
2391
|
+
]);
|
|
2392
|
+
}
|
|
2393
|
+
if ((entry.size ?? 0) > limits.maxEntryBytes) {
|
|
2394
|
+
throw new ReplicaxError(`Archive entry "${entryPath}" exceeds the per-file size limit.`);
|
|
2395
|
+
}
|
|
2396
|
+
return "extract";
|
|
2397
|
+
}
|
|
2398
|
+
if (type === "SymbolicLink" || type === "Link") {
|
|
2399
|
+
if (!limits.allowSymlinks) {
|
|
2400
|
+
throw new ReplicaxError(`Refusing to extract link entry from archive: "${entryPath}".`, [
|
|
2401
|
+
"Profile archives never contain symlinks; this one may be malicious."
|
|
2402
|
+
]);
|
|
2403
|
+
}
|
|
2404
|
+
return "skip";
|
|
2405
|
+
}
|
|
2406
|
+
throw new ReplicaxError(`Refusing to extract "${entryPath}" (unsupported tar entry: ${type}).`);
|
|
2407
|
+
}
|
|
2408
|
+
function validateArchive(resolved, limits) {
|
|
2409
|
+
return new Promise((resolve, reject) => {
|
|
2410
|
+
let entryCount = 0;
|
|
2411
|
+
let totalBytes = 0;
|
|
2412
|
+
let settled = false;
|
|
2413
|
+
const source = createReadStream(resolved);
|
|
2414
|
+
const parser = new Parser({});
|
|
2415
|
+
const fail = (err) => {
|
|
2416
|
+
if (settled) return;
|
|
2417
|
+
settled = true;
|
|
2418
|
+
source.destroy();
|
|
2419
|
+
reject(err);
|
|
2420
|
+
};
|
|
2421
|
+
const finish = () => {
|
|
2422
|
+
if (settled) return;
|
|
2423
|
+
settled = true;
|
|
2424
|
+
resolve();
|
|
2425
|
+
};
|
|
2426
|
+
parser.on("entry", (entry) => {
|
|
2427
|
+
if (settled) {
|
|
2428
|
+
entry.resume();
|
|
2429
|
+
return;
|
|
2430
|
+
}
|
|
2431
|
+
try {
|
|
2432
|
+
if (inspectEntry(entry, limits) === "extract") {
|
|
2433
|
+
entryCount += 1;
|
|
2434
|
+
if (entryCount > limits.maxEntries) {
|
|
2435
|
+
return fail(
|
|
2436
|
+
new ReplicaxError("Archive contains too many entries; refusing to extract.")
|
|
2437
|
+
);
|
|
2438
|
+
}
|
|
2439
|
+
totalBytes += entry.size ?? 0;
|
|
2440
|
+
if (totalBytes > limits.maxTotalBytes) {
|
|
2441
|
+
return fail(
|
|
2442
|
+
new ReplicaxError("Archive is too large when uncompressed; refusing to extract.")
|
|
2443
|
+
);
|
|
2444
|
+
}
|
|
2445
|
+
}
|
|
2446
|
+
} catch (err) {
|
|
2447
|
+
return fail(err);
|
|
2448
|
+
}
|
|
2449
|
+
entry.resume();
|
|
2450
|
+
});
|
|
2451
|
+
parser.on("end", finish);
|
|
2452
|
+
parser.on("error", (err) => fail(err));
|
|
2453
|
+
source.on("error", (err) => fail(err));
|
|
2454
|
+
source.pipe(parser);
|
|
2455
|
+
});
|
|
2456
|
+
}
|
|
2457
|
+
async function safeExtract(archivePath, destDir, limits) {
|
|
2458
|
+
const resolved = path9.resolve(archivePath);
|
|
2459
|
+
const stat = await fs8.stat(resolved).catch(() => null);
|
|
2460
|
+
if (!stat) {
|
|
2461
|
+
throw new ReplicaxError(`Archive not found: ${archivePath}`);
|
|
2462
|
+
}
|
|
2463
|
+
if (stat.size > limits.maxCompressedBytes) {
|
|
2464
|
+
throw new ReplicaxError("Archive file is too large; refusing to extract.");
|
|
2465
|
+
}
|
|
2466
|
+
await validateArchive(resolved, limits);
|
|
2467
|
+
await fs8.ensureDir(destDir);
|
|
2468
|
+
await tarExtract({
|
|
2469
|
+
file: resolved,
|
|
2470
|
+
cwd: destDir,
|
|
2471
|
+
strip: 0,
|
|
2472
|
+
filter: (_p, entry) => {
|
|
2473
|
+
try {
|
|
2474
|
+
return inspectEntry(entry, limits) === "extract";
|
|
2475
|
+
} catch {
|
|
2476
|
+
return false;
|
|
2477
|
+
}
|
|
2478
|
+
}
|
|
2479
|
+
});
|
|
2480
|
+
}
|
|
2481
|
+
async function extractToTemp(archivePath) {
|
|
2482
|
+
if (!await fs8.pathExists(path9.resolve(archivePath))) {
|
|
2483
|
+
throw new ReplicaxError(`Archive not found: ${archivePath}`);
|
|
2484
|
+
}
|
|
2485
|
+
const tmp = await fs8.mkdtemp(path9.join(os.tmpdir(), "replicax-import-"));
|
|
2486
|
+
try {
|
|
2487
|
+
await safeExtract(archivePath, tmp, PROFILE_ARCHIVE_LIMITS);
|
|
2488
|
+
} catch (err) {
|
|
2489
|
+
await fs8.remove(tmp).catch(() => void 0);
|
|
2490
|
+
throw err;
|
|
2491
|
+
}
|
|
2492
|
+
return tmp;
|
|
2493
|
+
}
|
|
2494
|
+
async function findProfileRoot(dir) {
|
|
2495
|
+
const hasProfile = async (d) => fs8.pathExists(path9.join(d, PROFILE_FILES.profile));
|
|
2496
|
+
if (await hasProfile(dir)) return dir;
|
|
2497
|
+
const entries = await fs8.readdir(dir, { withFileTypes: true });
|
|
2498
|
+
for (const entry of entries) {
|
|
2499
|
+
if (entry.isDirectory()) {
|
|
2500
|
+
const candidate = path9.join(dir, entry.name);
|
|
2501
|
+
if (await hasProfile(candidate)) return candidate;
|
|
2502
|
+
}
|
|
2503
|
+
}
|
|
2504
|
+
return null;
|
|
2505
|
+
}
|
|
2506
|
+
|
|
2507
|
+
// src/core/github.ts
|
|
2289
2508
|
function parseGitHubRef(input) {
|
|
2290
2509
|
const raw = input.trim();
|
|
2291
2510
|
if (!raw) {
|
|
@@ -2355,9 +2574,9 @@ function httpError(status, slug, hasToken) {
|
|
|
2355
2574
|
return new ReplicaxError(`GitHub returned HTTP ${status} for ${slug}.`);
|
|
2356
2575
|
}
|
|
2357
2576
|
async function firstSubdir(dir) {
|
|
2358
|
-
const entries = await
|
|
2577
|
+
const entries = await fs9.readdir(dir, { withFileTypes: true });
|
|
2359
2578
|
for (const entry of entries) {
|
|
2360
|
-
if (entry.isDirectory()) return
|
|
2579
|
+
if (entry.isDirectory()) return path10.join(dir, entry.name);
|
|
2361
2580
|
}
|
|
2362
2581
|
return null;
|
|
2363
2582
|
}
|
|
@@ -2381,14 +2600,14 @@ async function downloadRepo(ref) {
|
|
|
2381
2600
|
if (!res.ok) {
|
|
2382
2601
|
throw httpError(res.status, slug, Boolean(token));
|
|
2383
2602
|
}
|
|
2384
|
-
const tmpRoot = await
|
|
2385
|
-
const cleanup = () =>
|
|
2603
|
+
const tmpRoot = await fs9.mkdtemp(path10.join(os2.tmpdir(), "replicax-extract-"));
|
|
2604
|
+
const cleanup = () => fs9.remove(tmpRoot);
|
|
2386
2605
|
try {
|
|
2387
|
-
const tarPath =
|
|
2388
|
-
await
|
|
2389
|
-
const extractDir =
|
|
2390
|
-
await
|
|
2391
|
-
await
|
|
2606
|
+
const tarPath = path10.join(tmpRoot, "repo.tar.gz");
|
|
2607
|
+
await fs9.writeFile(tarPath, Buffer.from(await res.arrayBuffer()));
|
|
2608
|
+
const extractDir = path10.join(tmpRoot, "src");
|
|
2609
|
+
await fs9.ensureDir(extractDir);
|
|
2610
|
+
await safeExtract(tarPath, extractDir, REPO_ARCHIVE_LIMITS);
|
|
2392
2611
|
const repoRoot = await firstSubdir(extractDir);
|
|
2393
2612
|
if (!repoRoot) {
|
|
2394
2613
|
throw new ReplicaxError(`The downloaded archive for ${slug} was empty.`);
|
|
@@ -2430,7 +2649,9 @@ async function extractCommand(repo, options) {
|
|
|
2430
2649
|
name,
|
|
2431
2650
|
tooling: scan.tooling,
|
|
2432
2651
|
structure: scan.structure,
|
|
2433
|
-
metadata: scan.metadata
|
|
2652
|
+
metadata: scan.metadata,
|
|
2653
|
+
// Captured from a remote repo we don't control — untrusted for auto-install.
|
|
2654
|
+
source: "github"
|
|
2434
2655
|
});
|
|
2435
2656
|
reportSkippedSecrets(scan.skippedSecrets);
|
|
2436
2657
|
printScanSummary(bundle);
|
|
@@ -2440,7 +2661,7 @@ async function extractCommand(repo, options) {
|
|
|
2440
2661
|
logger.info("Dry run \u2014 no files were written.");
|
|
2441
2662
|
return;
|
|
2442
2663
|
}
|
|
2443
|
-
const outRoot = options.out ?
|
|
2664
|
+
const outRoot = options.out ? path11.resolve(options.out) : process.cwd();
|
|
2444
2665
|
const dir = profileDir(outRoot);
|
|
2445
2666
|
if (await profileExists(dir)) {
|
|
2446
2667
|
logger.warn(
|
|
@@ -2457,8 +2678,7 @@ async function extractCommand(repo, options) {
|
|
|
2457
2678
|
}
|
|
2458
2679
|
|
|
2459
2680
|
// src/commands/create.ts
|
|
2460
|
-
import
|
|
2461
|
-
import fs10 from "fs-extra";
|
|
2681
|
+
import path13 from "path";
|
|
2462
2682
|
|
|
2463
2683
|
// src/core/conflict-resolver.ts
|
|
2464
2684
|
import { select } from "@inquirer/prompts";
|
|
@@ -2500,8 +2720,8 @@ var ConflictResolver = class {
|
|
|
2500
2720
|
};
|
|
2501
2721
|
|
|
2502
2722
|
// src/core/project-generator.ts
|
|
2503
|
-
import
|
|
2504
|
-
import
|
|
2723
|
+
import path12 from "path";
|
|
2724
|
+
import fs10 from "fs-extra";
|
|
2505
2725
|
async function generateProject(options) {
|
|
2506
2726
|
const { bundle, targetDir, projectName, dryRun, conflict } = options;
|
|
2507
2727
|
const result = {
|
|
@@ -2511,16 +2731,16 @@ async function generateProject(options) {
|
|
|
2511
2731
|
filesSkipped: 0,
|
|
2512
2732
|
unsafeSkipped: []
|
|
2513
2733
|
};
|
|
2514
|
-
if (!dryRun) await
|
|
2734
|
+
if (!dryRun) await fs10.ensureDir(targetDir);
|
|
2515
2735
|
for (const dir of bundle.structure.directories) {
|
|
2516
2736
|
const safe = safeJoinable(dir);
|
|
2517
2737
|
if (!safe) {
|
|
2518
2738
|
result.unsafeSkipped.push(dir);
|
|
2519
2739
|
continue;
|
|
2520
2740
|
}
|
|
2521
|
-
const full =
|
|
2522
|
-
const existed = await
|
|
2523
|
-
if (!dryRun) await
|
|
2741
|
+
const full = path12.join(targetDir, safe);
|
|
2742
|
+
const existed = await fs10.pathExists(full);
|
|
2743
|
+
if (!dryRun) await fs10.ensureDir(full);
|
|
2524
2744
|
if (!existed) result.dirsCreated += 1;
|
|
2525
2745
|
result.entries.push({ kind: "dir", path: safe, action: existed ? "skip" : "create" });
|
|
2526
2746
|
}
|
|
@@ -2544,8 +2764,8 @@ async function writeFile(relPath, content, options, result) {
|
|
|
2544
2764
|
logger.warn(`Refusing to write unsafe path from profile: ${relPath}`);
|
|
2545
2765
|
return;
|
|
2546
2766
|
}
|
|
2547
|
-
const full =
|
|
2548
|
-
const exists = await
|
|
2767
|
+
const full = path12.join(options.targetDir, safe);
|
|
2768
|
+
const exists = await fs10.pathExists(full);
|
|
2549
2769
|
let action2 = exists ? "overwrite" : "create";
|
|
2550
2770
|
if (exists) {
|
|
2551
2771
|
const decision = await options.conflict.resolve(safe);
|
|
@@ -2558,8 +2778,8 @@ async function writeFile(relPath, content, options, result) {
|
|
|
2558
2778
|
action2 = "overwrite";
|
|
2559
2779
|
}
|
|
2560
2780
|
if (!options.dryRun) {
|
|
2561
|
-
await
|
|
2562
|
-
await
|
|
2781
|
+
await fs10.ensureDir(path12.dirname(full));
|
|
2782
|
+
await fs10.writeFile(full, content, "utf8");
|
|
2563
2783
|
}
|
|
2564
2784
|
result.filesWritten += 1;
|
|
2565
2785
|
result.entries.push({ kind: "file", path: safe, action: action2 });
|
|
@@ -2608,9 +2828,9 @@ async function createCommand(projectName, options) {
|
|
|
2608
2828
|
logger.warn(`Profile integrity check found ${mismatches.length} issue(s); continuing anyway.`);
|
|
2609
2829
|
logger.hint("Run `replicax validate` for details.");
|
|
2610
2830
|
}
|
|
2611
|
-
const targetDir =
|
|
2612
|
-
const leafName =
|
|
2613
|
-
if (
|
|
2831
|
+
const targetDir = path13.resolve(process.cwd(), projectName);
|
|
2832
|
+
const leafName = path13.basename(targetDir);
|
|
2833
|
+
if (path13.resolve(process.cwd()) === targetDir) {
|
|
2614
2834
|
throw new ReplicaxError("Refusing to scaffold into the current directory.", [
|
|
2615
2835
|
"Pass a new project name, e.g. `replicax create my-app`."
|
|
2616
2836
|
]);
|
|
@@ -2640,36 +2860,68 @@ async function createCommand(projectName, options) {
|
|
|
2640
2860
|
return;
|
|
2641
2861
|
}
|
|
2642
2862
|
logger.hint(`Location: ${relPosix(process.cwd(), targetDir)}/`);
|
|
2643
|
-
await maybeInstall(
|
|
2644
|
-
bundle.metadata.packageManager,
|
|
2645
|
-
targetDir,
|
|
2646
|
-
options,
|
|
2647
|
-
Boolean(bundle.tooling.packageJson)
|
|
2648
|
-
);
|
|
2863
|
+
await maybeInstall(bundle, targetDir, options);
|
|
2649
2864
|
logger.newline();
|
|
2650
2865
|
logger.success(`Project ${pc.bold(leafName)} is ready.`);
|
|
2651
2866
|
}
|
|
2652
|
-
|
|
2653
|
-
if (options.skipInstall) {
|
|
2654
|
-
|
|
2655
|
-
|
|
2656
|
-
|
|
2657
|
-
if (
|
|
2658
|
-
|
|
2659
|
-
|
|
2660
|
-
|
|
2661
|
-
}
|
|
2662
|
-
|
|
2663
|
-
|
|
2664
|
-
|
|
2665
|
-
|
|
2666
|
-
|
|
2867
|
+
function decideInstall(bundle, options) {
|
|
2868
|
+
if (options.skipInstall) return { kind: "skip-flag" };
|
|
2869
|
+
const devDeps = bundle.tooling.packageJson?.devDependencies ?? {};
|
|
2870
|
+
if (Object.keys(devDeps).length === 0) return { kind: "no-deps" };
|
|
2871
|
+
const manager = bundle.metadata.packageManager;
|
|
2872
|
+
if (manager === "unknown") return { kind: "no-manager" };
|
|
2873
|
+
const source = bundle.profile.source ?? "local";
|
|
2874
|
+
const trusted = source === "local";
|
|
2875
|
+
if (!trusted && !options.install) return { kind: "blocked", source, manager, devDeps };
|
|
2876
|
+
return { kind: "install", trusted, source, manager, devDeps };
|
|
2877
|
+
}
|
|
2878
|
+
function printDependencySummary(devDeps) {
|
|
2879
|
+
const names = Object.keys(devDeps).sort();
|
|
2880
|
+
for (const name of names.slice(0, 10)) logger.hint(` ${name} ${devDeps[name]}`);
|
|
2881
|
+
if (names.length > 10) logger.hint(` \u2026and ${names.length - 10} more`);
|
|
2882
|
+
}
|
|
2883
|
+
async function maybeInstall(bundle, targetDir, options) {
|
|
2884
|
+
const decision = decideInstall(bundle, options);
|
|
2885
|
+
switch (decision.kind) {
|
|
2886
|
+
case "skip-flag":
|
|
2887
|
+
logger.hint("Skipped dependency install (--skip-install).");
|
|
2888
|
+
return;
|
|
2889
|
+
case "no-deps":
|
|
2890
|
+
if (bundle.tooling.packageJson) logger.hint("No dependencies to install.");
|
|
2891
|
+
return;
|
|
2892
|
+
case "no-manager":
|
|
2893
|
+
logger.hint("No package manager detected; run your install command manually.");
|
|
2894
|
+
return;
|
|
2895
|
+
case "blocked":
|
|
2896
|
+
logger.newline();
|
|
2897
|
+
logger.warn(
|
|
2898
|
+
`Profile source is "${decision.source}" \u2014 skipping dependency install for safety.`
|
|
2899
|
+
);
|
|
2900
|
+
logger.hint(
|
|
2901
|
+
`Installing runs package lifecycle scripts. ${Object.keys(decision.devDeps).length} devDependencies would be added with ${decision.manager}:`
|
|
2902
|
+
);
|
|
2903
|
+
printDependencySummary(decision.devDeps);
|
|
2904
|
+
logger.hint(
|
|
2905
|
+
`Review them, then run \`${decision.manager} install\`, or re-run create with --install.`
|
|
2906
|
+
);
|
|
2907
|
+
return;
|
|
2908
|
+
case "install": {
|
|
2909
|
+
logger.newline();
|
|
2910
|
+
if (!decision.trusted) {
|
|
2911
|
+
logger.warn(
|
|
2912
|
+
`Installing for an untrusted ("${decision.source}") profile \u2014 package lifecycle scripts can execute code.`
|
|
2913
|
+
);
|
|
2914
|
+
}
|
|
2915
|
+
logger.info(
|
|
2916
|
+
`Installing ${Object.keys(decision.devDeps).length} devDependencies with ${decision.manager}\u2026`
|
|
2917
|
+
);
|
|
2918
|
+
printDependencySummary(decision.devDeps);
|
|
2919
|
+
const ok = await installDependencies(targetDir, decision.manager);
|
|
2920
|
+
if (ok) logger.success("Dependencies installed.");
|
|
2921
|
+
else logger.warn("Dependency install did not complete; run it manually.");
|
|
2922
|
+
return;
|
|
2923
|
+
}
|
|
2667
2924
|
}
|
|
2668
|
-
logger.newline();
|
|
2669
|
-
logger.info(`Installing dependencies with ${manager}\u2026`);
|
|
2670
|
-
const ok = await installDependencies(targetDir, manager);
|
|
2671
|
-
if (ok) logger.success("Dependencies installed.");
|
|
2672
|
-
else logger.warn("Dependency install did not complete; run it manually.");
|
|
2673
2925
|
}
|
|
2674
2926
|
|
|
2675
2927
|
// src/commands/sync.ts
|
|
@@ -2752,6 +3004,8 @@ async function syncCommand(options) {
|
|
|
2752
3004
|
tooling: scan.tooling,
|
|
2753
3005
|
structure: scan.structure,
|
|
2754
3006
|
metadata: scan.metadata,
|
|
3007
|
+
// A sync re-captures the local project, so the result is locally trusted.
|
|
3008
|
+
source: "local",
|
|
2755
3009
|
existing: existing.profile
|
|
2756
3010
|
});
|
|
2757
3011
|
const diff = diffBundles(existing, next);
|
|
@@ -2934,53 +3188,8 @@ async function validateCommand(options) {
|
|
|
2934
3188
|
|
|
2935
3189
|
// src/commands/export.ts
|
|
2936
3190
|
import path14 from "path";
|
|
2937
|
-
import fs12 from "fs-extra";
|
|
2938
|
-
import ora5 from "ora";
|
|
2939
|
-
|
|
2940
|
-
// src/core/archive.ts
|
|
2941
|
-
import os2 from "os";
|
|
2942
|
-
import path13 from "path";
|
|
2943
3191
|
import fs11 from "fs-extra";
|
|
2944
|
-
import
|
|
2945
|
-
async function exportProfile(profileDirectory, outPath) {
|
|
2946
|
-
const resolvedOut = path13.resolve(outPath);
|
|
2947
|
-
await fs11.ensureDir(path13.dirname(resolvedOut));
|
|
2948
|
-
const parent = path13.dirname(profileDirectory);
|
|
2949
|
-
const base = path13.basename(profileDirectory);
|
|
2950
|
-
await tarCreate(
|
|
2951
|
-
{
|
|
2952
|
-
gzip: true,
|
|
2953
|
-
file: resolvedOut,
|
|
2954
|
-
cwd: parent,
|
|
2955
|
-
// tar strips leading "/" and ".." by default, so extraction stays scoped.
|
|
2956
|
-
portable: true
|
|
2957
|
-
},
|
|
2958
|
-
[base]
|
|
2959
|
-
);
|
|
2960
|
-
}
|
|
2961
|
-
async function extractToTemp(archivePath) {
|
|
2962
|
-
const resolved = path13.resolve(archivePath);
|
|
2963
|
-
if (!await fs11.pathExists(resolved)) {
|
|
2964
|
-
throw new Error(`Archive not found: ${archivePath}`);
|
|
2965
|
-
}
|
|
2966
|
-
const tmp = await fs11.mkdtemp(path13.join(os2.tmpdir(), "replicax-import-"));
|
|
2967
|
-
await tarExtract2({ file: resolved, cwd: tmp, strip: 0 });
|
|
2968
|
-
return tmp;
|
|
2969
|
-
}
|
|
2970
|
-
async function findProfileRoot(dir) {
|
|
2971
|
-
const hasProfile = async (d) => fs11.pathExists(path13.join(d, PROFILE_FILES.profile));
|
|
2972
|
-
if (await hasProfile(dir)) return dir;
|
|
2973
|
-
const entries = await fs11.readdir(dir, { withFileTypes: true });
|
|
2974
|
-
for (const entry of entries) {
|
|
2975
|
-
if (entry.isDirectory()) {
|
|
2976
|
-
const candidate = path13.join(dir, entry.name);
|
|
2977
|
-
if (await hasProfile(candidate)) return candidate;
|
|
2978
|
-
}
|
|
2979
|
-
}
|
|
2980
|
-
return null;
|
|
2981
|
-
}
|
|
2982
|
-
|
|
2983
|
-
// src/commands/export.ts
|
|
3192
|
+
import ora5 from "ora";
|
|
2984
3193
|
async function exportCommand(options) {
|
|
2985
3194
|
const dir = options.profile ? await resolveProfileDir(options.profile) : profileDir(process.cwd());
|
|
2986
3195
|
if (!await profileExists(dir)) {
|
|
@@ -2993,7 +3202,7 @@ async function exportCommand(options) {
|
|
|
2993
3202
|
const spinner = ora5({ text: "Packaging profile\u2026" }).start();
|
|
2994
3203
|
await exportProfile(dir, outPath);
|
|
2995
3204
|
spinner.stop();
|
|
2996
|
-
const { size } = await
|
|
3205
|
+
const { size } = await fs11.stat(outPath);
|
|
2997
3206
|
logger.success(
|
|
2998
3207
|
`Exported "${bundle.profile.name}" \u2192 ${path14.relative(process.cwd(), outPath)} (${formatBytes(size)})`
|
|
2999
3208
|
);
|
|
@@ -3001,7 +3210,7 @@ async function exportCommand(options) {
|
|
|
3001
3210
|
}
|
|
3002
3211
|
|
|
3003
3212
|
// src/commands/import.ts
|
|
3004
|
-
import
|
|
3213
|
+
import fs12 from "fs-extra";
|
|
3005
3214
|
import ora6 from "ora";
|
|
3006
3215
|
import { confirm as confirm2 } from "@inquirer/prompts";
|
|
3007
3216
|
async function importCommand(archivePath, options) {
|
|
@@ -3017,6 +3226,7 @@ async function importCommand(archivePath, options) {
|
|
|
3017
3226
|
throw new ReplicaxError("The archive does not contain a ReplicaX profile.");
|
|
3018
3227
|
}
|
|
3019
3228
|
const bundle = await loadBundle(source);
|
|
3229
|
+
bundle.profile.source = "import";
|
|
3020
3230
|
spinner.succeed(`Validated profile "${bundle.profile.name}"`);
|
|
3021
3231
|
const dest = profileDir(process.cwd());
|
|
3022
3232
|
if (await profileExists(dest)) {
|
|
@@ -3029,7 +3239,7 @@ async function importCommand(archivePath, options) {
|
|
|
3029
3239
|
"Re-run with --force to overwrite it."
|
|
3030
3240
|
]);
|
|
3031
3241
|
}
|
|
3032
|
-
await
|
|
3242
|
+
await fs12.remove(dest);
|
|
3033
3243
|
}
|
|
3034
3244
|
await saveBundle(dest, bundle);
|
|
3035
3245
|
logger.newline();
|
|
@@ -3038,7 +3248,7 @@ async function importCommand(archivePath, options) {
|
|
|
3038
3248
|
);
|
|
3039
3249
|
logger.hint("Create a project with: replicax create <project-name>");
|
|
3040
3250
|
} finally {
|
|
3041
|
-
await
|
|
3251
|
+
await fs12.remove(tmp).catch(() => void 0);
|
|
3042
3252
|
}
|
|
3043
3253
|
}
|
|
3044
3254
|
|
|
@@ -3123,7 +3333,7 @@ async function doctorCommand(options) {
|
|
|
3123
3333
|
|
|
3124
3334
|
// src/commands/compare.ts
|
|
3125
3335
|
import path15 from "path";
|
|
3126
|
-
import
|
|
3336
|
+
import fs13 from "fs-extra";
|
|
3127
3337
|
|
|
3128
3338
|
// src/core/compare.ts
|
|
3129
3339
|
function detectionsOf(bundle) {
|
|
@@ -3233,7 +3443,7 @@ function comparisonHasChanges(comparison) {
|
|
|
3233
3443
|
// src/commands/compare.ts
|
|
3234
3444
|
async function resolveBundle(input) {
|
|
3235
3445
|
const resolved = path15.resolve(input);
|
|
3236
|
-
if (!await
|
|
3446
|
+
if (!await fs13.pathExists(resolved)) {
|
|
3237
3447
|
throw new ReplicaxError(`Path not found: ${input}`);
|
|
3238
3448
|
}
|
|
3239
3449
|
try {
|
|
@@ -3242,7 +3452,7 @@ async function resolveBundle(input) {
|
|
|
3242
3452
|
return { bundle: bundle2, label: `${bundle2.profile.name} (profile)` };
|
|
3243
3453
|
} catch {
|
|
3244
3454
|
}
|
|
3245
|
-
const stat = await
|
|
3455
|
+
const stat = await fs13.stat(resolved);
|
|
3246
3456
|
if (!stat.isDirectory()) {
|
|
3247
3457
|
throw new ReplicaxError(`Cannot compare "${input}": not a profile or a project directory.`, [
|
|
3248
3458
|
"Pass a project folder or a directory containing a .replicax profile."
|
|
@@ -3483,7 +3693,7 @@ program.command("init-skill").description(
|
|
|
3483
3693
|
"Generate an AI assistant skill from the detected tech stack (uses your configured AI)"
|
|
3484
3694
|
).option("--target <ai>", `Target AI assistant: ${SKILL_TARGET_IDS.join("|")}`).option("--name <name>", "Name the skill (defaults to the project folder name)").option("--provider <ai>", "Force the AI provider: claude|openai|gemini (default: auto-detect)").option("--model <id>", "Override the API model id for the chosen provider").option("--no-ai", "Skip the AI provider and use the built-in deterministic template").option("--dry-run", "Preview the skill without writing (no AI call)").option("--force", "Overwrite existing skill files").option("--verbose", "Show every detected file").action(action(initSkillCommand));
|
|
3485
3695
|
program.command("extract").argument("<repo>", "GitHub repo: owner/repo, a github.com URL, or owner/repo#branch").description("Extract a ReplicaX profile from a remote GitHub repository").option("--ref <ref>", "Branch, tag, or commit to fetch (default: the repo default branch)").option("--name <name>", "Name the profile (defaults to the repo name)").option("--out <dir>", "Directory to write the .replicax profile into (default: current dir)").option("--dry-run", "Preview what would be captured without writing").option("--verbose", "Show every detected file").action(action(extractCommand));
|
|
3486
|
-
program.command("create").argument("<project-name>", "Directory/name for the new project").description("Create a new project from a profile").option("--profile <path>", "Use a profile from a custom path").option("--skip-install", "Do not run the package manager install step").option("--dry-run", "Preview the output without writing").option("--force", "Overwrite conflicting files without prompting").option("--verbose", "Show every written file").action(action(createCommand));
|
|
3696
|
+
program.command("create").argument("<project-name>", "Directory/name for the new project").description("Create a new project from a profile").option("--profile <path>", "Use a profile from a custom path").option("--skip-install", "Do not run the package manager install step").option("--install", "Install deps even for imported/remote (untrusted) profiles").option("--dry-run", "Preview the output without writing").option("--force", "Overwrite conflicting files without prompting").option("--verbose", "Show every written file").action(action(createCommand));
|
|
3487
3697
|
program.command("sync").description("Update the profile from the current project state").option("--diff", "Show a detailed list of what changed").option("--force", "Rewrite the profile even if nothing changed").option("--verbose", "Show every detected file").action(action(syncCommand));
|
|
3488
3698
|
program.command("inspect").description("Display captured configuration and structure").option("--json", "Output as JSON").option("--section <section>", "Inspect one section: profile|tooling|structure|metadata").option("--profile <path>", "Inspect a profile at a custom path").action(action(inspectCommand));
|
|
3489
3699
|
program.command("validate").description("Check profile schema and integrity").option("--profile <path>", "Validate a profile at a custom path").action(action(validateCommand));
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@iamsaroj/replicax",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.6",
|
|
4
4
|
"description": "Copy the setup, not the code. Extract a project's tooling, structure, and conventions and recreate them in new projects.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
"bugs": {
|
|
13
13
|
"url": "https://github.com/khanalsaroj/replicaX/issues"
|
|
14
14
|
},
|
|
15
|
-
"homepage": "https://github.
|
|
15
|
+
"homepage": "https://khanalsaroj.github.io/replicaX",
|
|
16
16
|
"keywords": [
|
|
17
17
|
"cli",
|
|
18
18
|
"scaffold",
|