wyvrnpm 2.10.2 → 2.12.2
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 +1914 -1860
- package/bin/{wyvrn.js → wyvrnpm.js} +66 -0
- package/cmake/cpp.cmake +9 -9
- package/cmake/functions.cmake +224 -224
- package/cmake/macros.cmake +284 -284
- package/package.json +3 -2
- package/src/auth.js +66 -66
- package/src/binary-dir.js +95 -0
- package/src/bootstrap/cookbook.js +196 -196
- package/src/bootstrap/detect.js +150 -150
- package/src/bootstrap/index.js +220 -220
- package/src/bootstrap/version.js +72 -72
- package/src/build/cache.js +344 -344
- package/src/build/clone.js +170 -170
- package/src/build/cmake.js +342 -342
- package/src/build/index.js +299 -297
- package/src/build/msvc-env.js +260 -260
- package/src/build/recipe.js +188 -188
- package/src/commands/add.js +141 -141
- package/src/commands/bootstrap.js +96 -96
- package/src/commands/build.js +482 -452
- package/src/commands/cache.js +189 -189
- package/src/commands/clean.js +80 -80
- package/src/commands/configure.js +92 -92
- package/src/commands/init.js +70 -70
- package/src/commands/install-skill.js +115 -115
- package/src/commands/install.js +730 -674
- package/src/commands/link.js +320 -320
- package/src/commands/profile.js +237 -237
- package/src/commands/publish.js +584 -555
- package/src/commands/show.js +252 -252
- package/src/commands/version.js +187 -0
- package/src/compat.js +273 -273
- package/src/conf/index.js +415 -391
- package/src/conf/namespaces.js +94 -94
- package/src/context.js +230 -230
- package/src/http-fetch.js +53 -53
- package/src/ignore.js +118 -118
- package/src/logger.js +122 -122
- package/src/options.js +303 -303
- package/src/settings-overrides.js +152 -152
- package/src/toolchain/deps.js +164 -164
- package/src/toolchain/index.js +212 -212
- package/src/toolchain/presets.js +356 -340
- package/src/toolchain/template.cmake +77 -77
- package/src/upload-built.js +265 -265
- package/src/version-range.js +301 -301
- package/src/zip-safe.js +52 -52
- package/src/zip-stream.js +126 -0
package/src/bootstrap/index.js
CHANGED
|
@@ -1,220 +1,220 @@
|
|
|
1
|
-
'use strict';
|
|
2
|
-
|
|
3
|
-
const fs = require('fs');
|
|
4
|
-
const path = require('path');
|
|
5
|
-
const { execFileSync } = require('child_process');
|
|
6
|
-
|
|
7
|
-
const { tagToVersion, nameFromUrl } = require('./version');
|
|
8
|
-
const { detectFromRepo } = require('./detect');
|
|
9
|
-
const { lookupCookbook } = require('./cookbook');
|
|
10
|
-
const log = require('../logger');
|
|
11
|
-
|
|
12
|
-
/**
|
|
13
|
-
* Clone `gitUrl` into `destDir` so bootstrap can read the tree.
|
|
14
|
-
* Defaults to a full clone — bootstrap's target audience is "I forked
|
|
15
|
-
* these repos to maintain them", so they want the history. `--shallow`
|
|
16
|
-
* opts into `--depth 1` (faster, but `git describe --tags` will miss
|
|
17
|
-
* tags that aren't reachable from the single fetched commit, so the
|
|
18
|
-
* version detection degrades to HEAD SHA placeholder + TODO).
|
|
19
|
-
*
|
|
20
|
-
* Uses the user's ambient git auth (SSH keys, credential helper). We do
|
|
21
|
-
* NOT manage credentials here.
|
|
22
|
-
*
|
|
23
|
-
* @param {{ gitUrl: string, destDir: string, ref?: string | null, shallow?: boolean }} opts
|
|
24
|
-
*/
|
|
25
|
-
function cloneRepo({ gitUrl, destDir, ref = null, shallow = false }) {
|
|
26
|
-
const args = ['clone'];
|
|
27
|
-
if (shallow) args.push('--depth', '1');
|
|
28
|
-
if (ref) args.push('--branch', ref);
|
|
29
|
-
args.push('--', gitUrl, destDir);
|
|
30
|
-
log.info(` git ${args.join(' ')}`);
|
|
31
|
-
execFileSync('git', args, { stdio: 'inherit' });
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
/**
|
|
35
|
-
* Read HEAD SHA + nearest tag out of an existing clone. Pure
|
|
36
|
-
* read-side — never writes. Returns nulls on failure so the caller
|
|
37
|
-
* can surface a TODO rather than abort.
|
|
38
|
-
*
|
|
39
|
-
* @param {string} repoDir
|
|
40
|
-
* @returns {{ sha: string | null, tag: string | null }}
|
|
41
|
-
*/
|
|
42
|
-
function readGitState(repoDir) {
|
|
43
|
-
const safe = (args) => {
|
|
44
|
-
try {
|
|
45
|
-
return execFileSync('git', args, { cwd: repoDir, stdio: ['ignore', 'pipe', 'ignore'] })
|
|
46
|
-
.toString('utf8').trim() || null;
|
|
47
|
-
} catch { return null; }
|
|
48
|
-
};
|
|
49
|
-
return {
|
|
50
|
-
sha: safe(['rev-parse', 'HEAD']),
|
|
51
|
-
// `--abbrev=0` gives the nearest tag without the "-<N>-g<sha>" suffix.
|
|
52
|
-
tag: safe(['describe', '--tags', '--abbrev=0']),
|
|
53
|
-
};
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
/**
|
|
57
|
-
* Compose a first-draft `wyvrn.json` from detection + cookbook data.
|
|
58
|
-
* Pure: no filesystem writes, no network. `compose` is the unit-testable
|
|
59
|
-
* seam between orchestration (`bootstrap`) and persistence.
|
|
60
|
-
*
|
|
61
|
-
* Precedence for each field (highest wins):
|
|
62
|
-
* CLI override → cookbook hint → filesystem detection → safe default.
|
|
63
|
-
*
|
|
64
|
-
* @param {{
|
|
65
|
-
* url: string,
|
|
66
|
-
* repoDir: string,
|
|
67
|
-
* overrides?: { name?: string, version?: string, kind?: string, description?: string },
|
|
68
|
-
* detect?: ReturnType<typeof detectFromRepo>,
|
|
69
|
-
* gitState?: { sha: string | null, tag: string | null },
|
|
70
|
-
* }} args
|
|
71
|
-
* @returns {{ manifest: object, todos: string[] }}
|
|
72
|
-
*/
|
|
73
|
-
function compose({ url, repoDir, overrides = {}, detect = null, gitState = null }) {
|
|
74
|
-
const todos = [];
|
|
75
|
-
const detection = detect ?? detectFromRepo(repoDir);
|
|
76
|
-
const { sha, tag } = gitState ?? { sha: null, tag: null };
|
|
77
|
-
|
|
78
|
-
const repoTail = nameFromUrl(url);
|
|
79
|
-
const cookbook = lookupCookbook(repoTail) ?? {};
|
|
80
|
-
|
|
81
|
-
const name = overrides.name || repoTail;
|
|
82
|
-
if (!name) todos.push('name could not be derived from the URL — set "name" manually.');
|
|
83
|
-
|
|
84
|
-
let version = overrides.version || null;
|
|
85
|
-
if (!version && tag) {
|
|
86
|
-
version = tagToVersion(tag);
|
|
87
|
-
if (!version) {
|
|
88
|
-
todos.push(`nearest tag "${tag}" doesn't match a clean numeric version — set "version" manually (used HEAD SHA as a placeholder).`);
|
|
89
|
-
}
|
|
90
|
-
}
|
|
91
|
-
if (!version) {
|
|
92
|
-
version = '0.0.0.0';
|
|
93
|
-
todos.push('no usable git tag found — "version" defaulted to 0.0.0.0. Pin to a real release before publish.');
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
const kind = overrides.kind || cookbook.kind || detection.kind;
|
|
97
|
-
if (kind === 'unknown') {
|
|
98
|
-
todos.push('kind could not be detected — set "kind" to ConsoleApp | StaticLib | DynamicLib | HeaderOnlyLib | WebApp | Utility | Service | TestProject.');
|
|
99
|
-
}
|
|
100
|
-
for (const note of detection.notes) todos.push(`detect: ${note}`);
|
|
101
|
-
for (const note of (cookbook.notes ?? [])) todos.push(`cookbook: ${note}`);
|
|
102
|
-
|
|
103
|
-
// Safe default recipe for any CMake project. Header-only still goes
|
|
104
|
-
// through cmake --install so the INTERFACE target + install() rules
|
|
105
|
-
// land in the published zip.
|
|
106
|
-
const build = {
|
|
107
|
-
system: 'cmake',
|
|
108
|
-
configs: ['Debug', 'Release', 'RelWithDebInfo', 'MinSizeRel'],
|
|
109
|
-
configure: cookbook.configure ? [...cookbook.configure] : [],
|
|
110
|
-
installDir: 'install',
|
|
111
|
-
};
|
|
112
|
-
|
|
113
|
-
const manifest = {
|
|
114
|
-
name,
|
|
115
|
-
version,
|
|
116
|
-
schemaVersion: 2,
|
|
117
|
-
kind: kind === 'unknown' ? 'StaticLib' : kind,
|
|
118
|
-
description: overrides.description || `TODO: one-line description of ${name}`,
|
|
119
|
-
dependencies: cookbook.dependencies ? { ...cookbook.dependencies } : {},
|
|
120
|
-
build,
|
|
121
|
-
};
|
|
122
|
-
|
|
123
|
-
if (cookbook.dependencies) {
|
|
124
|
-
todos.push(
|
|
125
|
-
`dependencies seeded from cookbook: ${Object.keys(cookbook.dependencies).join(', ')}. ` +
|
|
126
|
-
`Verify versions and pin ranges before publish.`,
|
|
127
|
-
);
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
// `gitRepo` / `gitSha` are normally captured by `wyvrnpm publish` at
|
|
131
|
-
// publish time — we leave them out of the manifest (not an author
|
|
132
|
-
// field) but surface them in the TODO report so the user can
|
|
133
|
-
// double-check the clone landed at the ref they expected.
|
|
134
|
-
if (sha) todos.push(`git HEAD at bootstrap: ${sha}`);
|
|
135
|
-
if (tag) todos.push(`nearest git tag: ${tag}`);
|
|
136
|
-
|
|
137
|
-
return { manifest, todos };
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
/**
|
|
141
|
-
* Full bootstrap flow: optionally clone, read git state, compose manifest,
|
|
142
|
-
* write it to disk. Returns the result so callers (CLI, tests) can decide
|
|
143
|
-
* what to print.
|
|
144
|
-
*
|
|
145
|
-
* @param {{
|
|
146
|
-
* url: string,
|
|
147
|
-
* dest: string,
|
|
148
|
-
* ref?: string | null,
|
|
149
|
-
* name?: string,
|
|
150
|
-
* version?: string,
|
|
151
|
-
* kind?: string,
|
|
152
|
-
* description?: string,
|
|
153
|
-
* noClone?: boolean,
|
|
154
|
-
* dryRun?: boolean,
|
|
155
|
-
* force?: boolean,
|
|
156
|
-
* }} opts
|
|
157
|
-
* @returns {{ manifest: object, todos: string[], manifestPath: string, wrote: boolean }}
|
|
158
|
-
*/
|
|
159
|
-
function bootstrap(opts) {
|
|
160
|
-
const {
|
|
161
|
-
url, dest, ref = null,
|
|
162
|
-
name, version, kind, description,
|
|
163
|
-
noClone = false, dryRun = false, force = false, shallow = false,
|
|
164
|
-
} = opts;
|
|
165
|
-
|
|
166
|
-
if (!url && !noClone) throw new Error('bootstrap: --url (or a positional git URL) is required unless --no-clone');
|
|
167
|
-
if (!dest) throw new Error('bootstrap: --dest (or positional) is required');
|
|
168
|
-
|
|
169
|
-
const repoDir = path.resolve(dest);
|
|
170
|
-
|
|
171
|
-
if (!noClone) {
|
|
172
|
-
if (fs.existsSync(repoDir) && !force) {
|
|
173
|
-
throw new Error(
|
|
174
|
-
`bootstrap: destination already exists: ${repoDir}\n` +
|
|
175
|
-
' Pass --force to reuse it, or --no-clone to operate on it in place.',
|
|
176
|
-
);
|
|
177
|
-
}
|
|
178
|
-
if (fs.existsSync(repoDir) && force) {
|
|
179
|
-
fs.rmSync(repoDir, { recursive: true, force: true });
|
|
180
|
-
}
|
|
181
|
-
cloneRepo({ gitUrl: url, destDir: repoDir, ref, shallow });
|
|
182
|
-
} else if (!fs.existsSync(repoDir)) {
|
|
183
|
-
throw new Error(`bootstrap: --no-clone but directory does not exist: ${repoDir}`);
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
const detect = detectFromRepo(repoDir);
|
|
187
|
-
const gitState = readGitState(repoDir);
|
|
188
|
-
|
|
189
|
-
const { manifest, todos } = compose({
|
|
190
|
-
url: url || gitRemoteUrl(repoDir) || '',
|
|
191
|
-
repoDir,
|
|
192
|
-
overrides: { name, version, kind, description },
|
|
193
|
-
detect,
|
|
194
|
-
gitState,
|
|
195
|
-
});
|
|
196
|
-
|
|
197
|
-
const manifestPath = path.join(repoDir, 'wyvrn.json');
|
|
198
|
-
let wrote = false;
|
|
199
|
-
if (!dryRun) {
|
|
200
|
-
if (fs.existsSync(manifestPath) && !force) {
|
|
201
|
-
throw new Error(
|
|
202
|
-
`bootstrap: ${manifestPath} already exists. Pass --force to overwrite, or --dry-run to preview only.`,
|
|
203
|
-
);
|
|
204
|
-
}
|
|
205
|
-
fs.writeFileSync(manifestPath, JSON.stringify(manifest, null, 2) + '\n', 'utf8');
|
|
206
|
-
wrote = true;
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
return { manifest, todos, manifestPath, wrote };
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
function gitRemoteUrl(repoDir) {
|
|
213
|
-
try {
|
|
214
|
-
return execFileSync('git', ['remote', 'get-url', 'origin'], {
|
|
215
|
-
cwd: repoDir, stdio: ['ignore', 'pipe', 'ignore'],
|
|
216
|
-
}).toString('utf8').trim() || null;
|
|
217
|
-
} catch { return null; }
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
module.exports = { bootstrap, compose, readGitState, cloneRepo };
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
const { execFileSync } = require('child_process');
|
|
6
|
+
|
|
7
|
+
const { tagToVersion, nameFromUrl } = require('./version');
|
|
8
|
+
const { detectFromRepo } = require('./detect');
|
|
9
|
+
const { lookupCookbook } = require('./cookbook');
|
|
10
|
+
const log = require('../logger');
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Clone `gitUrl` into `destDir` so bootstrap can read the tree.
|
|
14
|
+
* Defaults to a full clone — bootstrap's target audience is "I forked
|
|
15
|
+
* these repos to maintain them", so they want the history. `--shallow`
|
|
16
|
+
* opts into `--depth 1` (faster, but `git describe --tags` will miss
|
|
17
|
+
* tags that aren't reachable from the single fetched commit, so the
|
|
18
|
+
* version detection degrades to HEAD SHA placeholder + TODO).
|
|
19
|
+
*
|
|
20
|
+
* Uses the user's ambient git auth (SSH keys, credential helper). We do
|
|
21
|
+
* NOT manage credentials here.
|
|
22
|
+
*
|
|
23
|
+
* @param {{ gitUrl: string, destDir: string, ref?: string | null, shallow?: boolean }} opts
|
|
24
|
+
*/
|
|
25
|
+
function cloneRepo({ gitUrl, destDir, ref = null, shallow = false }) {
|
|
26
|
+
const args = ['clone'];
|
|
27
|
+
if (shallow) args.push('--depth', '1');
|
|
28
|
+
if (ref) args.push('--branch', ref);
|
|
29
|
+
args.push('--', gitUrl, destDir);
|
|
30
|
+
log.info(` git ${args.join(' ')}`);
|
|
31
|
+
execFileSync('git', args, { stdio: 'inherit' });
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Read HEAD SHA + nearest tag out of an existing clone. Pure
|
|
36
|
+
* read-side — never writes. Returns nulls on failure so the caller
|
|
37
|
+
* can surface a TODO rather than abort.
|
|
38
|
+
*
|
|
39
|
+
* @param {string} repoDir
|
|
40
|
+
* @returns {{ sha: string | null, tag: string | null }}
|
|
41
|
+
*/
|
|
42
|
+
function readGitState(repoDir) {
|
|
43
|
+
const safe = (args) => {
|
|
44
|
+
try {
|
|
45
|
+
return execFileSync('git', args, { cwd: repoDir, stdio: ['ignore', 'pipe', 'ignore'] })
|
|
46
|
+
.toString('utf8').trim() || null;
|
|
47
|
+
} catch { return null; }
|
|
48
|
+
};
|
|
49
|
+
return {
|
|
50
|
+
sha: safe(['rev-parse', 'HEAD']),
|
|
51
|
+
// `--abbrev=0` gives the nearest tag without the "-<N>-g<sha>" suffix.
|
|
52
|
+
tag: safe(['describe', '--tags', '--abbrev=0']),
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Compose a first-draft `wyvrn.json` from detection + cookbook data.
|
|
58
|
+
* Pure: no filesystem writes, no network. `compose` is the unit-testable
|
|
59
|
+
* seam between orchestration (`bootstrap`) and persistence.
|
|
60
|
+
*
|
|
61
|
+
* Precedence for each field (highest wins):
|
|
62
|
+
* CLI override → cookbook hint → filesystem detection → safe default.
|
|
63
|
+
*
|
|
64
|
+
* @param {{
|
|
65
|
+
* url: string,
|
|
66
|
+
* repoDir: string,
|
|
67
|
+
* overrides?: { name?: string, version?: string, kind?: string, description?: string },
|
|
68
|
+
* detect?: ReturnType<typeof detectFromRepo>,
|
|
69
|
+
* gitState?: { sha: string | null, tag: string | null },
|
|
70
|
+
* }} args
|
|
71
|
+
* @returns {{ manifest: object, todos: string[] }}
|
|
72
|
+
*/
|
|
73
|
+
function compose({ url, repoDir, overrides = {}, detect = null, gitState = null }) {
|
|
74
|
+
const todos = [];
|
|
75
|
+
const detection = detect ?? detectFromRepo(repoDir);
|
|
76
|
+
const { sha, tag } = gitState ?? { sha: null, tag: null };
|
|
77
|
+
|
|
78
|
+
const repoTail = nameFromUrl(url);
|
|
79
|
+
const cookbook = lookupCookbook(repoTail) ?? {};
|
|
80
|
+
|
|
81
|
+
const name = overrides.name || repoTail;
|
|
82
|
+
if (!name) todos.push('name could not be derived from the URL — set "name" manually.');
|
|
83
|
+
|
|
84
|
+
let version = overrides.version || null;
|
|
85
|
+
if (!version && tag) {
|
|
86
|
+
version = tagToVersion(tag);
|
|
87
|
+
if (!version) {
|
|
88
|
+
todos.push(`nearest tag "${tag}" doesn't match a clean numeric version — set "version" manually (used HEAD SHA as a placeholder).`);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
if (!version) {
|
|
92
|
+
version = '0.0.0.0';
|
|
93
|
+
todos.push('no usable git tag found — "version" defaulted to 0.0.0.0. Pin to a real release before publish.');
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const kind = overrides.kind || cookbook.kind || detection.kind;
|
|
97
|
+
if (kind === 'unknown') {
|
|
98
|
+
todos.push('kind could not be detected — set "kind" to ConsoleApp | StaticLib | DynamicLib | HeaderOnlyLib | WebApp | Utility | Service | TestProject.');
|
|
99
|
+
}
|
|
100
|
+
for (const note of detection.notes) todos.push(`detect: ${note}`);
|
|
101
|
+
for (const note of (cookbook.notes ?? [])) todos.push(`cookbook: ${note}`);
|
|
102
|
+
|
|
103
|
+
// Safe default recipe for any CMake project. Header-only still goes
|
|
104
|
+
// through cmake --install so the INTERFACE target + install() rules
|
|
105
|
+
// land in the published zip.
|
|
106
|
+
const build = {
|
|
107
|
+
system: 'cmake',
|
|
108
|
+
configs: ['Debug', 'Release', 'RelWithDebInfo', 'MinSizeRel'],
|
|
109
|
+
configure: cookbook.configure ? [...cookbook.configure] : [],
|
|
110
|
+
installDir: 'install',
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
const manifest = {
|
|
114
|
+
name,
|
|
115
|
+
version,
|
|
116
|
+
schemaVersion: 2,
|
|
117
|
+
kind: kind === 'unknown' ? 'StaticLib' : kind,
|
|
118
|
+
description: overrides.description || `TODO: one-line description of ${name}`,
|
|
119
|
+
dependencies: cookbook.dependencies ? { ...cookbook.dependencies } : {},
|
|
120
|
+
build,
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
if (cookbook.dependencies) {
|
|
124
|
+
todos.push(
|
|
125
|
+
`dependencies seeded from cookbook: ${Object.keys(cookbook.dependencies).join(', ')}. ` +
|
|
126
|
+
`Verify versions and pin ranges before publish.`,
|
|
127
|
+
);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// `gitRepo` / `gitSha` are normally captured by `wyvrnpm publish` at
|
|
131
|
+
// publish time — we leave them out of the manifest (not an author
|
|
132
|
+
// field) but surface them in the TODO report so the user can
|
|
133
|
+
// double-check the clone landed at the ref they expected.
|
|
134
|
+
if (sha) todos.push(`git HEAD at bootstrap: ${sha}`);
|
|
135
|
+
if (tag) todos.push(`nearest git tag: ${tag}`);
|
|
136
|
+
|
|
137
|
+
return { manifest, todos };
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Full bootstrap flow: optionally clone, read git state, compose manifest,
|
|
142
|
+
* write it to disk. Returns the result so callers (CLI, tests) can decide
|
|
143
|
+
* what to print.
|
|
144
|
+
*
|
|
145
|
+
* @param {{
|
|
146
|
+
* url: string,
|
|
147
|
+
* dest: string,
|
|
148
|
+
* ref?: string | null,
|
|
149
|
+
* name?: string,
|
|
150
|
+
* version?: string,
|
|
151
|
+
* kind?: string,
|
|
152
|
+
* description?: string,
|
|
153
|
+
* noClone?: boolean,
|
|
154
|
+
* dryRun?: boolean,
|
|
155
|
+
* force?: boolean,
|
|
156
|
+
* }} opts
|
|
157
|
+
* @returns {{ manifest: object, todos: string[], manifestPath: string, wrote: boolean }}
|
|
158
|
+
*/
|
|
159
|
+
function bootstrap(opts) {
|
|
160
|
+
const {
|
|
161
|
+
url, dest, ref = null,
|
|
162
|
+
name, version, kind, description,
|
|
163
|
+
noClone = false, dryRun = false, force = false, shallow = false,
|
|
164
|
+
} = opts;
|
|
165
|
+
|
|
166
|
+
if (!url && !noClone) throw new Error('bootstrap: --url (or a positional git URL) is required unless --no-clone');
|
|
167
|
+
if (!dest) throw new Error('bootstrap: --dest (or positional) is required');
|
|
168
|
+
|
|
169
|
+
const repoDir = path.resolve(dest);
|
|
170
|
+
|
|
171
|
+
if (!noClone) {
|
|
172
|
+
if (fs.existsSync(repoDir) && !force) {
|
|
173
|
+
throw new Error(
|
|
174
|
+
`bootstrap: destination already exists: ${repoDir}\n` +
|
|
175
|
+
' Pass --force to reuse it, or --no-clone to operate on it in place.',
|
|
176
|
+
);
|
|
177
|
+
}
|
|
178
|
+
if (fs.existsSync(repoDir) && force) {
|
|
179
|
+
fs.rmSync(repoDir, { recursive: true, force: true });
|
|
180
|
+
}
|
|
181
|
+
cloneRepo({ gitUrl: url, destDir: repoDir, ref, shallow });
|
|
182
|
+
} else if (!fs.existsSync(repoDir)) {
|
|
183
|
+
throw new Error(`bootstrap: --no-clone but directory does not exist: ${repoDir}`);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
const detect = detectFromRepo(repoDir);
|
|
187
|
+
const gitState = readGitState(repoDir);
|
|
188
|
+
|
|
189
|
+
const { manifest, todos } = compose({
|
|
190
|
+
url: url || gitRemoteUrl(repoDir) || '',
|
|
191
|
+
repoDir,
|
|
192
|
+
overrides: { name, version, kind, description },
|
|
193
|
+
detect,
|
|
194
|
+
gitState,
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
const manifestPath = path.join(repoDir, 'wyvrn.json');
|
|
198
|
+
let wrote = false;
|
|
199
|
+
if (!dryRun) {
|
|
200
|
+
if (fs.existsSync(manifestPath) && !force) {
|
|
201
|
+
throw new Error(
|
|
202
|
+
`bootstrap: ${manifestPath} already exists. Pass --force to overwrite, or --dry-run to preview only.`,
|
|
203
|
+
);
|
|
204
|
+
}
|
|
205
|
+
fs.writeFileSync(manifestPath, JSON.stringify(manifest, null, 2) + '\n', 'utf8');
|
|
206
|
+
wrote = true;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
return { manifest, todos, manifestPath, wrote };
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
function gitRemoteUrl(repoDir) {
|
|
213
|
+
try {
|
|
214
|
+
return execFileSync('git', ['remote', 'get-url', 'origin'], {
|
|
215
|
+
cwd: repoDir, stdio: ['ignore', 'pipe', 'ignore'],
|
|
216
|
+
}).toString('utf8').trim() || null;
|
|
217
|
+
} catch { return null; }
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
module.exports = { bootstrap, compose, readGitState, cloneRepo };
|
package/src/bootstrap/version.js
CHANGED
|
@@ -1,72 +1,72 @@
|
|
|
1
|
-
'use strict';
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* Parse an upstream git tag into wyvrnpm's 4-part version scheme.
|
|
5
|
-
*
|
|
6
|
-
* Most C++ OSS projects publish 3-part semver tags (`v1.2.3`, `11.0.2`,
|
|
7
|
-
* `1.3.12`). wyvrnpm uses a 4-part `major.minor.patch.build` scheme
|
|
8
|
-
* throughout (see CLAUDE.md §4.2). We pad the upstream tag with `.0` for
|
|
9
|
-
* the build component so published manifests look like `1.3.12.0`.
|
|
10
|
-
*
|
|
11
|
-
* Accepted tag shapes:
|
|
12
|
-
* v1.2.3 → "1.2.3.0"
|
|
13
|
-
* 1.2.3 → "1.2.3.0"
|
|
14
|
-
* v1.2.3.4 → "1.2.3.4"
|
|
15
|
-
* v11.0 → "11.0.0.0" (2-part — padded)
|
|
16
|
-
* v11 → "11.0.0.0" (1-part — padded)
|
|
17
|
-
* release-1.2 → "1.2.0.0" (strips a leading "release-" / "ver-")
|
|
18
|
-
*
|
|
19
|
-
* Rejected (returns null):
|
|
20
|
-
* empty / non-string
|
|
21
|
-
* tags with suffixes ("1.2.3-rc1", "1.2.3+git", "1.2.3a") — callers
|
|
22
|
-
* fall back to the HEAD SHA and flag a TODO
|
|
23
|
-
* tags with 5+ numeric components
|
|
24
|
-
*
|
|
25
|
-
* @param {string | null} tag
|
|
26
|
-
* @returns {string | null} 4-part version, or null when the tag can't be
|
|
27
|
-
* cleanly promoted.
|
|
28
|
-
*/
|
|
29
|
-
function tagToVersion(tag) {
|
|
30
|
-
if (typeof tag !== 'string') return null;
|
|
31
|
-
let t = tag.trim();
|
|
32
|
-
if (!t) return null;
|
|
33
|
-
|
|
34
|
-
// Strip the most common prefixes authors use on git tags. Longest
|
|
35
|
-
// alternatives first so `version-2.0` matches the whole word, not
|
|
36
|
-
// just the leading `v`. We keep the allowlist tight — we'd rather
|
|
37
|
-
// fall through than silently misparse `release-candidate-1.2.3`.
|
|
38
|
-
t = t.replace(/^(version|release|ver|v)[-_.]?/i, '');
|
|
39
|
-
|
|
40
|
-
// Must be bare numeric segments separated by dots, nothing else.
|
|
41
|
-
if (!/^\d+(\.\d+){0,3}$/.test(t)) return null;
|
|
42
|
-
|
|
43
|
-
const parts = t.split('.').map((n) => parseInt(n, 10));
|
|
44
|
-
if (parts.some((n) => !Number.isFinite(n) || n < 0)) return null;
|
|
45
|
-
|
|
46
|
-
while (parts.length < 4) parts.push(0);
|
|
47
|
-
return parts.join('.');
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
/**
|
|
51
|
-
* Derive a wyvrnpm package name from a git URL.
|
|
52
|
-
*
|
|
53
|
-
* https://github.com/fmtlib/fmt.git → "fmt"
|
|
54
|
-
* git@github.com:fmtlib/fmt.git → "fmt"
|
|
55
|
-
* https://gitlab.com/bzip2/bzip2.git → "bzip2"
|
|
56
|
-
* https://github.com/Tencent/rapidjson.git → "rapidjson"
|
|
57
|
-
* https://github.com/nlohmann/json.git → "json" (caller may want "nlohmann-json")
|
|
58
|
-
*
|
|
59
|
-
* Pure: does not hit the network.
|
|
60
|
-
*
|
|
61
|
-
* @param {string} url
|
|
62
|
-
* @returns {string}
|
|
63
|
-
*/
|
|
64
|
-
function nameFromUrl(url) {
|
|
65
|
-
if (typeof url !== 'string' || !url.trim()) return '';
|
|
66
|
-
const trimmed = url.trim().replace(/\/$/, '');
|
|
67
|
-
// Split on the last slash or colon (SSH form).
|
|
68
|
-
const tail = trimmed.split(/[/:]/).pop() || '';
|
|
69
|
-
return tail.replace(/\.git$/i, '');
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
module.exports = { tagToVersion, nameFromUrl };
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Parse an upstream git tag into wyvrnpm's 4-part version scheme.
|
|
5
|
+
*
|
|
6
|
+
* Most C++ OSS projects publish 3-part semver tags (`v1.2.3`, `11.0.2`,
|
|
7
|
+
* `1.3.12`). wyvrnpm uses a 4-part `major.minor.patch.build` scheme
|
|
8
|
+
* throughout (see CLAUDE.md §4.2). We pad the upstream tag with `.0` for
|
|
9
|
+
* the build component so published manifests look like `1.3.12.0`.
|
|
10
|
+
*
|
|
11
|
+
* Accepted tag shapes:
|
|
12
|
+
* v1.2.3 → "1.2.3.0"
|
|
13
|
+
* 1.2.3 → "1.2.3.0"
|
|
14
|
+
* v1.2.3.4 → "1.2.3.4"
|
|
15
|
+
* v11.0 → "11.0.0.0" (2-part — padded)
|
|
16
|
+
* v11 → "11.0.0.0" (1-part — padded)
|
|
17
|
+
* release-1.2 → "1.2.0.0" (strips a leading "release-" / "ver-")
|
|
18
|
+
*
|
|
19
|
+
* Rejected (returns null):
|
|
20
|
+
* empty / non-string
|
|
21
|
+
* tags with suffixes ("1.2.3-rc1", "1.2.3+git", "1.2.3a") — callers
|
|
22
|
+
* fall back to the HEAD SHA and flag a TODO
|
|
23
|
+
* tags with 5+ numeric components
|
|
24
|
+
*
|
|
25
|
+
* @param {string | null} tag
|
|
26
|
+
* @returns {string | null} 4-part version, or null when the tag can't be
|
|
27
|
+
* cleanly promoted.
|
|
28
|
+
*/
|
|
29
|
+
function tagToVersion(tag) {
|
|
30
|
+
if (typeof tag !== 'string') return null;
|
|
31
|
+
let t = tag.trim();
|
|
32
|
+
if (!t) return null;
|
|
33
|
+
|
|
34
|
+
// Strip the most common prefixes authors use on git tags. Longest
|
|
35
|
+
// alternatives first so `version-2.0` matches the whole word, not
|
|
36
|
+
// just the leading `v`. We keep the allowlist tight — we'd rather
|
|
37
|
+
// fall through than silently misparse `release-candidate-1.2.3`.
|
|
38
|
+
t = t.replace(/^(version|release|ver|v)[-_.]?/i, '');
|
|
39
|
+
|
|
40
|
+
// Must be bare numeric segments separated by dots, nothing else.
|
|
41
|
+
if (!/^\d+(\.\d+){0,3}$/.test(t)) return null;
|
|
42
|
+
|
|
43
|
+
const parts = t.split('.').map((n) => parseInt(n, 10));
|
|
44
|
+
if (parts.some((n) => !Number.isFinite(n) || n < 0)) return null;
|
|
45
|
+
|
|
46
|
+
while (parts.length < 4) parts.push(0);
|
|
47
|
+
return parts.join('.');
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Derive a wyvrnpm package name from a git URL.
|
|
52
|
+
*
|
|
53
|
+
* https://github.com/fmtlib/fmt.git → "fmt"
|
|
54
|
+
* git@github.com:fmtlib/fmt.git → "fmt"
|
|
55
|
+
* https://gitlab.com/bzip2/bzip2.git → "bzip2"
|
|
56
|
+
* https://github.com/Tencent/rapidjson.git → "rapidjson"
|
|
57
|
+
* https://github.com/nlohmann/json.git → "json" (caller may want "nlohmann-json")
|
|
58
|
+
*
|
|
59
|
+
* Pure: does not hit the network.
|
|
60
|
+
*
|
|
61
|
+
* @param {string} url
|
|
62
|
+
* @returns {string}
|
|
63
|
+
*/
|
|
64
|
+
function nameFromUrl(url) {
|
|
65
|
+
if (typeof url !== 'string' || !url.trim()) return '';
|
|
66
|
+
const trimmed = url.trim().replace(/\/$/, '');
|
|
67
|
+
// Split on the last slash or colon (SSH form).
|
|
68
|
+
const tail = trimmed.split(/[/:]/).pop() || '';
|
|
69
|
+
return tail.replace(/\.git$/i, '');
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
module.exports = { tagToVersion, nameFromUrl };
|