create-promptex-js 0.0.1-alpha.1 → 1.0.0-alpha.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,55 +1,40 @@
1
- # create-promptex-js(alpha 原型)
1
+ # create-promptex-js
2
2
 
3
- promptex 專案鷹架的**最小原型**。產出一個可立即建置的專案骨架:宣告一份
4
- skill,建置後得到 Claude Code 的 `SKILL.md`。
3
+ Scaffold a [promptex](https://github.com/promptex-ai/promptex) TypeScript
4
+ project, plugin or adapter from the
5
+ [promptex-templates](https://github.com/promptex-ai/promptex-templates)
6
+ starters.
5
7
 
6
- ## 這不是什麼
7
-
8
- 本套件是早期 alpha。骨架只有四個檔案、一份範例宣告,沒有多單元配置、沒有
9
- 多平台選擇、沒有外掛與轉接器。正式版把骨架收在核心(`promptex init`),
10
- 本原型自帶一份副本,正式版發布時會被整份取代。
11
-
12
- 介面與骨架內容在正式版之前都會變,不保證任何相容性。
13
-
14
- ## 用法
15
-
16
- 不需要事先安裝,`npm create` 會取得後直接執行。需要 Node.js 22.14 以上。
17
-
18
- ```bash
19
- npm create promptex-js my-prompts
20
- ```
21
-
22
- 產出:
23
-
24
- ```text
25
- my-prompts/
26
- ├── .gitignore
27
- ├── README.md
28
- ├── package.json
29
- └── prompts/skill.ts
8
+ ```console
9
+ npm create promptex-js
30
10
  ```
31
11
 
32
- (上為 `--lang ts` 的形態;`--lang python` 產出 `pyproject.toml` 與
33
- `prompts/skill.py`,`--lang rust` 產出 `Cargo.toml` 與 `src/main.rs`。)
34
-
35
- ### 選項
12
+ Interactive by default; every answer has a flag for non-interactive use:
36
13
 
37
- ```text
38
- npm create promptex-js <目錄> [--lang ts|python|rust] [--yes]
39
-
40
- --lang <語言> 骨架語言,預設 ts
41
- --yes 目標目錄已有同名檔案時仍覆寫
14
+ ```console
15
+ npm create promptex-js my-prompts -- --template project --name my-prompts --no-git --yes
42
16
  ```
43
17
 
44
- 非互動:全部輸入取自位置參數與旗標,與正式版 `promptex init` 的介面一致。
45
- 目標目錄已有同名檔案且未帶 `--yes` 時**一個檔案都不寫**,不留半套骨架。
46
-
47
- ## 三生態等價
48
-
49
- `create-promptex-js`、`create-promptex-py`、`create-promptex-rs` 對同一組
50
- 參數產出逐字相同的檔案樹。正式版靠骨架住核心由結構保證,本原型是三份獨立
51
- 實作,等價由產物比對承擔。
52
-
53
- ## 授權
54
-
55
- MIT OR Apache-2.0,見 `LICENSE-MIT` 與 `LICENSE-APACHE`。
18
+ | Flag | Meaning |
19
+ | :--- | :--- |
20
+ | `--template <t>` | `project`(消費專案)、`plugin`、`adapter` |
21
+ | `--name <n>` | kebab-case 名稱;預設取目標目錄名正規化 |
22
+ | `--ref <r>` | 抓 promptex-templates 的指定 branch/tag;預設 `v<本套件版本>` |
23
+ | `--source <p>` | 改用本地 promptex-templates checkout(離線與開發用) |
24
+ | `--git` / `--no-git` | 是否 `git init`;互動預設是 |
25
+ | `--install` | 鷹架後直接 `npm install`;預設只印下一步 |
26
+ | `--yes` | 跳過互動,全部取預設或旗標值 |
27
+ | `--locale <l>` | 訊息與範例資源的語系:`en`(預設)或 `zh-Hant` |
28
+
29
+ 語系未以 `--locale` 指定時,依序讀 `PROMPTEX_LOCALE`、`LANGUAGE`、`LC_ALL`、
30
+ `LC_MESSAGES`、`LANG`,都沒有就用英文;`LC_ALL=C` 意為關掉在地化。`zh-TW`、
31
+ `zh-HK`、`zh-MO` 與 `zh-Hant-*` 都對到 `zh-Hant`。它同時決定鋪出來的範例資源
32
+ 用哪個語系寫([ADR-030](https://github.com/promptex-ai/ssot)):那份內容從產出
33
+ 當下起就歸你,框架不再改寫它。plugin 與 adapter 骨架裡教你怎麼填的註解維持英文。
34
+
35
+ 目標目錄非空時列出既有項目、零寫入退出。模板不內嵌:單一來源住
36
+ promptex-templates 倉庫,鷹架時以 git 取得對應版本 tag(需要 git;
37
+ `{{promptexVersion}}` 於鷹架當下以本套件版本填入,三套件與 SDK 同版發布)。
38
+
39
+ `promptex init` 與本工具的分工:init 在既有專案就地補上 promptex(非互動、
40
+ 極簡骨架);本工具從零開一個 repo(互動、三種模板、git init)。
@@ -1,63 +1,7 @@
1
1
  #!/usr/bin/env node
2
- // create-promptex-js 的進入點。`npm create promptex-js <目錄>` 解析到本套件。
2
+ // create-promptex-js 的轉發殼。
3
3
  //
4
- // 非互動:全部輸入取自位置參數與旗標,與正式版 `promptex init` 的介面一致。
5
- // 骨架決策不在此處,見 ../dist/index.js 的 skeleton。
6
-
7
- import { LANGS, scaffold } from '../dist/index.js'
8
-
9
- const USAGE = `create-promptex-js(alpha 原型)
10
-
11
- 用法:
12
- npm create promptex-js <目錄> -- [--lang ts|python|rust] [--yes]
13
-
14
- 選項:
15
- --lang <語言> 骨架語言,預設 ts
16
- --yes 目標目錄已有同名檔案時仍覆寫
17
- --help 顯示本說明
18
- `
19
-
20
- function parseArgs(argv) {
21
- const args = { dir: null, lang: 'ts', assumeYes: false }
22
- for (let i = 0; i < argv.length; i += 1) {
23
- const token = argv[i]
24
- if (token === '--yes' || token === '-y') args.assumeYes = true
25
- else if (token === '--lang') {
26
- const value = argv[i + 1]
27
- if (value === undefined) throw new Error('--lang 後面缺少值')
28
- if (!LANGS.includes(value)) throw new Error(`不支援的語言:${value}(可用:${LANGS.join('、')})`)
29
- args.lang = value
30
- i += 1
31
- } else if (token.startsWith('-')) throw new Error(`未知的選項:${token}`)
32
- else if (args.dir === null) args.dir = token
33
- else throw new Error(`多餘的位置參數:${token}`)
34
- }
35
- if (args.dir === null) throw new Error('缺少目標目錄。用法:npm create promptex-js <目錄>')
36
- return args
37
- }
38
-
39
- async function main() {
40
- const argv = process.argv.slice(2)
41
- if (argv.includes('--help') || argv.includes('-h')) {
42
- process.stdout.write(USAGE)
43
- return
44
- }
45
-
46
- const args = parseArgs(argv)
47
- const outcome = await scaffold(args.dir, args.lang, args.assumeYes)
48
-
49
- if (outcome.aborted) {
50
- process.stderr.write(`目標目錄已有同名檔案,未寫入任何檔案:\n`)
51
- for (const path of outcome.conflicts) process.stderr.write(` ${path}\n`)
52
- process.stderr.write(`確定要覆寫請加 --yes。\n`)
53
- process.exitCode = 1
54
- return
55
- }
56
-
57
- for (const path of outcome.written) process.stdout.write(`${path}\n`)
58
- }
59
-
60
- await main().catch((error) => {
61
- process.stderr.write(`${error.message}\n`)
62
- process.exitCode = 1
63
- })
4
+ // 邏輯住 `src/index.ts`(發布物為 `dist/index.js`),本檔只負責被 npm 當成
5
+ // bin 執行:鷹架的流程是頂層的一段腳本而非可呼叫的函式(它從 argv 讀到底、
6
+ // 逐步問完就結束),因此這裡 import 即執行,不另外呼叫誰。
7
+ import '../dist/index.js'
@@ -0,0 +1,6 @@
1
+ export declare function defaultRef(version: string): string;
2
+ /** `showCloneHint`:clone 失敗時是否附上「tag 可能尚未建立」提示,只在 GA 預設 ref 時為真。 */
3
+ export declare function resolveRef(explicit: string | undefined, version: string): {
4
+ ref: string;
5
+ showCloneHint: boolean;
6
+ };
@@ -0,0 +1,14 @@
1
+ // 預設 templates ref 的判定(決策 d006)。三個 create-* 鷹架規則一致:
2
+ // GA 版號(X.Y.Z)抓對應 tag `v<版本>`;帶預發布後綴(-alpha.N、-beta.N、-rc.N 等)
3
+ // 的版本,tag 不一定存在,預設抓 `main`;使用者給 --ref 一律照用。
4
+ const GA_VERSION = /^\d+\.\d+\.\d+$/;
5
+ export function defaultRef(version) {
6
+ return GA_VERSION.test(version) ? `v${version}` : 'main';
7
+ }
8
+ /** `showCloneHint`:clone 失敗時是否附上「tag 可能尚未建立」提示,只在 GA 預設 ref 時為真。 */
9
+ export function resolveRef(explicit, version) {
10
+ if (explicit)
11
+ return { ref: explicit, showCloneHint: false };
12
+ const ref = defaultRef(version);
13
+ return { ref, showCloneHint: ref !== 'main' };
14
+ }
package/dist/index.d.ts CHANGED
@@ -1,22 +1,2 @@
1
- /** 骨架支援的語言。與 `promptex init --lang` 的取值一致。 */
2
- export type Lang = 'ts' | 'python' | 'rust';
3
- export declare const LANGS: readonly Lang[];
4
- /**
5
- * 一份骨架的全部檔案,鍵為相對路徑。
6
- *
7
- * 純計算、不碰檔案系統:讓「骨架長什麼樣」可被直接比對,不必先寫到磁碟。
8
- */
9
- export declare function skeleton(name: string, lang: Lang): Record<string, string>;
10
- /** 一次初始化的處置結果。`aborted` 為真時 `written` 必為空。 */
11
- export interface Outcome {
12
- aborted: boolean;
13
- conflicts: string[];
14
- written: string[];
15
- }
16
- /**
17
- * 在目標目錄產出骨架。
18
- *
19
- * 全有全無:有同名檔案且未帶 assumeYes 時一個檔案都不寫,避免留下半套骨架。
20
- * 這與正式版 `promptex init` 的衝突處置一致。
21
- */
22
- export declare function scaffold(targetDir: string, lang: Lang, assumeYes: boolean): Promise<Outcome>;
1
+ #!/usr/bin/env node
2
+ export declare function normalizeName(raw: string): string;
package/dist/index.js CHANGED
@@ -1,177 +1,229 @@
1
- // create-promptex-js(alpha 原型):產出可立即建置的 promptex 專案骨架。
1
+ #!/usr/bin/env node
2
+ // 鷹架 CLI:從 promptex-templates 倉庫開出 TypeScript 的 project/plugin/
3
+ // adapter 專案。六階段:環境檢查 → 互動收集 → 模板取得 → 具現化 → 後處理 →
4
+ // 稽核輸出;非互動時(--yes 或無 TTY)全部取預設或旗標值。
2
5
  //
3
- // 骨架內容以資料承載(見 skeleton),三個生態的 create-promptex-* 產出逐字
4
- // 相同的檔案樹。正式版把骨架收在核心(promptex_core::init),本原型的三份
5
- // 實作各持一份副本,等價由 verify-promptex-poc.sh 的產物比對承擔。
6
- import { mkdir, readFile, writeFile } from 'node:fs/promises';
7
- import { dirname, join, resolve, basename } from 'node:path';
8
- export const LANGS = ['ts', 'python', 'rust'];
9
- /** 專案名稱的合法形態。作為套件名與 crate 名,字元集取三個生態的交集。 */
10
- const NAME_PATTERN = /^[a-zA-Z0-9][a-zA-Z0-9._-]*$/;
11
- const SKILL_ID = 'commit-helper';
12
- const SKILL_NAME = 'Commit 訊息協助';
13
- const SKILL_WHEN = '當使用者要求撰寫 commit 訊息時使用';
14
- const SKILL_P1 = '依 Conventional Commits 撰寫:type(scope): subject。';
15
- const SKILL_P2 = 'subject 用繁體中文、50 字內,不以句號結尾;body 說明變更動機而非變更內容。';
16
- /** 各語言的建置指令,投影進骨架 README 的建置段。 */
17
- const BUILD_COMMANDS = {
18
- ts: ['npm install', 'npm run build'],
19
- python: ['pip install --pre promptex-py', 'promptex build'],
20
- rust: ['cargo run'],
21
- };
22
- const readme = (name, lang) => [
23
- '# ' + name,
24
- '',
25
- '以 promptex 管理提示詞資源的專案骨架。',
26
- '',
27
- '## 建置',
28
- '',
29
- '```bash',
30
- ...BUILD_COMMANDS[lang],
31
- '```',
32
- '',
33
- '產出 `.claude/skills/' + SKILL_ID + '/SKILL.md`。',
34
- '',
35
- '## 下一步',
36
- '',
37
- '新增宣告,每份宣告投影成一份產物。',
38
- '',
39
- ].join('\n');
40
- const tsSkeleton = (name) => ({
41
- 'package.json': JSON.stringify({
42
- name,
43
- private: true,
44
- type: 'module',
45
- scripts: { build: 'promptex build' },
46
- dependencies: { 'promptex-js': '0.0.1-alpha.1' },
47
- }, null, 2) + '\n',
48
- 'prompts/skill.ts': [
49
- "import { defineSkill } from 'promptex-js'",
50
- '',
51
- 'export const commitHelper = defineSkill({',
52
- " id: '" + SKILL_ID + "',",
53
- " name: '" + SKILL_NAME + "',",
54
- " config: { whenToUse: '" + SKILL_WHEN + "' },",
55
- ' content: [',
56
- " '" + SKILL_P1 + "',",
57
- " '" + SKILL_P2 + "',",
58
- ' ],',
59
- '})',
60
- '',
61
- ].join('\n'),
62
- '.gitignore': 'node_modules/\n',
63
- 'README.md': readme(name, 'ts'),
64
- });
65
- const pythonSkeleton = (name) => ({
66
- 'pyproject.toml': [
67
- '[project]',
68
- 'name = "' + name + '"',
69
- 'version = "0.0.0"',
70
- 'requires-python = ">=3.10"',
71
- 'dependencies = ["promptex-py==0.0.1a1"]',
72
- '',
73
- ].join('\n'),
74
- 'prompts/skill.py': [
75
- 'from promptex import config, define_skill',
76
- '',
77
- 'commit_helper = define_skill(',
78
- ' "' + SKILL_ID + '",',
79
- ' "' + SKILL_NAME + '",',
80
- ' config=config(when_to_use="' + SKILL_WHEN + '"),',
81
- ' content=[',
82
- ' "' + SKILL_P1 + '",',
83
- ' "' + SKILL_P2 + '",',
84
- ' ],',
85
- ')',
86
- '',
87
- ].join('\n'),
88
- '.gitignore': '__pycache__/\n',
89
- 'README.md': readme(name, 'python'),
90
- });
91
- const rustSkeleton = (name) => ({
92
- 'Cargo.toml': [
93
- '[package]',
94
- 'name = "' + name + '"',
95
- 'version = "0.0.0"',
96
- 'edition = "2021"',
97
- '',
98
- '[dependencies]',
99
- 'promptex-rs = "0.0.1-alpha.1"',
100
- '',
101
- ].join('\n'),
102
- 'src/main.rs': [
103
- 'use promptex_rs::{build, define_skill, CapabilityConfig, SkillDef};',
104
- '',
105
- 'fn main() -> Result<(), Box<dyn std::error::Error>> {',
106
- ' define_skill(SkillDef {',
107
- ' id: "' + SKILL_ID + '".into(),',
108
- ' name: "' + SKILL_NAME + '".into(),',
109
- ' config: CapabilityConfig::new().when_to_use("' + SKILL_WHEN + '"),',
110
- ' content: vec![',
111
- ' "' + SKILL_P1 + '".into(),',
112
- ' "' + SKILL_P2 + '".into(),',
113
- ' ],',
114
- ' });',
115
- '',
116
- ' for path in build(".")? {',
117
- ' println!("{}", path.display());',
118
- ' }',
119
- ' Ok(())',
120
- '}',
121
- '',
122
- ].join('\n'),
123
- '.gitignore': 'target/\n',
124
- 'README.md': readme(name, 'rust'),
6
+ // 模板不內嵌:單一來源住 promptex-templates,GA 版預設抓與本套件版本對應的
7
+ // tag(v<版本>),預發布版預設抓 main,--ref 覆寫、--source 改用本地 checkout
8
+ // (離線與開發用)。
9
+ //
10
+ // 用法:
11
+ // npm create promptex-js [目標目錄] [-- --template project|plugin|adapter \
12
+ // --name <kebab 名> --ref <branch|tag> --source <本地路徑> \
13
+ // --git|--no-git --install --yes --locale <語系>]
14
+ import { execFileSync, spawnSync } from 'node:child_process';
15
+ import { existsSync, mkdirSync, mkdtempSync, readFileSync, readdirSync, rmSync, writeFileSync } from 'node:fs';
16
+ import { tmpdir } from 'node:os';
17
+ import { basename, dirname, join, resolve } from 'node:path';
18
+ import { fileURLToPath } from 'node:url';
19
+ import * as p from '@clack/prompts';
20
+ import mri from 'mri';
21
+ import { resolveRef } from './default-ref.js';
22
+ import { resolveLocale, t } from './messages.generated.js';
23
+ const here = fileURLToPath(new URL('.', import.meta.url));
24
+ const ownVersion = JSON.parse(readFileSync(join(here, '..', 'package.json'), 'utf8')).version;
25
+ const TEMPLATES = ['project', 'plugin', 'adapter'];
26
+ // 與 promptex init 的單元名正規化同一套規則(crates/promptex-core/src/init.rs)。
27
+ const RESERVED_UNIT_NAMES = ['cache', 'preview', 'content', 'content-index', 'graph', 'main'];
28
+ export function normalizeName(raw) {
29
+ let out = '';
30
+ for (const ch of raw.toLowerCase()) {
31
+ if (/[a-z0-9]/.test(ch))
32
+ out += ch;
33
+ else if (!out.endsWith('-'))
34
+ out += '-';
35
+ }
36
+ out = out.replace(/^-+|-+$/g, '');
37
+ return out === '' || RESERVED_UNIT_NAMES.includes(out) ? 'main' : out;
38
+ }
39
+ function bail(message) {
40
+ console.error(message);
41
+ process.exit(1);
42
+ }
43
+ const TEMPLATES_REPO = 'https://github.com/promptex-ai/promptex-templates.git';
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' },
125
48
  });
49
+ const locale = resolveLocale(args.locale);
50
+ const separator = t('common.list_separator', locale);
51
+ if (args.help) {
52
+ console.log(t('scaffold.usage.js', locale));
53
+ process.exit(0);
54
+ }
55
+ const interactive = process.stdin.isTTY && !args.yes;
56
+ p.intro(`create-promptex-js v${ownVersion}`);
57
+ // ── 互動收集(缺什麼問什麼;非互動時取預設) ──
126
58
  /**
127
- * 一份骨架的全部檔案,鍵為相對路徑。
59
+ * 缺什麼問什麼:旗標已給就用它,非互動(`--yes` 或無 TTY)取預設,其餘才問。
128
60
  *
129
- * 純計算、不碰檔案系統:讓「骨架長什麼樣」可被直接比對,不必先寫到磁碟。
61
+ * 泛型鎖住「問到的答案與預設值同型」:`p.text` 回字串、`p.confirm` 回布林,
62
+ * 兩者的呼叫點接著就把值餵給只收該型別的下游。
130
63
  */
131
- export function skeleton(name, lang) {
132
- if (!NAME_PATTERN.test(name)) {
133
- throw new Error(`專案名稱只能含英數與 . _ -,且須以英數開頭:${JSON.stringify(name)}`);
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);
73
+ }
74
+ return answer;
75
+ }
76
+ const targetDirRaw = await ask(args._[0], {
77
+ fallback: 'promptex-project',
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) }));
98
+ }
99
+ const defaultName = normalizeName(basename(targetDir));
100
+ const name = normalizeName(String(await ask(args.name, {
101
+ fallback: defaultName,
102
+ run: () => p.text({ message: t('scaffold.prompt.name', locale), defaultValue: defaultName, placeholder: defaultName }),
103
+ })));
104
+ const wantGit = args.git !== undefined
105
+ ? args.git
106
+ : await ask(undefined, {
107
+ fallback: true,
108
+ run: () => p.confirm({ message: t('scaffold.prompt.git_init', locale), initialValue: true }),
109
+ });
110
+ // ── 環境檢查:目標目錄非空即列衝突、零寫入退出(比照 promptex init)。 ──
111
+ if (existsSync(targetDir)) {
112
+ const entries = readdirSync(targetDir);
113
+ if (entries.length > 0) {
114
+ const listing = `${entries.slice(0, 10).join(separator)}${entries.length > 10 ? '…' : ''}`;
115
+ bail(t('scaffold.error.dir_not_empty', locale, { path: targetDir, entries: listing }));
134
116
  }
135
- if (lang === 'ts')
136
- return tsSkeleton(name);
137
- if (lang === 'python')
138
- return pythonSkeleton(name);
139
- if (lang === 'rust')
140
- return rustSkeleton(name);
141
- throw new Error(`不支援的語言:${lang}(可用:${LANGS.join('、')})`);
142
117
  }
143
- const exists = async (path) => {
118
+ const gitAvailable = spawnSync('git', ['--version'], { stdio: 'ignore' }).status === 0;
119
+ if (!args.source && !gitAvailable)
120
+ bail(t('scaffold.error.git_required', locale));
121
+ // ── 模板取得:--source 讀本地 checkout;否則淺 clone,GA 版預設 ref 綁本套件版本、預發布版取 main。 ──
122
+ let sourceRoot;
123
+ let sourceLabel;
124
+ let cloneDir;
125
+ if (args.source) {
126
+ sourceRoot = join(resolve(args.source), 'typescript');
127
+ sourceLabel = t('scaffold.source.local', locale, { path: resolve(args.source) });
128
+ }
129
+ else {
130
+ const { ref, showCloneHint } = resolveRef(args.ref, ownVersion);
131
+ cloneDir = mkdtempSync(join(tmpdir(), 'promptex-templates-'));
144
132
  try {
145
- await readFile(path);
146
- return true;
133
+ execFileSync('git', ['clone', '--depth', '1', '--branch', ref, TEMPLATES_REPO, cloneDir], {
134
+ stdio: ['ignore', 'ignore', 'inherit'],
135
+ });
147
136
  }
148
137
  catch {
149
- return false;
138
+ const hint = showCloneHint ? t('scaffold.error.clone_hint', locale) : '';
139
+ bail(t('scaffold.error.clone_failed', locale, { ref, hint }));
150
140
  }
151
- };
141
+ sourceRoot = join(cloneDir, 'typescript');
142
+ sourceLabel = `promptex-templates@${ref}`;
143
+ }
144
+ const starterDir = join(sourceRoot, `${template}-starter`);
145
+ if (!existsSync(starterDir))
146
+ bail(t('scaffold.error.starter_missing', locale, { template, path: starterDir }));
147
+ // ── 具現化:複製、佔位替換、_gitignore 改名。 ──
152
148
  /**
153
- * 在目標目錄產出骨架。
149
+ * 把一則訊息文字變成可直接嵌進單引號字串字面量的形態。
150
+ *
151
+ * 本套件只鋪 TypeScript 模板,而模板一律以單引號包住文字佔位符,因此界定符固定
152
+ * 是單引號。反斜線必須第一個處理:先跳脫界定符會讓後補的反斜線又被自己跳脫一次。
154
153
  *
155
- * 全有全無:有同名檔案且未帶 assumeYes 時一個檔案都不寫,避免留下半套骨架。
156
- * 這與正式版 `promptex init` 的衝突處置一致。
154
+ * 此前不跳脫,於是英文的 `this skill's content` 一進模板就提前關閉字串,
155
+ * `npm create promptex-js` 鋪出來的 plugin 與 adapter 專案在第一次建置即失敗
156
+ * (中文的對應文字沒有 ASCII 引號,因此只有英文使用者踩得到)。核心的 `init`
157
+ * 骨架走另一條路徑,同一個缺陷在那裡另行修正(`promptex_core::init`)。
157
158
  */
158
- export async function scaffold(targetDir, lang, assumeYes) {
159
- const dir = resolve(targetDir);
160
- const files = skeleton(basename(dir), lang);
161
- const paths = Object.keys(files).sort();
162
- const conflicts = [];
163
- for (const relative of paths) {
164
- if (await exists(join(dir, relative)))
165
- conflicts.push(relative);
166
- }
167
- if (conflicts.length > 0 && !assumeYes)
168
- return { aborted: true, conflicts, written: [] };
169
- const written = [];
170
- for (const relative of paths) {
171
- const path = join(dir, relative);
172
- await mkdir(dirname(path), { recursive: true });
173
- await writeFile(path, files[relative], 'utf8');
174
- written.push(path);
159
+ function escapeForLiteral(text) {
160
+ return text.replaceAll('\\', '\\\\').replaceAll('\n', '\\n').replaceAll("'", "\\'");
161
+ }
162
+ const tokens = {
163
+ '{{name}}': name,
164
+ '{{snakeName}}': name.replaceAll('-', '_'),
165
+ '{{unitName}}': name,
166
+ '{{promptexVersion}}': ownVersion,
167
+ // 模板的範例資源文字(ADR-030「一次性的起手內容」、AC-523):模板檔以佔位符留位,
168
+ // 寫檔當下代入本次語系的那一份。實作指引註解不在此列,它們在模板裡就是英文。
169
+ '{{adapterDemoRuleContent}}': escapeForLiteral(t('template.adapter_demo.rule_content', locale)),
170
+ '{{adapterDemoSkillContent}}': escapeForLiteral(t('template.adapter_demo.skill_content', locale)),
171
+ '{{adapterDemoSkillDescription}}': escapeForLiteral(t('template.adapter_demo.skill_description', locale)),
172
+ '{{adapterDemoSkillWhenToUse}}': escapeForLiteral(t('template.adapter_demo.skill_when_to_use', locale)),
173
+ '{{demoRuleName}}': escapeForLiteral(t('template.demo.rule_name', locale)),
174
+ '{{demoSkillName}}': escapeForLiteral(t('template.demo.skill_name', locale)),
175
+ '{{exampleRuleIntro}}': escapeForLiteral(t('template.example_rule.intro', locale)),
176
+ '{{exampleRuleName}}': escapeForLiteral(t('template.example_rule.name', locale)),
177
+ '{{exampleRuleOtherStates}}': escapeForLiteral(t('template.example_rule.other_states', locale)),
178
+ '{{exampleRuleScoped}}': escapeForLiteral(t('template.example_rule.scoped', locale)),
179
+ '{{exampleSkillDescription}}': escapeForLiteral(t('template.example_skill.description', locale)),
180
+ '{{exampleSkillIntro}}': escapeForLiteral(t('template.example_skill.intro', locale)),
181
+ '{{exampleSkillInvoked}}': escapeForLiteral(t('template.example_skill.invoked', locale)),
182
+ '{{exampleSkillInvokedRs}}': escapeForLiteral(t('template.example_skill.invoked_rs', locale)),
183
+ '{{exampleSkillName}}': escapeForLiteral(t('template.example_skill.name', locale)),
184
+ '{{exampleSkillWhenToUse}}': escapeForLiteral(t('template.example_skill.when_to_use', locale)),
185
+ '{{pluginDemoRuleContent}}': escapeForLiteral(t('template.plugin_demo.rule_content', locale)),
186
+ '{{pluginDemoSkillContent}}': escapeForLiteral(t('template.plugin_demo.skill_content', locale)),
187
+ '{{pluginDemoSkillDescription}}': escapeForLiteral(t('template.plugin_demo.skill_description', locale)),
188
+ '{{pluginDemoSkillWhenToUse}}': escapeForLiteral(t('template.plugin_demo.skill_when_to_use', locale)),
189
+ };
190
+ const substitute = (text) => Object.entries(tokens).reduce((acc, [token, value]) => acc.replaceAll(token, value), text);
191
+ function walk(dir, rel = '') {
192
+ const out = [];
193
+ for (const entry of readdirSync(dir, { withFileTypes: true })) {
194
+ const relPath = rel ? `${rel}/${entry.name}` : entry.name;
195
+ if (entry.isDirectory())
196
+ out.push(...walk(join(dir, entry.name), relPath));
197
+ else
198
+ out.push(relPath);
175
199
  }
176
- return { aborted: false, conflicts, written };
200
+ return out;
201
+ }
202
+ const written = [];
203
+ for (const rel of walk(starterDir)) {
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);
211
+ }
212
+ if (cloneDir)
213
+ rmSync(cloneDir, { recursive: true, force: true });
214
+ // ── 後處理:git init(可選)、--install 裝依賴。 ──
215
+ if (wantGit && gitAvailable)
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));
177
221
  }
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')}`);
@@ -0,0 +1,9 @@
1
+ export declare const MESSAGES: Record<string, Record<string, string>>;
2
+ /** 目錄認得的語系標籤。 */
3
+ export type LocaleTag = 'en' | 'zh-Hant';
4
+ /** POSIX 形態轉 BCP 47 並收斂為目錄認得的標籤;認不出來回 undefined。 */
5
+ export declare function parseLocale(tag: string | undefined): LocaleTag | undefined;
6
+ /** 依旗標與環境算出本次的語系。三項都認不出時回退路。 */
7
+ export declare function resolveLocale(flag?: string): LocaleTag;
8
+ /** 依語系取一則訊息,並以 `%{name}` 形態代入具名參數。 */
9
+ export declare function t(key: string, locale: LocaleTag, params?: Record<string, string | number>): string;
@@ -0,0 +1,296 @@
1
+ // 本檔由 scripts/gen-messages.mjs 自動產生,請勿手改。
2
+ //
3
+ // 來源是核心的訊息目錄 crates/promptex-core/locales/(ADR-030)。要改文字就改那裡,
4
+ // 再跑 `npm run gen:messages` 重新生成;直接改本檔會被核對閘擋下。
5
+ export const MESSAGES = {
6
+ "common.list_separator": {
7
+ "en": ", ",
8
+ "zh-Hant": "、",
9
+ },
10
+ "scaffold.cancelled": {
11
+ "en": "Cancelled, no file was written.",
12
+ "zh-Hant": "已取消,未寫入任何檔案。",
13
+ },
14
+ "scaffold.done.next_steps": {
15
+ "en": "Next steps:",
16
+ "zh-Hant": "下一步:",
17
+ },
18
+ "scaffold.done.written": {
19
+ "en": "Wrote %{count} files → %{path} (template source: %{source})",
20
+ "zh-Hant": "已寫入 %{count} 個檔案 → %{path}(模板來源:%{source})",
21
+ },
22
+ "scaffold.error.clone_failed": {
23
+ "en": "git clone promptex-templates@%{ref} failed. %{hint}",
24
+ "zh-Hant": "git clone promptex-templates@%{ref} 失敗。%{hint}",
25
+ },
26
+ "scaffold.error.clone_hint": {
27
+ "en": "The tag for this version may not have been created yet: use --ref main to get the latest, or --source <local-path> to take it offline.",
28
+ "zh-Hant": "此版本的 tag 可能尚未建立:用 --ref main 抓最新、或 --source <本地路徑> 離線取用。",
29
+ },
30
+ "scaffold.error.dir_create": {
31
+ "en": "Creating %{path} failed: %{reason}",
32
+ "zh-Hant": "建立 %{path} 失敗:%{reason}",
33
+ },
34
+ "scaffold.error.dir_not_empty": {
35
+ "en": "Target directory is not empty: %{path}\nExisting entries: %{entries}\nNo file was written.",
36
+ "zh-Hant": "目標目錄非空:%{path}\n既有項目:%{entries}\n未寫入任何檔案。",
37
+ },
38
+ "scaffold.error.file_read": {
39
+ "en": "Reading %{path} failed: %{reason}",
40
+ "zh-Hant": "讀取 %{path} 失敗:%{reason}",
41
+ },
42
+ "scaffold.error.file_write": {
43
+ "en": "Writing %{path} failed: %{reason}",
44
+ "zh-Hant": "寫入 %{path} 失敗:%{reason}",
45
+ },
46
+ "scaffold.error.flag_missing_value": {
47
+ "en": "%{flag} is missing its value",
48
+ "zh-Hant": "%{flag} 缺值",
49
+ },
50
+ "scaffold.error.git_required": {
51
+ "en": "Getting the template needs git (or give a local promptex-templates path with --source).",
52
+ "zh-Hant": "取得模板需要 git(或用 --source 指定本地 promptex-templates 路徑)。",
53
+ },
54
+ "scaffold.error.install_failed.js": {
55
+ "en": "npm install failed; the scaffold files were written, the dependencies were not installed.",
56
+ "zh-Hant": "npm install 失敗,鷹架檔案已寫入、依賴未裝。",
57
+ },
58
+ "scaffold.error.install_failed.py": {
59
+ "en": "uv sync failed; the scaffold files were written, the dependencies were not installed.",
60
+ "zh-Hant": "uv sync 失敗,鷹架檔案已寫入、依賴未裝。",
61
+ },
62
+ "scaffold.error.install_failed.rs": {
63
+ "en": "cargo fetch failed; the scaffold files were written, the dependencies were not fetched.",
64
+ "zh-Hant": "cargo fetch 失敗,鷹架檔案已寫入、依賴未取。",
65
+ },
66
+ "scaffold.error.starter_missing": {
67
+ "en": "The template source has no %{template}-starter: %{path}",
68
+ "zh-Hant": "模板來源缺 %{template}-starter:%{path}",
69
+ },
70
+ "scaffold.error.template_dir_read": {
71
+ "en": "Reading the template directory failed: %{reason}",
72
+ "zh-Hant": "讀取模板目錄失敗:%{reason}",
73
+ },
74
+ "scaffold.error.template_unsupported": {
75
+ "en": "Unsupported template: %{template} (available: %{choices})",
76
+ "zh-Hant": "不支援的模板:%{template}(可用:%{choices})",
77
+ },
78
+ "scaffold.error.unknown_argument": {
79
+ "en": "Unrecognized argument: %{arg}",
80
+ "zh-Hant": "不認得的引數:%{arg}",
81
+ },
82
+ "scaffold.error.uv_missing": {
83
+ "en": "--install needs uv, but no uv executable was found; the scaffold files were written, the dependencies were not installed.",
84
+ "zh-Hant": "--install 需要 uv,但找不到 uv 執行檔;鷹架檔案已寫入、依賴未裝。",
85
+ },
86
+ "scaffold.help.install": {
87
+ "en": "run uv sync right after scaffolding",
88
+ "zh-Hant": "鷹架後直接 uv sync",
89
+ },
90
+ "scaffold.help.locale": {
91
+ "en": "message locale (en|zh-Hant), defaults to the environment variables then English",
92
+ "zh-Hant": "訊息語系(en|zh-Hant),預設依環境變數,再退回英文",
93
+ },
94
+ "scaffold.help.name": {
95
+ "en": "kebab-case name, defaults to the normalized target directory name",
96
+ "zh-Hant": "kebab-case 名稱,預設取目標目錄名正規化",
97
+ },
98
+ "scaffold.help.ref": {
99
+ "en": "take the given branch/tag of promptex-templates (defaults to v<this package's version>; pre-release versions default to main)",
100
+ "zh-Hant": "抓 promptex-templates 的指定 branch/tag(預設 v<本套件版本>;預發布版預設 main)",
101
+ },
102
+ "scaffold.help.source": {
103
+ "en": "use a local promptex-templates checkout instead (for offline and development use)",
104
+ "zh-Hant": "改用本地 promptex-templates checkout(離線與開發用)",
105
+ },
106
+ "scaffold.help.target_dir": {
107
+ "en": "target directory",
108
+ "zh-Hant": "目標目錄",
109
+ },
110
+ "scaffold.help.template": {
111
+ "en": "project|plugin|adapter",
112
+ "zh-Hant": "project|plugin|adapter",
113
+ },
114
+ "scaffold.help.yes": {
115
+ "en": "skip the interactive prompts, take defaults or flag values throughout",
116
+ "zh-Hant": "跳過互動,全部取預設或旗標值",
117
+ },
118
+ "scaffold.prompt.git_init": {
119
+ "en": "Initialize a git repository?",
120
+ "zh-Hant": "初始化 git 倉庫?",
121
+ },
122
+ "scaffold.prompt.git_init_yn": {
123
+ "en": "Initialize a git repository? (y/n)",
124
+ "zh-Hant": "初始化 git 倉庫?(y/n)",
125
+ },
126
+ "scaffold.prompt.name": {
127
+ "en": "Name (kebab-case)",
128
+ "zh-Hant": "名稱(kebab-case)",
129
+ },
130
+ "scaffold.prompt.target_dir": {
131
+ "en": "Target directory",
132
+ "zh-Hant": "目標目錄",
133
+ },
134
+ "scaffold.prompt.template": {
135
+ "en": "Template",
136
+ "zh-Hant": "模板",
137
+ },
138
+ "scaffold.prompt.template_choices": {
139
+ "en": "Template (%{choices})",
140
+ "zh-Hant": "模板(%{choices})",
141
+ },
142
+ "scaffold.source.local": {
143
+ "en": "local template (%{path})",
144
+ "zh-Hant": "本地模板(%{path})",
145
+ },
146
+ "scaffold.template_option.adapter": {
147
+ "en": "adapter: a publishable third-party platform adapter package",
148
+ "zh-Hant": "adapter:可發布的第三方平台 adapter 套件",
149
+ },
150
+ "scaffold.template_option.plugin": {
151
+ "en": "plugin: a publishable plugin package",
152
+ "zh-Hant": "plugin:可發布的 plugin 套件",
153
+ },
154
+ "scaffold.template_option.project": {
155
+ "en": "project: prompt source tree (a consuming project)",
156
+ "zh-Hant": "project:提示詞源碼樹(消費專案)",
157
+ },
158
+ "scaffold.usage.js": {
159
+ "en": "create-promptex-js [target-dir] --template project|plugin|adapter --name <kebab-name> [--ref <branch|tag>] [--source <local-path>] [--git|--no-git] [--install] [--yes] [--locale <locale>]",
160
+ "zh-Hant": "create-promptex-js [目標目錄] --template project|plugin|adapter --name <kebab 名> [--ref <branch|tag>] [--source <本地路徑>] [--git|--no-git] [--install] [--yes] [--locale <語系>]",
161
+ },
162
+ "scaffold.usage.py": {
163
+ "en": "uvx create-promptex-py [target-dir] [--template project|plugin|adapter] [--name <kebab-name>] [--ref <branch|tag>] [--source <local-path>] [--git|--no-git] [--install] [--yes] [--locale <locale>]",
164
+ "zh-Hant": "uvx create-promptex-py [目標目錄] [--template project|plugin|adapter] [--name <kebab 名>] [--ref <branch|tag>] [--source <本地路徑>] [--git|--no-git] [--install] [--yes] [--locale <語系>]",
165
+ },
166
+ "scaffold.usage.rs": {
167
+ "en": "cargo create-promptex-rs [target-dir] --template project|plugin|adapter --name <kebab-name> [--ref <branch|tag>] [--source <local-path>] [--git|--no-git] [--install] [--yes] [--locale <locale>]",
168
+ "zh-Hant": "cargo create-promptex-rs [目標目錄] --template project|plugin|adapter --name <kebab 名> [--ref <branch|tag>] [--source <本地路徑>] [--git|--no-git] [--install] [--yes] [--locale <語系>]",
169
+ },
170
+ "template.adapter_demo.rule_content": {
171
+ "en": "On the custom platform this rule degrades to always-on, with its scope noted in the body.",
172
+ "zh-Hant": "本規則在自訂平台會降級為常駐,並於內文標註適用範圍。",
173
+ },
174
+ "template.adapter_demo.skill_content": {
175
+ "en": "This skill is projected onto two placements at once: claude and the custom platform.",
176
+ "zh-Hant": "本技能會同時投影到 claude 與自訂平台兩個落點。",
177
+ },
178
+ "template.adapter_demo.skill_description": {
179
+ "en": "A demo skill the adapter example projects",
180
+ "zh-Hant": "adapter 範例用來輸出的示範技能",
181
+ },
182
+ "template.adapter_demo.skill_when_to_use": {
183
+ "en": "When checking what the custom adapter emits",
184
+ "zh-Hant": "驗證自訂 adapter 的產出時",
185
+ },
186
+ "template.demo.rule_name": {
187
+ "en": "Demo rule",
188
+ "zh-Hant": "示範規則",
189
+ },
190
+ "template.demo.skill_name": {
191
+ "en": "Demo skill",
192
+ "zh-Hant": "示範技能",
193
+ },
194
+ "template.example_rule.intro": {
195
+ "en": "This is the example resource produced by promptex init; change it or delete it as your project needs.",
196
+ "zh-Hant": "這是 promptex init 產出的範例資源,可依專案需求修改或刪除。",
197
+ },
198
+ "template.example_rule.name": {
199
+ "en": "Example rule",
200
+ "zh-Hant": "範例規則",
201
+ },
202
+ "template.example_rule.other_states": {
203
+ "en": "The other two states: declare the always-on flag instead and it loads unconditionally, regardless of what you are doing; declare neither and you get the default third state, where the resource is still installed on the target but never enters context on its own, appearing only when another resource references it explicitly. A skeleton that stopped at the third state would hand the author a rule that compiles and does nothing.",
204
+ "zh-Hant": "另兩態:改宣告常駐旗標即無條件載入,不看當下在做什麼;兩者都不宣告則是預設的第三態,資源照樣安裝到目標端,卻不會自動進入上下文,只在被其他資源顯式引用時登場。骨架若停在第三態,作者拿到的會是一份編得動、卻不會有任何作用的規則。",
205
+ },
206
+ "template.example_rule.scoped": {
207
+ "en": "This rule declares a scope (every file under the prompts source directory), which puts it in the scoped-loading state, one of the three loading states. The scope declaration appears in the artifact as that platform's own loading declaration, and the platform loads the rule into context only while working on a matching file.",
208
+ "zh-Hant": "本規則宣告了適用範圍(源碼目錄 prompts 下的全部檔案),因此屬載入宣告三態中的範圍載入態:範圍宣告在產物裡呈現為該平台的載入宣告,平台於符合的檔案上工作時才把規則載入上下文。",
209
+ },
210
+ "template.example_skill.description": {
211
+ "en": "Shows how an invoked resource is declared: a description of what it does and trigger text for when to use it",
212
+ "zh-Hant": "示範被喚起型資源的宣告:描述它做什麼、觸發語料說何時用",
213
+ },
214
+ "template.example_skill.intro": {
215
+ "en": "This is the example skill that ships with the project starter; change it or delete it as your project needs.",
216
+ "zh-Hant": "這是 project starter 附帶的範例技能,可依專案需求修改或刪除。",
217
+ },
218
+ "template.example_skill.invoked": {
219
+ "en": "A skill is an invoked resource: config.whenToUse is its trigger text (required), and the platform decides from it when to load the skill into context. Set against the rule next to it, which uses scoped loading, the two together show the most common pair of loading declarations.",
220
+ "zh-Hant": "技能屬被喚起型資源:config.whenToUse 是觸發語料(必填),平台依它判斷何時把本技能載入上下文。與相鄰的規則(範圍載入)對照,兩者合起來展示了載入宣告最常用的兩種形態。",
221
+ },
222
+ "template.example_skill.invoked_rs": {
223
+ "en": "A skill is an invoked resource: whenToUse on config is its trigger text (required, enforced by the typestate parameter), and the platform decides from it when to load the skill into context. Set against the rule next to it, which uses scoped loading, the two together show the most common pair of loading declarations.",
224
+ "zh-Hant": "技能屬被喚起型資源:config 的 whenToUse 是觸發語料(必填,型別狀態參數把關),平台依它判斷何時把本技能載入上下文。與相鄰的規則(範圍載入)對照,兩者合起來展示了載入宣告最常用的兩種形態。",
225
+ },
226
+ "template.example_skill.name": {
227
+ "en": "Example skill",
228
+ "zh-Hant": "範例技能",
229
+ },
230
+ "template.example_skill.when_to_use": {
231
+ "en": "When demonstrating how an invoked resource is declared",
232
+ "zh-Hant": "示範被喚起型資源的宣告形態時",
233
+ },
234
+ "template.plugin_demo.rule_content": {
235
+ "en": "The plugin appends a line to this rule's content during the transform stage.",
236
+ "zh-Hant": "本規則的內容會在 transform 階段被 plugin 追加一行。",
237
+ },
238
+ "template.plugin_demo.skill_content": {
239
+ "en": "The plugin appends a line to this skill's content during the transform stage.",
240
+ "zh-Hant": "本技能的內容會在 transform 階段被 plugin 追加一行。",
241
+ },
242
+ "template.plugin_demo.skill_description": {
243
+ "en": "A demo skill the plugin example builds against",
244
+ "zh-Hant": "plugin 範例用來建置的示範技能",
245
+ },
246
+ "template.plugin_demo.skill_when_to_use": {
247
+ "en": "When checking how the plugin rewrites a skill",
248
+ "zh-Hant": "驗證 plugin 對技能的改寫時",
249
+ },
250
+ };
251
+ const FALLBACK = 'en';
252
+ const LOCALE_ENV_ORDER = ["PROMPTEX_LOCALE", "LANGUAGE", "LC_ALL", "LC_MESSAGES", "LANG"];
253
+ /** POSIX 形態轉 BCP 47 並收斂為目錄認得的標籤;認不出來回 undefined。 */
254
+ export function parseLocale(tag) {
255
+ const normalized = String(tag ?? '').trim().split(/[.@]/)[0].replaceAll('_', '-');
256
+ if (normalized === '')
257
+ return undefined;
258
+ const upper = normalized.toUpperCase();
259
+ if (upper === 'C' || upper === 'POSIX')
260
+ return 'en';
261
+ const [language, ...rest] = normalized.toLowerCase().split('-');
262
+ if (language === 'en')
263
+ return 'en';
264
+ if (language === 'zh' && rest.some((p) => ['hant', 'tw', 'hk', 'mo'].includes(p)))
265
+ return 'zh-Hant';
266
+ return undefined;
267
+ }
268
+ /** 依旗標與環境算出本次的語系。三項都認不出時回退路。 */
269
+ export function resolveLocale(flag) {
270
+ const fromFlag = parseLocale(flag);
271
+ if (fromFlag !== undefined)
272
+ return fromFlag;
273
+ for (const key of LOCALE_ENV_ORDER) {
274
+ const raw = process.env[key];
275
+ if (raw === undefined || raw.trim() === '')
276
+ continue;
277
+ for (const part of raw.split(':')) {
278
+ const found = parseLocale(part);
279
+ if (found !== undefined)
280
+ return found;
281
+ }
282
+ }
283
+ return FALLBACK;
284
+ }
285
+ /** 依語系取一則訊息,並以 `%{name}` 形態代入具名參數。 */
286
+ export function t(key, locale, params) {
287
+ const entry = MESSAGES[key];
288
+ if (entry === undefined)
289
+ return key;
290
+ const template = entry[locale] ?? entry[FALLBACK];
291
+ if (template === undefined)
292
+ return key;
293
+ if (params === undefined)
294
+ return template;
295
+ return template.replace(/%\{([^}]+)\}/g, (whole, name) => (name in params ? String(params[name]) : whole));
296
+ }
package/package.json CHANGED
@@ -1,35 +1,37 @@
1
1
  {
2
2
  "name": "create-promptex-js",
3
- "version": "0.0.1-alpha.1",
4
- "description": "promptex 專案鷹架的最小原型(alpha):產出可立即建置的專案骨架",
3
+ "version": "1.0.0-alpha.1",
4
+ "description": "Scaffold a promptex TypeScript project, plugin or adapter.",
5
5
  "license": "MIT OR Apache-2.0",
6
- "author": "promptex-ai",
7
- "homepage": "https://github.com/promptex-ai/promptex#readme",
8
6
  "repository": {
9
7
  "type": "git",
10
- "url": "git+https://github.com/promptex-ai/promptex.git"
8
+ "url": "https://github.com/promptex-ai/promptex.git",
9
+ "directory": "packages/create-promptex-js"
11
10
  },
12
- "bugs": {
13
- "url": "https://github.com/promptex-ai/promptex/issues"
14
- },
15
- "keywords": ["prompt", "prompt-engineering", "ai", "llm", "agent", "claude", "skills", "scaffold"],
16
11
  "type": "module",
17
- "main": "./dist/index.js",
18
- "types": "./dist/index.d.ts",
19
12
  "bin": {
20
13
  "create-promptex-js": "./bin/create-promptex-js.mjs"
21
14
  },
15
+ "files": [
16
+ "bin",
17
+ "dist"
18
+ ],
22
19
  "engines": {
23
- "node": ">=22.14.0"
20
+ "node": ">=18"
24
21
  },
25
- "files": ["dist", "bin", "README.md", "LICENSE-MIT", "LICENSE-APACHE"],
26
22
  "scripts": {
27
23
  "build": "tsc -p tsconfig.json",
28
- "prepack": "npm run build",
29
- "test": "node --test 'test/*.test.mjs'"
24
+ "pretest": "npm run build",
25
+ "test": "node --experimental-strip-types --test test/smoke.test.ts && npm run test:types",
26
+ "test:types": "tsc -p tsconfig.typelevel.json",
27
+ "prepack": "npm run build"
28
+ },
29
+ "dependencies": {
30
+ "@clack/prompts": "^0.10.0",
31
+ "mri": "^1.2.0"
30
32
  },
31
33
  "devDependencies": {
32
34
  "@types/node": "^22.10.0",
33
- "typescript": "^5.7.0"
35
+ "typescript": "^7.0.2"
34
36
  }
35
37
  }
package/LICENSE-APACHE DELETED
@@ -1,204 +0,0 @@
1
- Apache License
2
- Version 2.0, January 2004
3
- http://www.apache.org/licenses/
4
-
5
- TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
6
-
7
- 1. Definitions.
8
-
9
- "License" shall mean the terms and conditions for use, reproduction,
10
- and distribution as defined by Sections 1 through 9 of this document.
11
-
12
- "Licensor" shall mean the copyright owner or entity authorized by
13
- the copyright owner that is granting the License.
14
-
15
- "Legal Entity" shall mean the union of the acting entity and all
16
- other entities that control, are controlled by, or are under common
17
- control with that entity. For the purposes of this definition,
18
- "control" means (i) the power, direct or indirect, to cause the
19
- direction or management of such entity, whether by contract or
20
- otherwise, or (ii) ownership of fifty percent (50%) or more of the
21
- outstanding shares, or (iii) beneficial ownership of such entity.
22
-
23
- "You" (or "Your") shall mean an individual or Legal Entity
24
- exercising permissions granted by this License.
25
-
26
- "Source" form shall mean the preferred form for making modifications,
27
- including but not limited to software source code, documentation
28
- source, and configuration files.
29
-
30
- "Object" form shall mean any form resulting from mechanical
31
- transformation or translation of a Source form, including but
32
- not limited to compiled object code, generated documentation,
33
- and conversions to other media types.
34
-
35
- "Work" shall mean the work of authorship, whether in Source or
36
- Object form, made available under the License, as indicated by a
37
- copyright notice that is included in or attached to the work
38
- (an example is provided in the Appendix below).
39
-
40
- "Derivative Works" shall mean any work, whether in Source or Object
41
- form, that is based on (or derived from) the Work and for which the
42
- editorial revisions, annotations, elaborations, or other modifications
43
- represent, as a whole, an original work of authorship. For the
44
- purposes of this License, Derivative Works shall not include works
45
- that remain separable from, or merely link (or bind by name) to the
46
- interfaces of, the Work and Derivative Works thereof.
47
-
48
- "Contribution" shall mean any work of authorship, including the
49
- original version of the Work and any modifications or additions
50
- to that Work or Derivative Works thereof, that is intentionally
51
- submitted to Licensor for inclusion in the Work by the copyright
52
- owner or by an individual or Legal Entity authorized to submit on
53
- behalf of the copyright owner. For the purposes of this definition,
54
- "submitted" means any form of electronic, verbal, or written
55
- communication sent to the Licensor or its representatives,
56
- including but not limited to communication on electronic mailing
57
- lists, source code control systems, and issue tracking systems that
58
- are managed by, or on behalf of, the Licensor for the purpose of
59
- discussing and improving the Work, but excluding communication that
60
- is conspicuously marked or otherwise designated in writing by the
61
- copyright owner as "Not a Contribution."
62
-
63
- "Contributor" shall mean Licensor and any individual or Legal Entity
64
- on behalf of whom a Contribution has been received by Licensor and
65
- subsequently incorporated within the Work.
66
-
67
- 2. Grant of Copyright License. Subject to the terms and conditions of
68
- this License, each Contributor hereby grants to You a perpetual,
69
- worldwide, non-exclusive, no-charge, royalty-free, irrevocable
70
- copyright license to reproduce, prepare Derivative Works of,
71
- publicly display, publicly perform, sublicense, and distribute the
72
- Work and such Derivative Works in Source or Object form.
73
-
74
- 3. Grant of Patent License. Subject to the terms and conditions of
75
- this License, each Contributor hereby grants to You a perpetual,
76
- worldwide, non-exclusive, no-charge, royalty-free, irrevocable
77
- (except as stated in this section) patent license to make, have made,
78
- use, offer to sell, sell, import, and otherwise transfer the Work,
79
- where such license applies only to those patent claims licensable
80
- by such Contributor that are necessarily infringed by their
81
- Contribution(s) alone or by combination of their Contribution(s)
82
- with the Work to which such Contribution(s) was submitted. If You
83
- institute patent litigation against any entity (including a
84
- cross-claim or counterclaim in a lawsuit) alleging that the Work
85
- or a Contribution incorporated within the Work constitutes direct
86
- or contributory patent infringement, then any patent licenses
87
- granted to You under this License for that Work shall terminate
88
- as of the date such litigation is filed.
89
-
90
- 4. Redistribution. You may reproduce and distribute copies of the
91
- Work or Derivative Works thereof in any medium, with or without
92
- modifications, and in Source or Object form, provided that You
93
- meet the following conditions:
94
-
95
- (a) You must give any other recipients of the Work or
96
- Derivative Works a copy of this License; and
97
-
98
- (b) You must cause any modified files to carry prominent notices
99
- stating that You changed the files; and
100
-
101
- (c) You must retain, in the Source form of any Derivative Works
102
- that You distribute, all copyright, patent, trademark, and
103
- attribution notices from the Source form of the Work,
104
- excluding those notices that do not pertain to any part of
105
- the Derivative Works; and
106
-
107
- (d) If the Work includes a "NOTICE" text file as part of its
108
- distribution, then any Derivative Works that You distribute must
109
- include a readable copy of the attribution notices contained
110
- within such NOTICE file, excluding those notices that do not
111
- pertain to any part of the Derivative Works, in at least one
112
- of the following places: within a NOTICE text file distributed
113
- as part of the Derivative Works; within the Source form or
114
- documentation, if provided along with the Derivative Works; or,
115
- within a display generated by the Derivative Works, if and
116
- wherever such third-party notices normally appear. The contents
117
- of the NOTICE file are for informational purposes only and
118
- do not modify the License. You may add Your own attribution
119
- notices within Derivative Works that You distribute, alongside
120
- or as an addendum to the NOTICE text from the Work, provided
121
- that such additional attribution notices cannot be construed
122
- as modifying the License.
123
-
124
- You may add Your own copyright statement to Your modifications and
125
- may provide additional or different license terms and conditions
126
- for use, reproduction, or distribution of Your modifications, or
127
- for any such Derivative Works as a whole, provided Your use,
128
- reproduction, and distribution of the Work otherwise complies with
129
- the conditions stated in this License.
130
-
131
- 5. Submission of Contributions. Unless You explicitly state otherwise,
132
- any Contribution intentionally submitted for inclusion in the Work
133
- by You to the Licensor shall be under the terms and conditions of
134
- this License, without any additional terms or conditions.
135
- Notwithstanding the above, nothing herein shall supersede or modify
136
- the terms of any separate license agreement you may have executed
137
- with Licensor regarding such Contributions.
138
-
139
- 6. Trademarks. This License does not grant permission to use the trade
140
- names, trademarks, service marks, or product names of the Licensor,
141
- except as required for reasonable and customary use in describing
142
- the origin of the Work and reproducing the content of the NOTICE
143
- file.
144
-
145
- 7. Disclaimer of Warranty. Unless required by applicable law or
146
- agreed to in writing, Licensor provides the Work (and each
147
- Contributor provides its Contributions) on an "AS IS" BASIS,
148
- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
149
- implied, including, without limitation, any warranties or conditions
150
- of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
151
- PARTICULAR PURPOSE. You are solely responsible for determining the
152
- appropriateness of using or redistributing the Work and assume any
153
- risks associated with Your exercise of permissions under this
154
- License.
155
-
156
- 8. Limitation of Liability. In no event and under no legal theory,
157
- whether in tort (including negligence), contract, or otherwise,
158
- unless required by applicable law (such as deliberate and grossly
159
- negligent acts) or agreed to in writing, shall any Contributor be
160
- liable to You for damages, including any direct, indirect, special,
161
- incidental, or consequential damages of any character arising as a
162
- result of this License or out of the use or inability to use the
163
- Work (including but not limited to damages for loss of goodwill,
164
- work stoppage, computer failure or malfunction, or any and all
165
- other commercial damages or losses), even if such Contributor
166
- has been advised of the possibility of such damages.
167
-
168
- 9. Accepting Warranty or Additional Liability. While redistributing
169
- the Work or Derivative Works thereof, You may choose to offer,
170
- and charge a fee for, acceptance of support, warranty, indemnity,
171
- or other liability obligations and/or rights consistent with this
172
- License. However, in accepting such obligations, You may act only
173
- on Your own behalf and on Your sole responsibility, not on behalf
174
- of any other Contributor, and only if You agree to indemnify,
175
- defend, and hold each Contributor harmless for any liability
176
- incurred by, or claims asserted against, such Contributor by reason
177
- of your accepting any such warranty or additional liability.
178
-
179
- END OF TERMS AND CONDITIONS
180
-
181
- APPENDIX: How to apply the Apache License to your work.
182
-
183
- To apply the Apache License to your work, attach the following
184
- boilerplate notice, with the fields enclosed by brackets "[]"
185
- replaced with your own identifying information. (Don't include
186
- the brackets!) The text should be enclosed in the appropriate
187
- comment syntax for the file format. We also recommend that a
188
- file or class name and description of purpose be included on the
189
- same "printed page" as the copyright notice for easier
190
- identification within third-party archives.
191
-
192
- Copyright 2026 promptex-ai
193
-
194
- Licensed under the Apache License, Version 2.0 (the "License");
195
- you may not use this file except in compliance with the License.
196
- You may obtain a copy of the License at
197
-
198
- http://www.apache.org/licenses/LICENSE-2.0
199
-
200
- Unless required by applicable law or agreed to in writing, software
201
- distributed under the License is distributed on an "AS IS" BASIS,
202
- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
203
- See the License for the specific language governing permissions and
204
- limitations under the License.
package/LICENSE-MIT DELETED
@@ -1,21 +0,0 @@
1
- MIT License
2
-
3
- Copyright (c) 2026 promptex-ai
4
-
5
- Permission is hereby granted, free of charge, to any person obtaining a copy
6
- of this software and associated documentation files (the "Software"), to deal
7
- in the Software without restriction, including without limitation the rights
8
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
- copies of the Software, and to permit persons to whom the Software is
10
- furnished to do so, subject to the following conditions:
11
-
12
- The above copyright notice and this permission notice shall be included in all
13
- copies or substantial portions of the Software.
14
-
15
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
- SOFTWARE.