create-promptex-js 1.0.0 → 1.1.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 +102 -18
- package/dist/contract.generated.d.ts +24 -0
- package/dist/contract.generated.js +59 -0
- package/dist/index.js +356 -176
- package/dist/messages.generated.js +145 -61
- package/dist/plan.d.ts +105 -0
- package/dist/plan.js +327 -0
- package/package.json +3 -2
- package/templates/adapter-starter/README.md +45 -0
- package/templates/adapter-starter/_gitignore +6 -0
- package/templates/adapter-starter/example/promptex.config.ts +12 -0
- package/templates/adapter-starter/example/prompts/nodes.ts +18 -0
- package/templates/adapter-starter/package.json +28 -0
- package/templates/adapter-starter/promptex.config.schema.json +12 -0
- package/templates/adapter-starter/src/index.ts +93 -0
- package/templates/adapter-starter/tsconfig.json +14 -0
- package/templates/plugin-starter/README.md +40 -0
- package/templates/plugin-starter/_gitignore +5 -0
- package/templates/plugin-starter/example/promptex.config.ts +12 -0
- package/templates/plugin-starter/example/prompts/nodes.ts +16 -0
- package/templates/plugin-starter/package.json +28 -0
- package/templates/plugin-starter/promptex.config.schema.json +12 -0
- package/templates/plugin-starter/src/index.ts +63 -0
- package/templates/plugin-starter/tsconfig.json +14 -0
- package/templates/project-starter/README.md +30 -0
- package/templates/project-starter/_gitignore +11 -0
- package/templates/project-starter/package.json +13 -0
- package/templates/project-starter/promptex.config.ts +8 -0
- package/templates/project-starter/prompts/example-skill.ts +14 -0
- package/templates/project-starter/prompts/example.ts +14 -0
- package/dist/default-ref.d.ts +0 -6
- package/dist/default-ref.js +0 -14
package/dist/index.js
CHANGED
|
@@ -1,27 +1,32 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
// 鷹架 CLI
|
|
3
|
-
//
|
|
4
|
-
//
|
|
2
|
+
// 鷹架 CLI:開出 TypeScript 的 project/plugin/adapter 專案,或在既有資料夾加入 promptex。
|
|
3
|
+
// 目標不存在或只有系統、版本控制、編輯器的檔案時走建立模式,否則走初始化模式
|
|
4
|
+
// (m007 d004)。規則與套用在 plan.ts,本檔負責旗標、互動、輸出與結束碼。
|
|
5
5
|
//
|
|
6
|
-
//
|
|
7
|
-
//
|
|
8
|
-
//
|
|
6
|
+
// Templates ship inside this package (templates/), so every version scaffolds the
|
|
7
|
+
// templates it was released with, offline. --source points at a local templates
|
|
8
|
+
// directory (the one holding the *-starter folders) for template development.
|
|
9
9
|
//
|
|
10
10
|
// 用法:
|
|
11
|
-
//
|
|
12
|
-
// --name <kebab 名> --
|
|
13
|
-
// --
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
import {
|
|
17
|
-
import {
|
|
11
|
+
// pnpm create promptex-js [目標目錄] [--template project|plugin|adapter] \
|
|
12
|
+
// [--name <kebab 名>] [--source <模板目錄>] [--git|--no-git] [--install] \
|
|
13
|
+
// [--force] [--dry-run [--json]] [--yes] [--locale <語系>]
|
|
14
|
+
//
|
|
15
|
+
// 結束碼:0 成功(含已初始化),1 執行失敗,2 用法錯誤(ADR-041)。
|
|
16
|
+
import { spawnSync } from 'node:child_process';
|
|
17
|
+
import { existsSync, readFileSync, readdirSync } from 'node:fs';
|
|
18
|
+
import { basename, join, resolve } from 'node:path';
|
|
18
19
|
import { fileURLToPath } from 'node:url';
|
|
19
20
|
import * as p from '@clack/prompts';
|
|
20
21
|
import mri from 'mri';
|
|
21
|
-
import {
|
|
22
|
+
import { errorCode, FORMAT_VERSION } from './contract.generated.js';
|
|
22
23
|
import { resolveLocale, t } from './messages.generated.js';
|
|
24
|
+
import { applyPlan, cdPath, commandAvailable, CreateError, detectPackageManager, findConfig, inspectTarget, isInsideGitWorktree, isNonInteractive, missingInputs, nextSteps, planFiles, PM_COMMANDS, summarize, } from './plan.js';
|
|
23
25
|
const here = fileURLToPath(new URL('.', import.meta.url));
|
|
24
26
|
const ownVersion = JSON.parse(readFileSync(join(here, '..', 'package.json'), 'utf8')).version;
|
|
27
|
+
const BUNDLED_TEMPLATES = join(here, '..', 'templates');
|
|
28
|
+
const PACKAGE = { name: 'create-promptex-js', version: ownVersion };
|
|
29
|
+
const LANGUAGE = 'ts';
|
|
25
30
|
const TEMPLATES = ['project', 'plugin', 'adapter'];
|
|
26
31
|
// 與 promptex init 的單元名正規化同一套規則(crates/promptex-core/src/init.rs)。
|
|
27
32
|
const RESERVED_UNIT_NAMES = ['cache', 'preview', 'content', 'content-index', 'graph', 'main'];
|
|
@@ -36,115 +41,297 @@ export function normalizeName(raw) {
|
|
|
36
41
|
out = out.replace(/^-+|-+$/g, '');
|
|
37
42
|
return out === '' || RESERVED_UNIT_NAMES.includes(out) ? 'main' : out;
|
|
38
43
|
}
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
44
|
+
const BOOLEAN_FLAGS = ['install', 'yes', 'git', 'force', 'dry-run', 'json', 'help'];
|
|
45
|
+
const STRING_FLAGS = ['template', 'name', 'source', 'locale'];
|
|
46
|
+
// ── 輸出 ──
|
|
47
|
+
// 帶 --json 時 stdout 只放一份 JSON,人讀訊息一律不印,錯誤另在 stderr 寫一行(ADR-041)。
|
|
48
|
+
// 語系與 --json 在解析旗標之前先取,解析失敗時的訊息才用得上它們。
|
|
49
|
+
const rawArgv = process.argv.slice(2);
|
|
50
|
+
const jsonRequested = rawArgv.includes('--json');
|
|
51
|
+
function localeFlag(argv) {
|
|
52
|
+
for (let i = 0; i < argv.length; i++) {
|
|
53
|
+
if (argv[i] === '--locale')
|
|
54
|
+
return argv[i + 1];
|
|
55
|
+
if (argv[i].startsWith('--locale='))
|
|
56
|
+
return argv[i].slice('--locale='.length);
|
|
57
|
+
}
|
|
58
|
+
return undefined;
|
|
42
59
|
}
|
|
43
|
-
const
|
|
44
|
-
const args = mri(process.argv.slice(2), {
|
|
45
|
-
boolean: ['install', 'yes', 'git', 'help'],
|
|
46
|
-
string: ['template', 'name', 'ref', 'source', 'locale'],
|
|
47
|
-
alias: { h: 'help' },
|
|
48
|
-
});
|
|
49
|
-
const locale = resolveLocale(args.locale);
|
|
60
|
+
const locale = resolveLocale(localeFlag(rawArgv));
|
|
50
61
|
const separator = t('common.list_separator', locale);
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
62
|
+
/** 細節值裡的陣列在訊息中以語系的清單分隔符接起來。 */
|
|
63
|
+
function messageParams(details) {
|
|
64
|
+
return Object.fromEntries(Object.entries(details).map(([k, v]) => [k, Array.isArray(v) ? v.join(separator) : v]));
|
|
54
65
|
}
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
* 兩者的呼叫點接著就把值餵給只收該型別的下游。
|
|
63
|
-
*/
|
|
64
|
-
async function ask(value, prompt) {
|
|
65
|
-
if (value !== undefined)
|
|
66
|
-
return value;
|
|
67
|
-
if (!interactive)
|
|
68
|
-
return prompt.fallback;
|
|
69
|
-
const answer = await prompt.run();
|
|
70
|
-
if (p.isCancel(answer)) {
|
|
71
|
-
p.cancel(t('scaffold.cancelled', locale));
|
|
72
|
-
process.exit(1);
|
|
66
|
+
function fail(error) {
|
|
67
|
+
const record = errorCode(error.code);
|
|
68
|
+
const message = t(record.messageKey, locale, messageParams(error.details));
|
|
69
|
+
const firstLine = message.split('\n')[0];
|
|
70
|
+
if (jsonRequested) {
|
|
71
|
+
console.log(JSON.stringify({ formatVersion: FORMAT_VERSION, error: { code: error.code, message: firstLine, details: error.details } }));
|
|
72
|
+
console.error(firstLine);
|
|
73
73
|
}
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
run: () => p.text({
|
|
79
|
-
message: t('scaffold.prompt.target_dir', locale),
|
|
80
|
-
placeholder: 'promptex-project',
|
|
81
|
-
defaultValue: 'promptex-project',
|
|
82
|
-
}),
|
|
83
|
-
});
|
|
84
|
-
const targetDir = resolve(String(targetDirRaw));
|
|
85
|
-
const template = await ask(args.template, {
|
|
86
|
-
fallback: 'project',
|
|
87
|
-
run: () => p.select({
|
|
88
|
-
message: t('scaffold.prompt.template', locale),
|
|
89
|
-
options: [
|
|
90
|
-
{ value: 'project', label: t('scaffold.template_option.project', locale) },
|
|
91
|
-
{ value: 'plugin', label: t('scaffold.template_option.plugin', locale) },
|
|
92
|
-
{ value: 'adapter', label: t('scaffold.template_option.adapter', locale) },
|
|
93
|
-
],
|
|
94
|
-
}),
|
|
95
|
-
});
|
|
96
|
-
if (!TEMPLATES.includes(template)) {
|
|
97
|
-
bail(t('scaffold.error.template_unsupported', locale, { template, choices: TEMPLATES.join(separator) }));
|
|
74
|
+
else {
|
|
75
|
+
console.error(message);
|
|
76
|
+
}
|
|
77
|
+
process.exit(record.exitCode);
|
|
98
78
|
}
|
|
99
|
-
|
|
100
|
-
const
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
const
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
const
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
79
|
+
function parseArgs() {
|
|
80
|
+
const args = mri(rawArgv, { boolean: BOOLEAN_FLAGS, string: STRING_FLAGS, alias: { h: 'help' } });
|
|
81
|
+
// mri accepts any flag, so reject the ones this tool does not declare (a removed
|
|
82
|
+
// --ref must fail loudly instead of being ignored). `--no-git` arrives as `git`.
|
|
83
|
+
const knownFlags = new Set(['_', 'h', ...BOOLEAN_FLAGS, ...STRING_FLAGS]);
|
|
84
|
+
for (const key of Object.keys(args)) {
|
|
85
|
+
if (knownFlags.has(key))
|
|
86
|
+
continue;
|
|
87
|
+
// 回報使用者打的原字(`-x`、`--ref`),與另兩個 create 相同;`--ref=main` 只取旗標名。
|
|
88
|
+
const raw = rawArgv.find((a) => a === `-${key}` || a === `--${key}` || a.startsWith(`--${key}=`));
|
|
89
|
+
throw new CreateError('USAGE_UNKNOWN_ARGUMENT', { arg: raw?.split('=')[0] ?? `--${key}` });
|
|
90
|
+
}
|
|
91
|
+
// mri 把缺值的字串旗標填成空字串。
|
|
92
|
+
for (const key of STRING_FLAGS) {
|
|
93
|
+
const value = args[key];
|
|
94
|
+
if (value === '' || typeof value === 'boolean')
|
|
95
|
+
throw new CreateError('USAGE_FLAG_MISSING_VALUE', { flag: `--${key}` });
|
|
116
96
|
}
|
|
97
|
+
if (args._.length > 1)
|
|
98
|
+
throw new CreateError('USAGE_UNKNOWN_ARGUMENT', { arg: String(args._[1]) });
|
|
99
|
+
if (args.json && !args['dry-run'])
|
|
100
|
+
throw new CreateError('USAGE_JSON_REQUIRES_DRY_RUN');
|
|
101
|
+
return args;
|
|
102
|
+
}
|
|
103
|
+
/** 三個 create 共用的說明版面:用法、兩種模式、旗標逐行。 */
|
|
104
|
+
function helpText() {
|
|
105
|
+
const options = [
|
|
106
|
+
['[target-dir]', 'scaffold.help.target_dir'],
|
|
107
|
+
['--template', 'scaffold.help.template'],
|
|
108
|
+
['--name', 'scaffold.help.name'],
|
|
109
|
+
['--source', 'scaffold.help.source'],
|
|
110
|
+
['--git, --no-git', 'scaffold.help.git'],
|
|
111
|
+
['--install', 'scaffold.help.install'],
|
|
112
|
+
['--force', 'scaffold.help.force'],
|
|
113
|
+
['--dry-run', 'scaffold.help.dry_run'],
|
|
114
|
+
['--json', 'scaffold.help.json'],
|
|
115
|
+
['--yes', 'scaffold.help.yes'],
|
|
116
|
+
['--locale', 'scaffold.help.locale'],
|
|
117
|
+
];
|
|
118
|
+
return [
|
|
119
|
+
t('scaffold.usage.js', locale),
|
|
120
|
+
'',
|
|
121
|
+
t('scaffold.usage.modes', locale),
|
|
122
|
+
'',
|
|
123
|
+
t('scaffold.usage.options', locale),
|
|
124
|
+
...options.map(([flag, key]) => ` ${flag.padEnd(16)}${t(key, locale)}`),
|
|
125
|
+
].join('\n');
|
|
117
126
|
}
|
|
118
|
-
const
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
127
|
+
const REASON_KEYS = {
|
|
128
|
+
exists: 'scaffold.skip.exists',
|
|
129
|
+
'manifest-kept': 'scaffold.skip.manifest_kept',
|
|
130
|
+
'example-dir-exists': 'scaffold.skip.example_dir_exists',
|
|
131
|
+
'init-mode': 'scaffold.skip.init_mode',
|
|
132
|
+
'marker-present': 'scaffold.skip.marker_present',
|
|
133
|
+
symlink: 'scaffold.skip.symlink',
|
|
134
|
+
blocked: 'scaffold.skip.blocked',
|
|
135
|
+
};
|
|
136
|
+
/** 人讀的動作清單:依建立、覆寫、追加、跳過分組,組內依路徑排序。 */
|
|
137
|
+
function actionLines(actions) {
|
|
138
|
+
const summary = summarize(actions);
|
|
139
|
+
const rows = [
|
|
140
|
+
...summary.create.map((path) => [t('scaffold.action.create', locale), path]),
|
|
141
|
+
...summary.overwrite.map((path) => [t('scaffold.action.overwrite', locale), path]),
|
|
142
|
+
...summary.append.map((path) => [t('scaffold.action.append', locale), path]),
|
|
143
|
+
...summary.skip.map(({ path, reason }) => [
|
|
144
|
+
t('scaffold.action.skip', locale),
|
|
145
|
+
`${path} (${t(REASON_KEYS[reason], locale)})`,
|
|
146
|
+
]),
|
|
147
|
+
];
|
|
148
|
+
const width = Math.max(0, ...rows.map(([label]) => label.length));
|
|
149
|
+
return rows.map(([label, text]) => ` ${label.padEnd(width)} ${text}`);
|
|
150
|
+
}
|
|
151
|
+
/** `--dry-run --json` 的成功輸出。欄位與另兩個 create 逐欄相同,只有 package 與 language 依語言而異。 */
|
|
152
|
+
function report(opts) {
|
|
153
|
+
return {
|
|
154
|
+
formatVersion: FORMAT_VERSION,
|
|
155
|
+
package: PACKAGE,
|
|
156
|
+
language: LANGUAGE,
|
|
157
|
+
mode: opts.mode,
|
|
158
|
+
alreadyInitialized: opts.existingConfig !== null,
|
|
159
|
+
existingConfig: opts.existingConfig,
|
|
160
|
+
target: opts.targetDir,
|
|
161
|
+
template: opts.template,
|
|
162
|
+
name: opts.name,
|
|
163
|
+
files: summarize(opts.actions),
|
|
164
|
+
git: opts.git,
|
|
165
|
+
};
|
|
128
166
|
}
|
|
129
|
-
|
|
130
|
-
const
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
167
|
+
async function main() {
|
|
168
|
+
const args = parseArgs();
|
|
169
|
+
if (args.help) {
|
|
170
|
+
console.log(helpText());
|
|
171
|
+
process.exit(0);
|
|
172
|
+
}
|
|
173
|
+
const interactive = !isNonInteractive(Boolean(process.stdin.isTTY), Boolean(args.yes) || Boolean(args.json), process.env);
|
|
174
|
+
const say = (line) => {
|
|
175
|
+
if (jsonRequested)
|
|
176
|
+
return;
|
|
177
|
+
if (interactive)
|
|
178
|
+
p.log.message(line);
|
|
179
|
+
else
|
|
180
|
+
console.log(line);
|
|
181
|
+
};
|
|
182
|
+
// 目前沒有必填輸入(m008 d007:非互動時全部有預設值)。新增必填輸入時把它的
|
|
183
|
+
// hasDefault 設成 false,非互動執行就會以結束碼 2 一次列出所有缺的旗標。
|
|
184
|
+
const missing = missingInputs([
|
|
185
|
+
{ flag: '[target-dir]', provided: args._[0] !== undefined, hasDefault: true },
|
|
186
|
+
{ flag: '--template', provided: args.template !== undefined, hasDefault: true },
|
|
187
|
+
{ flag: '--name', provided: args.name !== undefined, hasDefault: true },
|
|
188
|
+
], interactive);
|
|
189
|
+
if (missing.length > 0)
|
|
190
|
+
throw new CreateError('USAGE_MISSING_INPUTS', { flags: missing });
|
|
191
|
+
if (interactive)
|
|
192
|
+
p.intro(`create-promptex-js v${ownVersion}`);
|
|
193
|
+
/** 缺什麼問什麼:旗標已給就用它,非互動取預設,其餘才問。 */
|
|
194
|
+
async function ask(value, prompt) {
|
|
195
|
+
if (value !== undefined)
|
|
196
|
+
return value;
|
|
197
|
+
if (!interactive)
|
|
198
|
+
return prompt.fallback;
|
|
199
|
+
const answer = await prompt.run();
|
|
200
|
+
if (p.isCancel(answer))
|
|
201
|
+
throw new CreateError('RUN_CANCELLED');
|
|
202
|
+
return answer;
|
|
203
|
+
}
|
|
204
|
+
// ── 目標與模式:輸出的第一行說明所選的模式。 ──
|
|
205
|
+
const targetRaw = await ask(args._[0] === undefined ? undefined : String(args._[0]), {
|
|
206
|
+
fallback: 'promptex-project',
|
|
207
|
+
run: () => p.text({
|
|
208
|
+
message: t('scaffold.prompt.target_dir', locale),
|
|
209
|
+
placeholder: 'promptex-project',
|
|
210
|
+
defaultValue: 'promptex-project',
|
|
211
|
+
}),
|
|
212
|
+
});
|
|
213
|
+
const targetDir = resolve(String(targetRaw));
|
|
214
|
+
const state = inspectTarget(targetDir);
|
|
215
|
+
if (state.mode === 'create') {
|
|
216
|
+
say(t('scaffold.mode.create', locale, { path: targetDir }));
|
|
217
|
+
}
|
|
218
|
+
else {
|
|
219
|
+
const shown = `${state.unlisted.slice(0, 10).join(separator)}${state.unlisted.length > 10 ? '…' : ''}`;
|
|
220
|
+
say(t('scaffold.mode.init', locale, { path: targetDir, entries: shown }));
|
|
221
|
+
}
|
|
222
|
+
// 初始化模式只支援 project(m008 d007),不問模板。
|
|
223
|
+
const template = state.mode === 'init'
|
|
224
|
+
? (args.template ?? 'project')
|
|
225
|
+
: await ask(args.template, {
|
|
226
|
+
fallback: 'project',
|
|
227
|
+
run: () => p.select({
|
|
228
|
+
message: t('scaffold.prompt.template', locale),
|
|
229
|
+
options: [
|
|
230
|
+
{ value: 'project', label: t('scaffold.template_option.project', locale) },
|
|
231
|
+
{ value: 'plugin', label: t('scaffold.template_option.plugin', locale) },
|
|
232
|
+
{ value: 'adapter', label: t('scaffold.template_option.adapter', locale) },
|
|
233
|
+
],
|
|
234
|
+
}),
|
|
135
235
|
});
|
|
236
|
+
if (!TEMPLATES.includes(template)) {
|
|
237
|
+
throw new CreateError('USAGE_TEMPLATE_UNSUPPORTED', { template, choices: TEMPLATES });
|
|
238
|
+
}
|
|
239
|
+
if (state.mode === 'init' && template !== 'project') {
|
|
240
|
+
throw new CreateError('USAGE_TEMPLATE_NEEDS_EMPTY_DIR', { template, path: targetDir, entries: state.unlisted });
|
|
136
241
|
}
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
242
|
+
const defaultName = normalizeName(basename(targetDir));
|
|
243
|
+
const existingConfig = state.mode === 'init' ? findConfig(targetDir) : null;
|
|
244
|
+
if (existingConfig !== null) {
|
|
245
|
+
const git = { init: false, reason: 'already-initialized' };
|
|
246
|
+
if (jsonRequested) {
|
|
247
|
+
console.log(JSON.stringify(report({ mode: state.mode, existingConfig, targetDir, template, name: defaultName, actions: [], git })));
|
|
248
|
+
}
|
|
249
|
+
else {
|
|
250
|
+
say(t('scaffold.already_initialized', locale, { config: existingConfig }));
|
|
251
|
+
}
|
|
252
|
+
process.exit(0);
|
|
140
253
|
}
|
|
141
|
-
|
|
142
|
-
|
|
254
|
+
const name = normalizeName(String(await ask(args.name, {
|
|
255
|
+
fallback: defaultName,
|
|
256
|
+
run: () => p.text({ message: t('scaffold.prompt.name', locale), defaultValue: defaultName, placeholder: defaultName }),
|
|
257
|
+
})));
|
|
258
|
+
// ── git:在既有工作樹內就不 init,也不問。 ──
|
|
259
|
+
const insideWorktree = isInsideGitWorktree(targetDir);
|
|
260
|
+
const gitAvailable = commandAvailable('git');
|
|
261
|
+
if (args.git === true && !gitAvailable)
|
|
262
|
+
throw new CreateError('RUN_GIT_UNAVAILABLE');
|
|
263
|
+
const wantGit = args.git !== undefined
|
|
264
|
+
? args.git
|
|
265
|
+
: insideWorktree
|
|
266
|
+
? false
|
|
267
|
+
: await ask(undefined, {
|
|
268
|
+
fallback: true,
|
|
269
|
+
run: () => p.confirm({ message: t('scaffold.prompt.git_init', locale), initialValue: true }),
|
|
270
|
+
});
|
|
271
|
+
const git = insideWorktree
|
|
272
|
+
? { init: false, reason: 'inside-worktree' }
|
|
273
|
+
: !wantGit
|
|
274
|
+
? { init: false, reason: 'disabled' }
|
|
275
|
+
: !gitAvailable
|
|
276
|
+
? { init: false, reason: 'unavailable' }
|
|
277
|
+
: { init: true, reason: null };
|
|
278
|
+
// ── 模板取得:預設用內附模板;--source 讀本地模板目錄。 ──
|
|
279
|
+
const templatesRoot = args.source ? resolve(args.source) : BUNDLED_TEMPLATES;
|
|
280
|
+
const sourceLabel = args.source
|
|
281
|
+
? t('scaffold.source.local', locale, { path: templatesRoot })
|
|
282
|
+
: t('scaffold.source.bundled', locale, { version: ownVersion });
|
|
283
|
+
const starterDir = join(templatesRoot, `${template}-starter`);
|
|
284
|
+
if (!existsSync(starterDir))
|
|
285
|
+
throw new CreateError('RUN_STARTER_MISSING', { template, path: starterDir });
|
|
286
|
+
const files = loadTemplateFiles(starterDir, name);
|
|
287
|
+
// ── 計畫:逐檔決定建立、覆寫、追加或跳過。--dry-run 到此為止。 ──
|
|
288
|
+
const actions = planFiles(targetDir, files, state.mode, template, Boolean(args.force));
|
|
289
|
+
if (args['dry-run']) {
|
|
290
|
+
if (jsonRequested) {
|
|
291
|
+
console.log(JSON.stringify(report({ mode: state.mode, existingConfig: null, targetDir, template, name, actions, git })));
|
|
292
|
+
}
|
|
293
|
+
else {
|
|
294
|
+
say(t('scaffold.dry_run.header', locale, { path: targetDir, source: sourceLabel }));
|
|
295
|
+
for (const line of actionLines(actions))
|
|
296
|
+
say(line);
|
|
297
|
+
if (git.reason === 'inside-worktree')
|
|
298
|
+
say(t('scaffold.git.inside_worktree', locale));
|
|
299
|
+
if (git.init)
|
|
300
|
+
say(t('scaffold.git.will_init', locale));
|
|
301
|
+
}
|
|
302
|
+
process.exit(0);
|
|
303
|
+
}
|
|
304
|
+
applyPlan(targetDir, actions);
|
|
305
|
+
// ── 後處理:git init(可選)、--install 裝依賴。兩種模式都不自動 commit。 ──
|
|
306
|
+
if (git.init)
|
|
307
|
+
spawnSync('git', ['init', '--quiet'], { cwd: targetDir, stdio: 'inherit' });
|
|
308
|
+
const pm = detectPackageManager(targetDir, () => commandAvailable('pnpm'));
|
|
309
|
+
if (args.install) {
|
|
310
|
+
const command = PM_COMMANDS[pm].install;
|
|
311
|
+
if (!commandAvailable(pm))
|
|
312
|
+
throw new CreateError('RUN_INSTALL_TOOL_MISSING', { command });
|
|
313
|
+
const [bin, ...rest] = command.split(' ');
|
|
314
|
+
const r = spawnSync(bin, rest, { cwd: targetDir, stdio: 'inherit', shell: process.platform === 'win32' });
|
|
315
|
+
if (r.status !== 0)
|
|
316
|
+
throw new CreateError('RUN_INSTALL_FAILED', { command });
|
|
317
|
+
}
|
|
318
|
+
// ── 稽核輸出:做了什麼、來源為何、下一步。 ──
|
|
319
|
+
const summary = summarize(actions);
|
|
320
|
+
const count = summary.create.length + summary.overwrite.length + summary.append.length;
|
|
321
|
+
say(t('scaffold.done.written', locale, { count, path: targetDir, source: sourceLabel }));
|
|
322
|
+
for (const line of actionLines(actions))
|
|
323
|
+
say(line);
|
|
324
|
+
if (git.reason === 'inside-worktree')
|
|
325
|
+
say(t('scaffold.git.inside_worktree', locale));
|
|
326
|
+
const manifestKept = summary.skip.some((s) => s.path === 'package.json');
|
|
327
|
+
const steps = nextSteps({ template, cd: cdPath(targetDir, process.cwd()), manifestKept, installed: Boolean(args.install), pm });
|
|
328
|
+
const nextBlock = `${t('scaffold.done.next_steps', locale)}\n${steps.map((s, i) => ` ${i + 1}. ${s}`).join('\n')}`;
|
|
329
|
+
if (interactive)
|
|
330
|
+
p.outro(nextBlock);
|
|
331
|
+
else
|
|
332
|
+
console.log(nextBlock);
|
|
143
333
|
}
|
|
144
|
-
|
|
145
|
-
if (!existsSync(starterDir))
|
|
146
|
-
bail(t('scaffold.error.starter_missing', locale, { template, path: starterDir }));
|
|
147
|
-
// ── 具現化:複製、佔位替換、_gitignore 改名。 ──
|
|
334
|
+
// ── 具現化:佔位替換、_gitignore 改名。 ──
|
|
148
335
|
/**
|
|
149
336
|
* 把一則訊息文字變成可直接嵌進單引號字串字面量的形態。
|
|
150
337
|
*
|
|
@@ -159,71 +346,64 @@ if (!existsSync(starterDir))
|
|
|
159
346
|
function escapeForLiteral(text) {
|
|
160
347
|
return text.replaceAll('\\', '\\\\').replaceAll('\n', '\\n').replaceAll("'", "\\'");
|
|
161
348
|
}
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
const
|
|
193
|
-
|
|
194
|
-
const
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
349
|
+
function loadTemplateFiles(starterDir, name) {
|
|
350
|
+
const tokens = {
|
|
351
|
+
'{{name}}': name,
|
|
352
|
+
'{{snakeName}}': name.replaceAll('-', '_'),
|
|
353
|
+
'{{unitName}}': name,
|
|
354
|
+
'{{promptexVersion}}': ownVersion,
|
|
355
|
+
// 模板的範例資源文字(ADR-030「一次性的起手內容」、AC-523):模板檔以佔位符留位,
|
|
356
|
+
// 寫檔當下代入本次語系的那一份。實作指引註解不在此列,它們在模板裡就是英文。
|
|
357
|
+
'{{adapterDemoRuleContent}}': escapeForLiteral(t('template.adapter_demo.rule_content', locale)),
|
|
358
|
+
'{{adapterDemoSkillContent}}': escapeForLiteral(t('template.adapter_demo.skill_content', locale)),
|
|
359
|
+
'{{adapterDemoSkillDescription}}': escapeForLiteral(t('template.adapter_demo.skill_description', locale)),
|
|
360
|
+
'{{adapterDemoSkillWhenToUse}}': escapeForLiteral(t('template.adapter_demo.skill_when_to_use', locale)),
|
|
361
|
+
'{{demoRuleName}}': escapeForLiteral(t('template.demo.rule_name', locale)),
|
|
362
|
+
'{{demoSkillName}}': escapeForLiteral(t('template.demo.skill_name', locale)),
|
|
363
|
+
'{{exampleRuleIntro}}': escapeForLiteral(t('template.example_rule.intro', locale)),
|
|
364
|
+
'{{exampleRuleName}}': escapeForLiteral(t('template.example_rule.name', locale)),
|
|
365
|
+
'{{exampleRuleOtherStates}}': escapeForLiteral(t('template.example_rule.other_states', locale)),
|
|
366
|
+
'{{exampleRuleScoped}}': escapeForLiteral(t('template.example_rule.scoped', locale)),
|
|
367
|
+
'{{exampleSkillDescription}}': escapeForLiteral(t('template.example_skill.description', locale)),
|
|
368
|
+
'{{exampleSkillIntro}}': escapeForLiteral(t('template.example_skill.intro', locale)),
|
|
369
|
+
'{{exampleSkillInvoked}}': escapeForLiteral(t('template.example_skill.invoked', locale)),
|
|
370
|
+
'{{exampleSkillInvokedRs}}': escapeForLiteral(t('template.example_skill.invoked_rs', locale)),
|
|
371
|
+
'{{exampleSkillName}}': escapeForLiteral(t('template.example_skill.name', locale)),
|
|
372
|
+
'{{exampleSkillWhenToUse}}': escapeForLiteral(t('template.example_skill.when_to_use', locale)),
|
|
373
|
+
'{{pluginDemoRuleContent}}': escapeForLiteral(t('template.plugin_demo.rule_content', locale)),
|
|
374
|
+
'{{pluginDemoSkillContent}}': escapeForLiteral(t('template.plugin_demo.skill_content', locale)),
|
|
375
|
+
'{{pluginDemoSkillDescription}}': escapeForLiteral(t('template.plugin_demo.skill_description', locale)),
|
|
376
|
+
'{{pluginDemoSkillWhenToUse}}': escapeForLiteral(t('template.plugin_demo.skill_when_to_use', locale)),
|
|
377
|
+
};
|
|
378
|
+
const substitute = (text) => Object.entries(tokens).reduce((acc, [token, value]) => acc.replaceAll(token, value), text);
|
|
379
|
+
const walk = (dir, rel = '') => {
|
|
380
|
+
const out = [];
|
|
381
|
+
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
|
382
|
+
const relPath = rel ? `${rel}/${entry.name}` : entry.name;
|
|
383
|
+
if (entry.isDirectory())
|
|
384
|
+
out.push(...walk(join(dir, entry.name), relPath));
|
|
385
|
+
else
|
|
386
|
+
out.push(relPath);
|
|
387
|
+
}
|
|
388
|
+
return out;
|
|
389
|
+
};
|
|
390
|
+
return walk(starterDir).map((rel) => {
|
|
391
|
+
const dest = substitute(rel);
|
|
392
|
+
let content;
|
|
393
|
+
try {
|
|
394
|
+
content = readFileSync(join(starterDir, rel), 'utf8');
|
|
395
|
+
}
|
|
396
|
+
catch (e) {
|
|
397
|
+
throw new CreateError('RUN_READ_FAILED', { path: join(starterDir, rel), detail: e instanceof Error ? e.message : String(e) });
|
|
398
|
+
}
|
|
399
|
+
return { rel: dest === '_gitignore' ? '.gitignore' : dest, content: substitute(content) };
|
|
400
|
+
});
|
|
201
401
|
}
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
let destRel = substitute(rel);
|
|
205
|
-
if (destRel === '_gitignore')
|
|
206
|
-
destRel = '.gitignore';
|
|
207
|
-
const destPath = join(targetDir, destRel);
|
|
208
|
-
mkdirSync(dirname(destPath), { recursive: true });
|
|
209
|
-
writeFileSync(destPath, substitute(readFileSync(join(starterDir, rel), 'utf8')));
|
|
210
|
-
written.push(destRel);
|
|
402
|
+
try {
|
|
403
|
+
await main();
|
|
211
404
|
}
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
spawnSync('git', ['init', '--quiet'], { cwd: targetDir, stdio: 'inherit' });
|
|
217
|
-
if (args.install) {
|
|
218
|
-
const r = spawnSync('npm', ['install'], { cwd: targetDir, stdio: 'inherit', shell: process.platform === 'win32' });
|
|
219
|
-
if (r.status !== 0)
|
|
220
|
-
bail(t('scaffold.error.install_failed.js', locale));
|
|
405
|
+
catch (e) {
|
|
406
|
+
if (e instanceof CreateError)
|
|
407
|
+
fail(e);
|
|
408
|
+
throw e;
|
|
221
409
|
}
|
|
222
|
-
// ── 稽核輸出:寫了什麼、來源為何、下一步。 ──
|
|
223
|
-
p.log.success(t('scaffold.done.written', locale, { count: written.length, path: targetDir, source: sourceLabel }));
|
|
224
|
-
for (const rel of written)
|
|
225
|
-
p.log.message(` ${rel}`);
|
|
226
|
-
const nextSteps = template === 'project'
|
|
227
|
-
? [`cd ${targetDirRaw}`, ...(args.install ? [] : ['npm install']), 'npm run install:prompts']
|
|
228
|
-
: [`cd ${targetDirRaw}`, ...(args.install ? [] : ['npm install']), 'npm run build', 'cd example && npx promptex build --install'];
|
|
229
|
-
p.outro(`${t('scaffold.done.next_steps', locale)}\n${nextSteps.map((s, i) => ` ${i + 1}. ${s}`).join('\n')}`);
|