harbor-templater 0.0.0

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 ADDED
@@ -0,0 +1,394 @@
1
+ # harbor-templater
2
+
3
+ A new CLI generated with oclif
4
+
5
+ ## Dynamic Templates (WIP)
6
+
7
+ This CLI is being extended into a dynamic project scaffolder (Vite-style) driven by a JSON template.
8
+
9
+ - Template JSON spec: ./docs/template-json.md
10
+ - Examples: ./docs/examples/
11
+
12
+ [![oclif](https://img.shields.io/badge/cli-oclif-brightgreen.svg)](https://oclif.io)
13
+ [![Version](https://img.shields.io/npm/v/harbor-templater.svg)](https://npmjs.org/package/harbor-templater)
14
+ [![Downloads/week](https://img.shields.io/npm/dw/harbor-templater.svg)](https://npmjs.org/package/harbor-templater)
15
+
16
+ <!-- toc -->
17
+ * [harbor-templater](#harbor-templater)
18
+ * [Usage](#usage)
19
+ * [Commands](#commands)
20
+ <!-- tocstop -->
21
+
22
+ # Usage
23
+
24
+ <!-- usage -->
25
+ ```sh-session
26
+ $ npm install -g harbor-templater
27
+ $ harbor-templater COMMAND
28
+ running command...
29
+ $ harbor-templater (--version)
30
+ harbor-templater/0.0.0 win32-x64 node-v24.1.0
31
+ $ harbor-templater --help [COMMAND]
32
+ USAGE
33
+ $ harbor-templater COMMAND
34
+ ...
35
+ ```
36
+ <!-- usagestop -->
37
+
38
+ # Commands
39
+
40
+ <!-- commands -->
41
+ * [`harbor-templater help [COMMAND]`](#harbor-templater-help-command)
42
+ * [`harbor-templater init`](#harbor-templater-init)
43
+ * [`harbor-templater plugins`](#harbor-templater-plugins)
44
+ * [`harbor-templater plugins add PLUGIN`](#harbor-templater-plugins-add-plugin)
45
+ * [`harbor-templater plugins:inspect PLUGIN...`](#harbor-templater-pluginsinspect-plugin)
46
+ * [`harbor-templater plugins install PLUGIN`](#harbor-templater-plugins-install-plugin)
47
+ * [`harbor-templater plugins link PATH`](#harbor-templater-plugins-link-path)
48
+ * [`harbor-templater plugins remove [PLUGIN]`](#harbor-templater-plugins-remove-plugin)
49
+ * [`harbor-templater plugins reset`](#harbor-templater-plugins-reset)
50
+ * [`harbor-templater plugins uninstall [PLUGIN]`](#harbor-templater-plugins-uninstall-plugin)
51
+ * [`harbor-templater plugins unlink [PLUGIN]`](#harbor-templater-plugins-unlink-plugin)
52
+ * [`harbor-templater plugins update`](#harbor-templater-plugins-update)
53
+
54
+ ## `harbor-templater help [COMMAND]`
55
+
56
+ Display help for harbor-templater.
57
+
58
+ ```
59
+ USAGE
60
+ $ harbor-templater help [COMMAND...] [-n]
61
+
62
+ ARGUMENTS
63
+ [COMMAND...] Command to show help for.
64
+
65
+ FLAGS
66
+ -n, --nested-commands Include all nested commands in the output.
67
+
68
+ DESCRIPTION
69
+ Display help for harbor-templater.
70
+ ```
71
+
72
+ _See code: [@oclif/plugin-help](https://github.com/oclif/plugin-help/blob/v6.2.36/src/commands/help.ts)_
73
+
74
+ ## `harbor-templater init`
75
+
76
+ Scaffold a project from a JSON template
77
+
78
+ ```
79
+ USAGE
80
+ $ harbor-templater init -t <value> [-o <value>] [--answer <value>...] [--defaults] [--dryRun]
81
+ [--conflict error|skip|overwrite|prompt] [--force] [--allowMissingEnv]
82
+
83
+ FLAGS
84
+ -o, --out=<value> [default: .] Base output directory (relative targets resolve from here)
85
+ -t, --template=<value> (required) Path or URL to a template JSON file
86
+ --allowMissingEnv Do not fail if an environment variable is missing for an environment step
87
+ --answer=<value>... Provide an answer: --answer key=value (repeatable)
88
+ --conflict=<option> [default: prompt] When a target already exists: error|skip|overwrite|prompt
89
+ <options: error|skip|overwrite|prompt>
90
+ --defaults Do not prompt; use defaults and provided --answer values
91
+ --dryRun Print actions without writing files or running commands
92
+ --force Overwrite existing files when copying
93
+
94
+ DESCRIPTION
95
+ Scaffold a project from a JSON template
96
+
97
+ EXAMPLES
98
+ $ harbor-templater init --template ./docs/examples/minimal.template.json --out ./my-app
99
+
100
+ $ harbor-templater init -t template.json -o . --answer projectDir=./my-app --defaults
101
+ ```
102
+
103
+ _See code: [src/commands/init/index.ts](https://github.com/bendigiorgio/harbor-templater/blob/v0.0.0/src/commands/init/index.ts)_
104
+
105
+ ## `harbor-templater plugins`
106
+
107
+ List installed plugins.
108
+
109
+ ```
110
+ USAGE
111
+ $ harbor-templater plugins [--json] [--core]
112
+
113
+ FLAGS
114
+ --core Show core plugins.
115
+
116
+ GLOBAL FLAGS
117
+ --json Format output as json.
118
+
119
+ DESCRIPTION
120
+ List installed plugins.
121
+
122
+ EXAMPLES
123
+ $ harbor-templater plugins
124
+ ```
125
+
126
+ _See code: [@oclif/plugin-plugins](https://github.com/oclif/plugin-plugins/blob/v5.4.54/src/commands/plugins/index.ts)_
127
+
128
+ ## `harbor-templater plugins add PLUGIN`
129
+
130
+ Installs a plugin into harbor-templater.
131
+
132
+ ```
133
+ USAGE
134
+ $ harbor-templater plugins add PLUGIN... [--json] [-f] [-h] [-s | -v]
135
+
136
+ ARGUMENTS
137
+ PLUGIN... Plugin to install.
138
+
139
+ FLAGS
140
+ -f, --force Force npm to fetch remote resources even if a local copy exists on disk.
141
+ -h, --help Show CLI help.
142
+ -s, --silent Silences npm output.
143
+ -v, --verbose Show verbose npm output.
144
+
145
+ GLOBAL FLAGS
146
+ --json Format output as json.
147
+
148
+ DESCRIPTION
149
+ Installs a plugin into harbor-templater.
150
+
151
+ Uses npm to install plugins.
152
+
153
+ Installation of a user-installed plugin will override a core plugin.
154
+
155
+ Use the HARBOR_TEMPLATER_NPM_LOG_LEVEL environment variable to set the npm loglevel.
156
+ Use the HARBOR_TEMPLATER_NPM_REGISTRY environment variable to set the npm registry.
157
+
158
+ ALIASES
159
+ $ harbor-templater plugins add
160
+
161
+ EXAMPLES
162
+ Install a plugin from npm registry.
163
+
164
+ $ harbor-templater plugins add myplugin
165
+
166
+ Install a plugin from a github url.
167
+
168
+ $ harbor-templater plugins add https://github.com/someuser/someplugin
169
+
170
+ Install a plugin from a github slug.
171
+
172
+ $ harbor-templater plugins add someuser/someplugin
173
+ ```
174
+
175
+ ## `harbor-templater plugins:inspect PLUGIN...`
176
+
177
+ Displays installation properties of a plugin.
178
+
179
+ ```
180
+ USAGE
181
+ $ harbor-templater plugins inspect PLUGIN...
182
+
183
+ ARGUMENTS
184
+ PLUGIN... [default: .] Plugin to inspect.
185
+
186
+ FLAGS
187
+ -h, --help Show CLI help.
188
+ -v, --verbose
189
+
190
+ GLOBAL FLAGS
191
+ --json Format output as json.
192
+
193
+ DESCRIPTION
194
+ Displays installation properties of a plugin.
195
+
196
+ EXAMPLES
197
+ $ harbor-templater plugins inspect myplugin
198
+ ```
199
+
200
+ _See code: [@oclif/plugin-plugins](https://github.com/oclif/plugin-plugins/blob/v5.4.54/src/commands/plugins/inspect.ts)_
201
+
202
+ ## `harbor-templater plugins install PLUGIN`
203
+
204
+ Installs a plugin into harbor-templater.
205
+
206
+ ```
207
+ USAGE
208
+ $ harbor-templater plugins install PLUGIN... [--json] [-f] [-h] [-s | -v]
209
+
210
+ ARGUMENTS
211
+ PLUGIN... Plugin to install.
212
+
213
+ FLAGS
214
+ -f, --force Force npm to fetch remote resources even if a local copy exists on disk.
215
+ -h, --help Show CLI help.
216
+ -s, --silent Silences npm output.
217
+ -v, --verbose Show verbose npm output.
218
+
219
+ GLOBAL FLAGS
220
+ --json Format output as json.
221
+
222
+ DESCRIPTION
223
+ Installs a plugin into harbor-templater.
224
+
225
+ Uses npm to install plugins.
226
+
227
+ Installation of a user-installed plugin will override a core plugin.
228
+
229
+ Use the HARBOR_TEMPLATER_NPM_LOG_LEVEL environment variable to set the npm loglevel.
230
+ Use the HARBOR_TEMPLATER_NPM_REGISTRY environment variable to set the npm registry.
231
+
232
+ ALIASES
233
+ $ harbor-templater plugins add
234
+
235
+ EXAMPLES
236
+ Install a plugin from npm registry.
237
+
238
+ $ harbor-templater plugins install myplugin
239
+
240
+ Install a plugin from a github url.
241
+
242
+ $ harbor-templater plugins install https://github.com/someuser/someplugin
243
+
244
+ Install a plugin from a github slug.
245
+
246
+ $ harbor-templater plugins install someuser/someplugin
247
+ ```
248
+
249
+ _See code: [@oclif/plugin-plugins](https://github.com/oclif/plugin-plugins/blob/v5.4.54/src/commands/plugins/install.ts)_
250
+
251
+ ## `harbor-templater plugins link PATH`
252
+
253
+ Links a plugin into the CLI for development.
254
+
255
+ ```
256
+ USAGE
257
+ $ harbor-templater plugins link PATH [-h] [--install] [-v]
258
+
259
+ ARGUMENTS
260
+ PATH [default: .] path to plugin
261
+
262
+ FLAGS
263
+ -h, --help Show CLI help.
264
+ -v, --verbose
265
+ --[no-]install Install dependencies after linking the plugin.
266
+
267
+ DESCRIPTION
268
+ Links a plugin into the CLI for development.
269
+
270
+ Installation of a linked plugin will override a user-installed or core plugin.
271
+
272
+ e.g. If you have a user-installed or core plugin that has a 'hello' command, installing a linked plugin with a 'hello'
273
+ command will override the user-installed or core plugin implementation. This is useful for development work.
274
+
275
+
276
+ EXAMPLES
277
+ $ harbor-templater plugins link myplugin
278
+ ```
279
+
280
+ _See code: [@oclif/plugin-plugins](https://github.com/oclif/plugin-plugins/blob/v5.4.54/src/commands/plugins/link.ts)_
281
+
282
+ ## `harbor-templater plugins remove [PLUGIN]`
283
+
284
+ Removes a plugin from the CLI.
285
+
286
+ ```
287
+ USAGE
288
+ $ harbor-templater plugins remove [PLUGIN...] [-h] [-v]
289
+
290
+ ARGUMENTS
291
+ [PLUGIN...] plugin to uninstall
292
+
293
+ FLAGS
294
+ -h, --help Show CLI help.
295
+ -v, --verbose
296
+
297
+ DESCRIPTION
298
+ Removes a plugin from the CLI.
299
+
300
+ ALIASES
301
+ $ harbor-templater plugins unlink
302
+ $ harbor-templater plugins remove
303
+
304
+ EXAMPLES
305
+ $ harbor-templater plugins remove myplugin
306
+ ```
307
+
308
+ ## `harbor-templater plugins reset`
309
+
310
+ Remove all user-installed and linked plugins.
311
+
312
+ ```
313
+ USAGE
314
+ $ harbor-templater plugins reset [--hard] [--reinstall]
315
+
316
+ FLAGS
317
+ --hard Delete node_modules and package manager related files in addition to uninstalling plugins.
318
+ --reinstall Reinstall all plugins after uninstalling.
319
+ ```
320
+
321
+ _See code: [@oclif/plugin-plugins](https://github.com/oclif/plugin-plugins/blob/v5.4.54/src/commands/plugins/reset.ts)_
322
+
323
+ ## `harbor-templater plugins uninstall [PLUGIN]`
324
+
325
+ Removes a plugin from the CLI.
326
+
327
+ ```
328
+ USAGE
329
+ $ harbor-templater plugins uninstall [PLUGIN...] [-h] [-v]
330
+
331
+ ARGUMENTS
332
+ [PLUGIN...] plugin to uninstall
333
+
334
+ FLAGS
335
+ -h, --help Show CLI help.
336
+ -v, --verbose
337
+
338
+ DESCRIPTION
339
+ Removes a plugin from the CLI.
340
+
341
+ ALIASES
342
+ $ harbor-templater plugins unlink
343
+ $ harbor-templater plugins remove
344
+
345
+ EXAMPLES
346
+ $ harbor-templater plugins uninstall myplugin
347
+ ```
348
+
349
+ _See code: [@oclif/plugin-plugins](https://github.com/oclif/plugin-plugins/blob/v5.4.54/src/commands/plugins/uninstall.ts)_
350
+
351
+ ## `harbor-templater plugins unlink [PLUGIN]`
352
+
353
+ Removes a plugin from the CLI.
354
+
355
+ ```
356
+ USAGE
357
+ $ harbor-templater plugins unlink [PLUGIN...] [-h] [-v]
358
+
359
+ ARGUMENTS
360
+ [PLUGIN...] plugin to uninstall
361
+
362
+ FLAGS
363
+ -h, --help Show CLI help.
364
+ -v, --verbose
365
+
366
+ DESCRIPTION
367
+ Removes a plugin from the CLI.
368
+
369
+ ALIASES
370
+ $ harbor-templater plugins unlink
371
+ $ harbor-templater plugins remove
372
+
373
+ EXAMPLES
374
+ $ harbor-templater plugins unlink myplugin
375
+ ```
376
+
377
+ ## `harbor-templater plugins update`
378
+
379
+ Update installed plugins.
380
+
381
+ ```
382
+ USAGE
383
+ $ harbor-templater plugins update [-h] [-v]
384
+
385
+ FLAGS
386
+ -h, --help Show CLI help.
387
+ -v, --verbose
388
+
389
+ DESCRIPTION
390
+ Update installed plugins.
391
+ ```
392
+
393
+ _See code: [@oclif/plugin-plugins](https://github.com/oclif/plugin-plugins/blob/v5.4.54/src/commands/plugins/update.ts)_
394
+ <!-- commandsstop -->
package/bin/dev.cmd ADDED
@@ -0,0 +1,3 @@
1
+ @echo off
2
+
3
+ node --loader ts-node/esm --no-warnings=ExperimentalWarning "%~dp0\dev" %*
package/bin/dev.js ADDED
@@ -0,0 +1,5 @@
1
+ #!/usr/bin/env -S node --loader ts-node/esm --disable-warning=ExperimentalWarning
2
+
3
+ import {execute} from '@oclif/core'
4
+
5
+ await execute({development: true, dir: import.meta.url})
package/bin/run.cmd ADDED
@@ -0,0 +1,3 @@
1
+ @echo off
2
+
3
+ node "%~dp0\run" %*
package/bin/run.js ADDED
@@ -0,0 +1,5 @@
1
+ #!/usr/bin/env node
2
+
3
+ import {execute} from '@oclif/core'
4
+
5
+ await execute({dir: import.meta.url})
@@ -0,0 +1,16 @@
1
+ import { Command } from "@oclif/core";
2
+ export default class Init extends Command {
3
+ static description: string;
4
+ static examples: string[];
5
+ static flags: {
6
+ template: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
7
+ out: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
8
+ answer: import("@oclif/core/interfaces").OptionFlag<string[] | undefined, import("@oclif/core/interfaces").CustomOptions>;
9
+ defaults: import("@oclif/core/interfaces").BooleanFlag<boolean>;
10
+ dryRun: import("@oclif/core/interfaces").BooleanFlag<boolean>;
11
+ conflict: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
12
+ force: import("@oclif/core/interfaces").BooleanFlag<boolean>;
13
+ allowMissingEnv: import("@oclif/core/interfaces").BooleanFlag<boolean>;
14
+ };
15
+ run(): Promise<void>;
16
+ }
@@ -0,0 +1,302 @@
1
+ import fs from "node:fs/promises";
2
+ import path from "node:path";
3
+ import { checkbox, confirm, input, select } from "@inquirer/prompts";
4
+ import { Command, Flags } from "@oclif/core";
5
+ import picomatch from "picomatch";
6
+ import { runShellCommand } from "../../lib/commands.js";
7
+ import { applyEnvironmentReplacements } from "../../lib/environment.js";
8
+ import { copyPath, ensureDir, looksLikeDirTarget, pathExists, } from "../../lib/fs-ops.js";
9
+ import { mergeIntoTarget } from "../../lib/merge.js";
10
+ import { resolveSource } from "../../lib/sources.js";
11
+ import { buildInitialContext, evaluateCondition, interpolate, resolveTargetPath, templateQuestions, templateSteps, } from "../../lib/template-engine.js";
12
+ export default class Init extends Command {
13
+ static description = "Scaffold a project from a JSON template";
14
+ static examples = [
15
+ `<%= config.bin %> <%= command.id %> --template ./docs/examples/minimal.template.json --out ./my-app`,
16
+ `<%= config.bin %> <%= command.id %> -t template.json -o . --answer projectDir=./my-app --defaults`,
17
+ ];
18
+ static flags = {
19
+ template: Flags.string({
20
+ char: "t",
21
+ description: "Path or URL to a template JSON file",
22
+ required: true,
23
+ }),
24
+ out: Flags.string({
25
+ char: "o",
26
+ description: "Base output directory (relative targets resolve from here)",
27
+ default: ".",
28
+ }),
29
+ answer: Flags.string({
30
+ description: "Provide an answer: --answer key=value (repeatable)",
31
+ multiple: true,
32
+ }),
33
+ defaults: Flags.boolean({
34
+ description: "Do not prompt; use defaults and provided --answer values",
35
+ default: false,
36
+ }),
37
+ dryRun: Flags.boolean({
38
+ description: "Print actions without writing files or running commands",
39
+ default: false,
40
+ }),
41
+ conflict: Flags.string({
42
+ description: "When a target already exists: error|skip|overwrite|prompt",
43
+ options: ["error", "skip", "overwrite", "prompt"],
44
+ default: "prompt",
45
+ }),
46
+ force: Flags.boolean({
47
+ description: "Overwrite existing files when copying",
48
+ default: false,
49
+ }),
50
+ allowMissingEnv: Flags.boolean({
51
+ description: "Do not fail if an environment variable is missing for an environment step",
52
+ default: false,
53
+ }),
54
+ };
55
+ async run() {
56
+ const { flags } = await this.parse(Init);
57
+ const outDir = path.resolve(flags.out);
58
+ await ensureDir(outDir);
59
+ const initialAnswers = parseAnswers(flags.answer ?? []);
60
+ const template = await loadTemplate(flags.template);
61
+ const ctx = buildInitialContext(outDir, initialAnswers);
62
+ await collectAnswers({
63
+ questions: templateQuestions(template),
64
+ ctx,
65
+ defaults: flags.defaults,
66
+ });
67
+ const steps = templateSteps(template);
68
+ await executeSteps({
69
+ steps,
70
+ ctx,
71
+ log: (m) => this.log(m),
72
+ dryRun: flags.dryRun,
73
+ force: flags.force,
74
+ conflict: flags.conflict,
75
+ defaults: flags.defaults,
76
+ allowMissingEnv: flags.allowMissingEnv,
77
+ });
78
+ }
79
+ }
80
+ function parseAnswers(pairs) {
81
+ const out = {};
82
+ for (const pair of pairs) {
83
+ const idx = pair.indexOf("=");
84
+ if (idx === -1)
85
+ throw new Error(`Invalid --answer: ${pair} (expected key=value)`);
86
+ const key = pair.slice(0, idx).trim();
87
+ const value = pair.slice(idx + 1).trim();
88
+ out[key] = value;
89
+ }
90
+ return out;
91
+ }
92
+ async function loadTemplate(templateRef) {
93
+ if (templateRef.startsWith("http://") || templateRef.startsWith("https://")) {
94
+ const response = await fetch(templateRef);
95
+ if (!response.ok) {
96
+ throw new Error(`Failed to download template: ${response.status} ${response.statusText}`);
97
+ }
98
+ return (await response.json());
99
+ }
100
+ const raw = await fs.readFile(path.resolve(templateRef), "utf8");
101
+ return JSON.parse(raw);
102
+ }
103
+ async function collectAnswers(args) {
104
+ for (const q of args.questions) {
105
+ if (!evaluateCondition(args.ctx, q.when))
106
+ continue;
107
+ if (q.id in args.ctx.answers)
108
+ continue;
109
+ if (args.defaults) {
110
+ if (q.default !== undefined) {
111
+ args.ctx.answers[q.id] = q.default;
112
+ continue;
113
+ }
114
+ if (q.required)
115
+ throw new Error(`Missing required answer: ${q.id}`);
116
+ continue;
117
+ }
118
+ args.ctx.answers[q.id] = await promptForQuestion(q);
119
+ if (q.required &&
120
+ (args.ctx.answers[q.id] === "" || args.ctx.answers[q.id] == null)) {
121
+ throw new Error(`Missing required answer: ${q.id}`);
122
+ }
123
+ }
124
+ }
125
+ async function promptForQuestion(q) {
126
+ switch (q.type) {
127
+ case "input":
128
+ return await input({
129
+ message: q.message,
130
+ default: typeof q.default === "string" ? q.default : undefined,
131
+ });
132
+ case "confirm":
133
+ return await confirm({
134
+ message: q.message,
135
+ default: typeof q.default === "boolean" ? q.default : undefined,
136
+ });
137
+ case "select": {
138
+ const choices = (q.options ?? []).map((o) => ({
139
+ name: o.label,
140
+ value: o.value,
141
+ }));
142
+ return await select({
143
+ message: q.message,
144
+ choices,
145
+ default: typeof q.default === "string" ? q.default : undefined,
146
+ });
147
+ }
148
+ case "multiselect": {
149
+ const defaultValues = new Set(Array.isArray(q.default) ? q.default : []);
150
+ const choices = (q.options ?? []).map((o) => ({
151
+ name: o.label,
152
+ value: o.value,
153
+ checked: defaultValues.has(o.value),
154
+ }));
155
+ return await checkbox({ message: q.message, choices });
156
+ }
157
+ }
158
+ }
159
+ async function executeSteps(args) {
160
+ for (const step of args.steps) {
161
+ if (!evaluateCondition(args.ctx, step.when))
162
+ continue;
163
+ switch (step.type) {
164
+ case "copy": {
165
+ const target = resolveTargetPath(args.ctx.outDir, interpolate(step.target, args.ctx));
166
+ const src = await resolveSource(interpolate(step.source, args.ctx));
167
+ if (args.dryRun) {
168
+ args.log(`copy ${step.source} -> ${target}`);
169
+ break;
170
+ }
171
+ const policy = effectiveConflictPolicy(args);
172
+ if (src.kind === "dir") {
173
+ const shouldProceed = await handleDirConflict({
174
+ policy,
175
+ source: step.source,
176
+ target,
177
+ defaults: args.defaults,
178
+ });
179
+ if (!shouldProceed) {
180
+ args.log(`skip copy ${step.source} -> ${target} (exists)`);
181
+ break;
182
+ }
183
+ const excludeMatcher = step.exclude?.length
184
+ ? picomatch(step.exclude, { dot: true })
185
+ : null;
186
+ await fs.cp(src.path, target, {
187
+ recursive: true,
188
+ force: policy === "overwrite" || args.force,
189
+ filter: excludeMatcher
190
+ ? (srcEntry) => {
191
+ const rel = path
192
+ .relative(src.path, srcEntry)
193
+ .replaceAll("\\", "/");
194
+ // keep root
195
+ if (!rel)
196
+ return true;
197
+ return !excludeMatcher(rel);
198
+ }
199
+ : undefined,
200
+ });
201
+ }
202
+ else {
203
+ // If target ends with '/', treat it as directory and keep filename
204
+ const finalTarget = looksLikeDirTarget(step.target)
205
+ ? path.join(target, path.basename(src.path))
206
+ : target;
207
+ const shouldProceed = await handleFileConflict({
208
+ policy,
209
+ source: step.source,
210
+ target: finalTarget,
211
+ defaults: args.defaults,
212
+ });
213
+ if (!shouldProceed) {
214
+ args.log(`skip copy ${step.source} -> ${finalTarget} (exists)`);
215
+ break;
216
+ }
217
+ await copyPath(src.path, finalTarget, {
218
+ force: policy === "overwrite" || args.force,
219
+ });
220
+ }
221
+ break;
222
+ }
223
+ case "merge": {
224
+ const target = resolveTargetPath(args.ctx.outDir, interpolate(step.target, args.ctx));
225
+ const src = await resolveSource(interpolate(step.source, args.ctx));
226
+ if (src.kind !== "file")
227
+ throw new Error("merge source must resolve to a file");
228
+ if (args.dryRun) {
229
+ args.log(`merge ${step.source} -> ${target}`);
230
+ break;
231
+ }
232
+ await mergeIntoTarget(src.path, target, step.merge ?? {});
233
+ break;
234
+ }
235
+ case "environment": {
236
+ const target = resolveTargetPath(args.ctx.outDir, interpolate(step.target, args.ctx));
237
+ if (args.dryRun) {
238
+ args.log(`environment ${target}`);
239
+ break;
240
+ }
241
+ await applyEnvironmentReplacements(target, step.variables, {
242
+ allowMissing: args.allowMissingEnv,
243
+ });
244
+ break;
245
+ }
246
+ case "command": {
247
+ const cwd = resolveTargetPath(args.ctx.outDir, step.workingDirectory
248
+ ? interpolate(step.workingDirectory, args.ctx)
249
+ : args.ctx.outDir);
250
+ const cmd = interpolate(step.command, args.ctx);
251
+ if (args.dryRun) {
252
+ args.log(`command (${cwd}): ${cmd}`);
253
+ break;
254
+ }
255
+ await runShellCommand(cmd, cwd);
256
+ break;
257
+ }
258
+ }
259
+ }
260
+ }
261
+ function effectiveConflictPolicy(args) {
262
+ // If user asked for no prompting, don't prompt on conflicts.
263
+ if (args.defaults && args.conflict === "prompt")
264
+ return "error";
265
+ return args.conflict;
266
+ }
267
+ async function handleFileConflict(args) {
268
+ const exists = await pathExists(args.target);
269
+ if (!exists)
270
+ return true;
271
+ switch (args.policy) {
272
+ case "overwrite":
273
+ return true;
274
+ case "skip":
275
+ return false;
276
+ case "error":
277
+ throw new Error(`Target exists: ${args.target}`);
278
+ case "prompt":
279
+ return await confirm({
280
+ message: `Overwrite ${args.target}?`,
281
+ default: false,
282
+ });
283
+ }
284
+ }
285
+ async function handleDirConflict(args) {
286
+ const exists = await pathExists(args.target);
287
+ if (!exists)
288
+ return true;
289
+ switch (args.policy) {
290
+ case "overwrite":
291
+ return true;
292
+ case "skip":
293
+ return false;
294
+ case "error":
295
+ throw new Error(`Target exists: ${args.target}`);
296
+ case "prompt":
297
+ return await confirm({
298
+ message: `Directory exists. Merge/overwrite into ${args.target}?`,
299
+ default: false,
300
+ });
301
+ }
302
+ }
@@ -0,0 +1 @@
1
+ export { run } from '@oclif/core';
package/dist/index.js ADDED
@@ -0,0 +1 @@
1
+ export { run } from '@oclif/core';
@@ -0,0 +1 @@
1
+ export declare function runShellCommand(command: string, cwd: string): Promise<void>;
@@ -0,0 +1,13 @@
1
+ import { spawn } from "node:child_process";
2
+ export async function runShellCommand(command, cwd) {
3
+ await new Promise((resolve, reject) => {
4
+ const child = spawn(command, { cwd, shell: true, stdio: "inherit" });
5
+ child.on("error", reject);
6
+ child.on("exit", (code) => {
7
+ if (code === 0)
8
+ resolve();
9
+ else
10
+ reject(new Error(`Command failed (${code}): ${command}`));
11
+ });
12
+ });
13
+ }
@@ -0,0 +1,3 @@
1
+ export declare function applyEnvironmentReplacements(targetPath: string, variables: Record<string, string>, options: {
2
+ allowMissing: boolean;
3
+ }): Promise<void>;
@@ -0,0 +1,14 @@
1
+ import fs from "node:fs/promises";
2
+ export async function applyEnvironmentReplacements(targetPath, variables, options) {
3
+ let contents = await fs.readFile(targetPath, "utf8");
4
+ for (const [envName, placeholder] of Object.entries(variables)) {
5
+ const value = process.env[envName];
6
+ if (value == null) {
7
+ if (options.allowMissing)
8
+ continue;
9
+ throw new Error(`Missing environment variable: ${envName}`);
10
+ }
11
+ contents = contents.split(placeholder).join(value);
12
+ }
13
+ await fs.writeFile(targetPath, contents, "utf8");
14
+ }
@@ -0,0 +1,6 @@
1
+ export declare function ensureDir(dirPath: string): Promise<void>;
2
+ export declare function pathExists(targetPath: string): Promise<boolean>;
3
+ export declare function copyPath(sourcePath: string, targetPath: string, options: {
4
+ force: boolean;
5
+ }): Promise<void>;
6
+ export declare function looksLikeDirTarget(target: string): boolean;
@@ -0,0 +1,32 @@
1
+ import fs from "node:fs/promises";
2
+ import path from "node:path";
3
+ export async function ensureDir(dirPath) {
4
+ await fs.mkdir(dirPath, { recursive: true });
5
+ }
6
+ export async function pathExists(targetPath) {
7
+ return await fs
8
+ .access(targetPath)
9
+ .then(() => true)
10
+ .catch(() => false);
11
+ }
12
+ export async function copyPath(sourcePath, targetPath, options) {
13
+ const stat = await fs.stat(sourcePath);
14
+ if (stat.isDirectory()) {
15
+ await ensureDir(targetPath);
16
+ // Node 18+ supports fs.cp
17
+ await fs.cp(sourcePath, targetPath, {
18
+ recursive: true,
19
+ force: options.force,
20
+ });
21
+ return;
22
+ }
23
+ await ensureDir(path.dirname(targetPath));
24
+ if (!options.force) {
25
+ if (await pathExists(targetPath))
26
+ throw new Error(`Target exists: ${targetPath}`);
27
+ }
28
+ await fs.copyFile(sourcePath, targetPath);
29
+ }
30
+ export function looksLikeDirTarget(target) {
31
+ return target.endsWith("/") || target.endsWith("\\");
32
+ }
@@ -0,0 +1,5 @@
1
+ export type MergeOptions = {
2
+ format?: "json" | "yaml" | "text";
3
+ strategy?: "deep" | "shallow" | "append" | "prepend";
4
+ };
5
+ export declare function mergeIntoTarget(sourcePath: string, targetPath: string, options: MergeOptions): Promise<void>;
@@ -0,0 +1,65 @@
1
+ import fs from "node:fs/promises";
2
+ import path from "node:path";
3
+ import { ensureDir } from "./fs-ops.js";
4
+ export async function mergeIntoTarget(sourcePath, targetPath, options) {
5
+ const format = options.format ?? "json";
6
+ const strategy = options.strategy ?? "deep";
7
+ if (format === "yaml") {
8
+ throw new Error("YAML merge is not implemented yet");
9
+ }
10
+ await ensureDir(path.dirname(targetPath));
11
+ if (format === "text") {
12
+ const incoming = await fs.readFile(sourcePath, "utf8");
13
+ let existing = "";
14
+ try {
15
+ existing = await fs.readFile(targetPath, "utf8");
16
+ }
17
+ catch {
18
+ // doesn't exist
19
+ }
20
+ const merged = strategy === "prepend"
21
+ ? `${incoming}${existing}`
22
+ : strategy === "append"
23
+ ? `${existing}${incoming}`
24
+ : incoming;
25
+ await fs.writeFile(targetPath, merged, "utf8");
26
+ return;
27
+ }
28
+ // json
29
+ const incoming = JSON.parse(await fs.readFile(sourcePath, "utf8"));
30
+ let existing = {};
31
+ try {
32
+ existing = JSON.parse(await fs.readFile(targetPath, "utf8"));
33
+ }
34
+ catch {
35
+ existing = {};
36
+ }
37
+ const merged = strategy === "shallow"
38
+ ? shallowMerge(existing, incoming)
39
+ : deepMerge(existing, incoming);
40
+ await fs.writeFile(targetPath, `${JSON.stringify(merged, null, 2)}\n`, "utf8");
41
+ }
42
+ function shallowMerge(a, b) {
43
+ if (!isPlainObject(a) || !isPlainObject(b))
44
+ return b;
45
+ return { ...a, ...b };
46
+ }
47
+ function deepMerge(a, b) {
48
+ if (Array.isArray(a) && Array.isArray(b))
49
+ return [...a, ...b];
50
+ if (!isPlainObject(a) || !isPlainObject(b))
51
+ return b;
52
+ const out = { ...a };
53
+ for (const [key, value] of Object.entries(b)) {
54
+ if (Object.hasOwn(out, key)) {
55
+ out[key] = deepMerge(out[key], value);
56
+ }
57
+ else {
58
+ out[key] = value;
59
+ }
60
+ }
61
+ return out;
62
+ }
63
+ function isPlainObject(value) {
64
+ return value != null && typeof value === "object" && !Array.isArray(value);
65
+ }
@@ -0,0 +1,8 @@
1
+ export type ResolvedSource = {
2
+ kind: "file";
3
+ path: string;
4
+ } | {
5
+ kind: "dir";
6
+ path: string;
7
+ };
8
+ export declare function resolveSource(source: string): Promise<ResolvedSource>;
@@ -0,0 +1,77 @@
1
+ import fs from "node:fs/promises";
2
+ import os from "node:os";
3
+ import path from "node:path";
4
+ import { x as untar } from "tar";
5
+ export async function resolveSource(source) {
6
+ if (source.startsWith("http://") || source.startsWith("https://")) {
7
+ return await downloadUrlToTempFile(source);
8
+ }
9
+ if (source.startsWith("github:")) {
10
+ return await downloadGitHubToTemp(source);
11
+ }
12
+ // local filesystem
13
+ const localPath = path.resolve(source);
14
+ const stat = await fs.stat(localPath);
15
+ return stat.isDirectory()
16
+ ? { kind: "dir", path: localPath }
17
+ : { kind: "file", path: localPath };
18
+ }
19
+ async function downloadUrlToTempFile(url) {
20
+ const response = await fetch(url);
21
+ if (!response.ok)
22
+ throw new Error(`Failed to download ${url}: ${response.status} ${response.statusText}`);
23
+ const buffer = Buffer.from(await response.arrayBuffer());
24
+ const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "harbor-templater-url-"));
25
+ const filePath = path.join(tmpDir, "download");
26
+ await fs.writeFile(filePath, buffer);
27
+ return { kind: "file", path: filePath };
28
+ }
29
+ // github:<owner>/<repo>#<ref>:<path>
30
+ // If <path> points to a directory, returns kind=dir.
31
+ async function downloadGitHubToTemp(source) {
32
+ const parsed = parseGitHubSource(source);
33
+ const tarballUrl = `https://codeload.github.com/${parsed.owner}/${parsed.repo}/tar.gz/${parsed.ref}`;
34
+ const response = await fetch(tarballUrl);
35
+ if (!response.ok) {
36
+ throw new Error(`Failed to download ${tarballUrl}: ${response.status} ${response.statusText}`);
37
+ }
38
+ const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "harbor-templater-gh-"));
39
+ const tarPath = path.join(tmpDir, "repo.tgz");
40
+ const buffer = Buffer.from(await response.arrayBuffer());
41
+ await fs.writeFile(tarPath, buffer);
42
+ // GitHub tarballs wrap content in a single top-level folder: <repo>-<shaOrRef>/
43
+ const extractDir = path.join(tmpDir, "extract");
44
+ await fs.mkdir(extractDir);
45
+ await untar({ file: tarPath, cwd: extractDir });
46
+ const entries = await fs.readdir(extractDir);
47
+ if (entries.length === 0)
48
+ throw new Error("Downloaded GitHub tarball was empty");
49
+ const firstEntry = entries[0];
50
+ if (!firstEntry)
51
+ throw new Error("Downloaded GitHub tarball was empty");
52
+ const root = path.join(extractDir, firstEntry);
53
+ const candidate = path.join(root, parsed.subpath);
54
+ const stat = await fs.stat(candidate);
55
+ return stat.isDirectory()
56
+ ? { kind: "dir", path: candidate }
57
+ : { kind: "file", path: candidate };
58
+ }
59
+ function parseGitHubSource(input) {
60
+ const trimmed = input.slice("github:".length);
61
+ const hashIdx = trimmed.indexOf("#");
62
+ const colonIdx = trimmed.indexOf(":");
63
+ if (hashIdx === -1 || colonIdx === -1 || colonIdx < hashIdx) {
64
+ throw new Error(`Invalid github source: ${input}. Expected github:<owner>/<repo>#<ref>:<path>`);
65
+ }
66
+ const repoPart = trimmed.slice(0, hashIdx);
67
+ const refPart = trimmed.slice(hashIdx + 1, colonIdx);
68
+ const pathPart = trimmed.slice(colonIdx + 1);
69
+ const [owner, repo] = repoPart.split("/");
70
+ if (!owner || !repo)
71
+ throw new Error(`Invalid github source: ${input} (missing owner/repo)`);
72
+ if (!refPart)
73
+ throw new Error(`Invalid github source: ${input} (missing ref)`);
74
+ if (!pathPart)
75
+ throw new Error(`Invalid github source: ${input} (missing path)`);
76
+ return { owner, repo, ref: refPart, subpath: pathPart };
77
+ }
@@ -0,0 +1,12 @@
1
+ import type { Condition, JSONTemplate, TemplateQuestion, TemplateStep, ValueRef } from "./template-json.js";
2
+ export type TemplateContext = {
3
+ answers: Record<string, unknown>;
4
+ outDir: string;
5
+ };
6
+ export declare function buildInitialContext(outDir: string, initialAnswers?: Record<string, unknown>): TemplateContext;
7
+ export declare function getRefValue(ctx: TemplateContext, ref: ValueRef): unknown;
8
+ export declare function evaluateCondition(ctx: TemplateContext, condition?: Condition): boolean;
9
+ export declare function interpolate(input: string, ctx: TemplateContext): string;
10
+ export declare function resolveTargetPath(outDir: string, target: string): string;
11
+ export declare function templateQuestions(template: JSONTemplate): TemplateQuestion[];
12
+ export declare function templateSteps(template: JSONTemplate): TemplateStep[];
@@ -0,0 +1,76 @@
1
+ import path from "node:path";
2
+ export function buildInitialContext(outDir, initialAnswers) {
3
+ return {
4
+ answers: { ...(initialAnswers ?? {}) },
5
+ outDir,
6
+ };
7
+ }
8
+ export function getRefValue(ctx, ref) {
9
+ const refPath = ref.ref;
10
+ if (!refPath)
11
+ return undefined;
12
+ const parts = refPath.split(".");
13
+ let current = ctx;
14
+ for (const part of parts) {
15
+ if (current == null)
16
+ return undefined;
17
+ if (typeof current !== "object")
18
+ return undefined;
19
+ const record = current;
20
+ current = record[part];
21
+ }
22
+ return current;
23
+ }
24
+ export function evaluateCondition(ctx, condition) {
25
+ if (!condition)
26
+ return true;
27
+ switch (condition.op) {
28
+ case "eq":
29
+ return getRefValue(ctx, condition.left) === condition.right;
30
+ case "neq":
31
+ return getRefValue(ctx, condition.left) !== condition.right;
32
+ case "in": {
33
+ const value = getRefValue(ctx, condition.left);
34
+ return Array.isArray(condition.right) && condition.right.includes(value);
35
+ }
36
+ case "notIn": {
37
+ const value = getRefValue(ctx, condition.left);
38
+ return Array.isArray(condition.right) && !condition.right.includes(value);
39
+ }
40
+ case "truthy":
41
+ return Boolean(getRefValue(ctx, condition.value));
42
+ case "falsy":
43
+ return !getRefValue(ctx, condition.value);
44
+ case "exists": {
45
+ const value = getRefValue(ctx, condition.value);
46
+ return value !== undefined && value !== null;
47
+ }
48
+ case "and":
49
+ return condition.conditions.every((c) => evaluateCondition(ctx, c));
50
+ case "or":
51
+ return condition.conditions.some((c) => evaluateCondition(ctx, c));
52
+ case "not":
53
+ return !evaluateCondition(ctx, condition.condition);
54
+ }
55
+ }
56
+ const INTERPOLATION = /\{\{\s*([^}]+?)\s*\}\}/g;
57
+ export function interpolate(input, ctx) {
58
+ return input.replace(INTERPOLATION, (_match, expr) => {
59
+ const trimmed = String(expr ?? "").trim();
60
+ // Support {{answers.foo}} and {{outDir}}
61
+ if (trimmed === "outDir")
62
+ return ctx.outDir;
63
+ // Any other expression is treated as a ref path
64
+ const value = getRefValue(ctx, { ref: trimmed });
65
+ return value == null ? "" : String(value);
66
+ });
67
+ }
68
+ export function resolveTargetPath(outDir, target) {
69
+ return path.isAbsolute(target) ? target : path.resolve(outDir, target);
70
+ }
71
+ export function templateQuestions(template) {
72
+ return template.questions ?? [];
73
+ }
74
+ export function templateSteps(template) {
75
+ return template.steps;
76
+ }
@@ -0,0 +1,71 @@
1
+ export type JSONTemplate = {
2
+ author?: string;
3
+ description?: string;
4
+ name: string;
5
+ version: string;
6
+ questions?: TemplateQuestion[];
7
+ steps: TemplateStep[];
8
+ };
9
+ export type TemplateQuestion = {
10
+ id: string;
11
+ type: "input" | "confirm" | "select" | "multiselect";
12
+ message: string;
13
+ default?: string | boolean | string[];
14
+ required?: boolean;
15
+ options?: Array<{
16
+ label: string;
17
+ value: string;
18
+ }>;
19
+ when?: Condition;
20
+ };
21
+ export type TemplateStep = CopyStep | MergeStep | EnvironmentStep | CommandStep;
22
+ type StepBase = {
23
+ when?: Condition;
24
+ };
25
+ export type CopyStep = StepBase & {
26
+ type: "copy";
27
+ source: string;
28
+ target: string;
29
+ exclude?: string[];
30
+ };
31
+ export type MergeStep = StepBase & {
32
+ type: "merge";
33
+ source: string;
34
+ target: string;
35
+ merge?: {
36
+ format?: "json" | "yaml" | "text";
37
+ strategy?: "deep" | "shallow" | "append" | "prepend";
38
+ };
39
+ };
40
+ export type EnvironmentStep = StepBase & {
41
+ type: "environment";
42
+ target: string;
43
+ variables: Record<string, string>;
44
+ };
45
+ export type CommandStep = StepBase & {
46
+ type: "command";
47
+ command: string;
48
+ workingDirectory?: string;
49
+ };
50
+ export type ValueRef = {
51
+ ref: string;
52
+ };
53
+ export type Condition = {
54
+ op: "eq" | "neq";
55
+ left: ValueRef;
56
+ right: unknown;
57
+ } | {
58
+ op: "in" | "notIn";
59
+ left: ValueRef;
60
+ right: unknown[];
61
+ } | {
62
+ op: "truthy" | "falsy" | "exists";
63
+ value: ValueRef;
64
+ } | {
65
+ op: "and" | "or";
66
+ conditions: Condition[];
67
+ } | {
68
+ op: "not";
69
+ condition: Condition;
70
+ };
71
+ export {};
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,94 @@
1
+ {
2
+ "commands": {
3
+ "init": {
4
+ "aliases": [],
5
+ "args": {},
6
+ "description": "Scaffold a project from a JSON template",
7
+ "examples": [
8
+ "<%= config.bin %> <%= command.id %> --template ./docs/examples/minimal.template.json --out ./my-app",
9
+ "<%= config.bin %> <%= command.id %> -t template.json -o . --answer projectDir=./my-app --defaults"
10
+ ],
11
+ "flags": {
12
+ "template": {
13
+ "char": "t",
14
+ "description": "Path or URL to a template JSON file",
15
+ "name": "template",
16
+ "required": true,
17
+ "hasDynamicHelp": false,
18
+ "multiple": false,
19
+ "type": "option"
20
+ },
21
+ "out": {
22
+ "char": "o",
23
+ "description": "Base output directory (relative targets resolve from here)",
24
+ "name": "out",
25
+ "default": ".",
26
+ "hasDynamicHelp": false,
27
+ "multiple": false,
28
+ "type": "option"
29
+ },
30
+ "answer": {
31
+ "description": "Provide an answer: --answer key=value (repeatable)",
32
+ "name": "answer",
33
+ "hasDynamicHelp": false,
34
+ "multiple": true,
35
+ "type": "option"
36
+ },
37
+ "defaults": {
38
+ "description": "Do not prompt; use defaults and provided --answer values",
39
+ "name": "defaults",
40
+ "allowNo": false,
41
+ "type": "boolean"
42
+ },
43
+ "dryRun": {
44
+ "description": "Print actions without writing files or running commands",
45
+ "name": "dryRun",
46
+ "allowNo": false,
47
+ "type": "boolean"
48
+ },
49
+ "conflict": {
50
+ "description": "When a target already exists: error|skip|overwrite|prompt",
51
+ "name": "conflict",
52
+ "default": "prompt",
53
+ "hasDynamicHelp": false,
54
+ "multiple": false,
55
+ "options": [
56
+ "error",
57
+ "skip",
58
+ "overwrite",
59
+ "prompt"
60
+ ],
61
+ "type": "option"
62
+ },
63
+ "force": {
64
+ "description": "Overwrite existing files when copying",
65
+ "name": "force",
66
+ "allowNo": false,
67
+ "type": "boolean"
68
+ },
69
+ "allowMissingEnv": {
70
+ "description": "Do not fail if an environment variable is missing for an environment step",
71
+ "name": "allowMissingEnv",
72
+ "allowNo": false,
73
+ "type": "boolean"
74
+ }
75
+ },
76
+ "hasDynamicHelp": false,
77
+ "hiddenAliases": [],
78
+ "id": "init",
79
+ "pluginAlias": "harbor-templater",
80
+ "pluginName": "harbor-templater",
81
+ "pluginType": "core",
82
+ "strict": true,
83
+ "enableJsonFlag": false,
84
+ "isESM": true,
85
+ "relativePath": [
86
+ "dist",
87
+ "commands",
88
+ "init",
89
+ "index.js"
90
+ ]
91
+ }
92
+ },
93
+ "version": "0.0.0"
94
+ }
package/package.json ADDED
@@ -0,0 +1,95 @@
1
+ {
2
+ "name": "harbor-templater",
3
+ "description": "A CLI tool for scaffolding projects using Harbor templates",
4
+ "version": "0.0.0",
5
+ "author": "Ben Di Giorgio",
6
+ "bin": {
7
+ "harbor-templater": "./bin/run.js"
8
+ },
9
+ "bugs": "https://github.com/bendigiorgio/harbor-templater/issues",
10
+ "dependencies": {
11
+ "@inquirer/prompts": "^8.2.0",
12
+ "@oclif/core": "^4",
13
+ "@oclif/plugin-help": "^6",
14
+ "@oclif/plugin-plugins": "^5",
15
+ "picomatch": "^4.0.3",
16
+ "tar": "^7.5.3"
17
+ },
18
+ "devDependencies": {
19
+ "@biomejs/biome": "^2.3.11",
20
+ "@commitlint/cli": "^20.3.1",
21
+ "@commitlint/config-conventional": "^20.3.1",
22
+ "@eslint/compat": "^1",
23
+ "@oclif/prettier-config": "^0.2.1",
24
+ "@oclif/test": "^4",
25
+ "@semantic-release/changelog": "^6.0.3",
26
+ "@semantic-release/commit-analyzer": "^13.0.1",
27
+ "@semantic-release/exec": "^7.1.0",
28
+ "@semantic-release/git": "^10.0.1",
29
+ "@semantic-release/github": "^12.0.2",
30
+ "@semantic-release/npm": "^13.1.3",
31
+ "@semantic-release/release-notes-generator": "^14.1.0",
32
+ "@types/chai": "^4",
33
+ "@types/mocha": "^10",
34
+ "@types/node": "^24.10.9",
35
+ "@types/picomatch": "^4.0.2",
36
+ "chai": "^4",
37
+ "eslint": "^9",
38
+ "eslint-config-oclif": "^6",
39
+ "eslint-config-prettier": "^10",
40
+ "husky": "^9.1.7",
41
+ "mocha": "^10",
42
+ "oclif": "^4",
43
+ "semantic-release": "^25.0.2",
44
+ "shx": "^0.3.3",
45
+ "ts-node": "^10",
46
+ "typescript": "^5"
47
+ },
48
+ "engines": {
49
+ "node": ">=18.0.0"
50
+ },
51
+ "files": [
52
+ "./bin",
53
+ "./dist",
54
+ "./oclif.manifest.json"
55
+ ],
56
+ "homepage": "https://github.com/bendigiorgio/harbor-templater",
57
+ "keywords": [
58
+ "oclif"
59
+ ],
60
+ "license": "MIT",
61
+ "main": "dist/index.js",
62
+ "type": "module",
63
+ "publishConfig": {
64
+ "access": "public"
65
+ },
66
+ "oclif": {
67
+ "bin": "harbor-templater",
68
+ "dirname": "harbor-templater",
69
+ "commands": "./dist/commands",
70
+ "plugins": [
71
+ "@oclif/plugin-help",
72
+ "@oclif/plugin-plugins"
73
+ ],
74
+ "topicSeparator": " "
75
+ },
76
+ "repository": "bendigiorgio/harbor-templater",
77
+ "scripts": {
78
+ "build": "shx rm -rf dist && shx rm -f tsconfig.tsbuildinfo && tsc -p tsconfig.json",
79
+ "format": "biome format",
80
+ "lint": "biome lint",
81
+ "check": "biome check",
82
+ "lint:fix": "biome lint --fix",
83
+ "postpack": "shx rm -f oclif.manifest.json",
84
+ "posttest": "pnpm run lint",
85
+ "prepack": "pnpm run build && oclif manifest && oclif readme",
86
+ "prepublishOnly": "pnpm test && pnpm run build && pnpm run prepack",
87
+ "prepare": "husky",
88
+ "commitlint": "commitlint --edit",
89
+ "release": "semantic-release",
90
+ "test": "mocha --forbid-only \"test/**/*.test.ts\"",
91
+ "version": "oclif readme && git add README.md"
92
+ },
93
+ "types": "dist/index.d.ts",
94
+ "packageManager": "pnpm@10.12.1+sha512.f0dda8580f0ee9481c5c79a1d927b9164f2c478e90992ad268bbb2465a736984391d6333d2c327913578b2804af33474ca554ba29c04a8b13060a717675ae3ac"
95
+ }