pan-stack-cli 0.1.0 → 0.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -33,7 +33,7 @@ Sau đó gõ `pan` thay cho `./pan`.
33
33
 
34
34
  Tuỳ chọn git (forward xuống script):
35
35
 
36
- - `--base-dir DIR` — thư mục gốc cho path repo con (mặc định: `.`)
36
+ - `--base-dir DIR` — thư mục gốc cho path repo con (mặc định: thư mục chứa `.project.yaml`; tự walk-up từ cwd nếu không có ngay tại đó)
37
37
  - `--dry-run` — chỉ in lệnh
38
38
  - `[manifest]` — đường dẫn manifest (mặc định: `.project.yaml`)
39
39
 
@@ -58,12 +58,21 @@ commands:
58
58
  description: Deploy backend local
59
59
  exec: scripts/deploy.sh
60
60
 
61
+ install:
62
+ alias: i
63
+ description: Cài dependencies (pan i hoặc pan install)
64
+ shell: |
65
+ set -euo pipefail
66
+ pnpm install
67
+
61
68
  lint:
62
69
  shell: |
63
70
  set -euo pipefail
64
71
  pnpm lint
65
72
  ```
66
73
 
74
+ Alias: `alias: i` hoặc `aliases: i, ins` (cách nhau bằng dấu phẩy).
75
+
67
76
  ### Schema `.pan.yaml` (workspace root)
68
77
 
69
78
  Lệnh orchestration — `cwd` tương đối workspace root; dùng `PAN_WORKSPACE_ROOT` trong shell:
package/bin/pan.js CHANGED
@@ -95,8 +95,10 @@ function printAllCommands(sources) {
95
95
  const targetHint = command.hasTargets
96
96
  ? ` [${command.targets.join("|")}]`
97
97
  : "";
98
+ const aliasHint =
99
+ command.aliases.length > 0 ? ` (${command.aliases.join(", ")})` : "";
98
100
  const desc = command.description ? ` — ${command.description}` : "";
99
- console.log(` ${command.name}${targetHint}${desc}`);
101
+ console.log(` ${command.name}${aliasHint}${targetHint}${desc}`);
100
102
  }
101
103
  }
102
104
  }
@@ -126,12 +128,13 @@ async function main() {
126
128
  return;
127
129
  }
128
130
 
131
+ const allSources = loadAllCommandSources();
129
132
  const matches = findSourcesWithCommand(command);
130
133
  if (matches.length > 1 && !project && process.stdin.isTTY) {
131
134
  console.log(formatProjectList(matches, command));
132
135
  }
133
136
 
134
- await runWithProjectResolution(matches, command, args, project);
137
+ await runWithProjectResolution(matches, command, args, project, allSources);
135
138
  }
136
139
 
137
140
  main().catch((error) => {
package/lib/discover.js CHANGED
@@ -6,6 +6,7 @@ const {
6
6
  loadManifest,
7
7
  loadPanConfig,
8
8
  getCommandDefinition,
9
+ commandAliases,
9
10
  validateRepo,
10
11
  cleanValue,
11
12
  } = require("./manifest");
@@ -63,12 +64,17 @@ function walkUpManifests(startDir) {
63
64
  return found;
64
65
  }
65
66
 
66
- function findNearestWorkspace(startDir = process.cwd()) {
67
+ function findNearestProjectManifest(startDir = process.cwd()) {
67
68
  const walked = walkUpManifests(startDir);
68
- if (walked.length === 0) {
69
+ return walked[0] || null;
70
+ }
71
+
72
+ function findNearestWorkspace(startDir = process.cwd()) {
73
+ const manifestPath = findNearestProjectManifest(startDir);
74
+ if (!manifestPath) {
69
75
  return null;
70
76
  }
71
- return loadManifest(walked[0]);
77
+ return loadManifest(manifestPath);
72
78
  }
73
79
 
74
80
  function loadCommandSourcesForWorkspace(workspace) {
@@ -166,9 +172,18 @@ function sourceMatchesProject(source, projectFlag) {
166
172
  );
167
173
  }
168
174
 
175
+ function sourceHasCommand(source, commandName) {
176
+ if (source.commands[commandName]) {
177
+ return true;
178
+ }
179
+ return Object.values(source.commands || {}).some((def) =>
180
+ commandAliases(def).includes(commandName)
181
+ );
182
+ }
183
+
169
184
  function findSourcesWithCommand(commandName, startDir = process.cwd()) {
170
- return loadAllCommandSources(startDir).filter(
171
- (source) => source.commands[commandName]
185
+ return loadAllCommandSources(startDir).filter((source) =>
186
+ sourceHasCommand(source, commandName)
172
187
  );
173
188
  }
174
189
 
@@ -195,6 +210,7 @@ function isBuiltinGitCommand(name) {
195
210
  module.exports = {
196
211
  BUILTIN_GIT_COMMANDS,
197
212
  PAN_CONFIG_FILE,
213
+ findNearestProjectManifest,
198
214
  findNearestWorkspace,
199
215
  loadAllCommandSources,
200
216
  loadCommandSourcesForWorkspace,
@@ -47,7 +47,7 @@ function main() {
47
47
  return;
48
48
  }
49
49
 
50
- const repos = loadManifestRepos({
50
+ const { repos } = loadManifestRepos({
51
51
  manifestPath: args.manifest,
52
52
  baseDir: args.baseDir,
53
53
  });
@@ -40,7 +40,7 @@ function main() {
40
40
  return;
41
41
  }
42
42
 
43
- const repos = loadManifestRepos({
43
+ const { repos } = loadManifestRepos({
44
44
  manifestPath: args.manifest,
45
45
  baseDir: args.baseDir,
46
46
  });
@@ -5,7 +5,6 @@
5
5
  * Alias tương đương grv trong oh-my-zsh — hiển thị URL đầy đủ (fetch/push).
6
6
  */
7
7
 
8
- const path = require("path");
9
8
  const { parseCommonArgs, loadManifestRepos, printCommonOptions } = require("./helpers/cli");
10
9
  const { ensureRepoExists, isGitRepo, listRemotesVerbose } = require("./helpers/git");
11
10
 
@@ -28,8 +27,7 @@ function printRepoRemotes({ label, destination, dryRun }) {
28
27
  console.log("");
29
28
  }
30
29
 
31
- function showWorkspaceRootRemotes({ manifestPath, baseDir, dryRun }) {
32
- const workspaceRoot = path.resolve(baseDir || path.dirname(manifestPath || ".project.yaml"));
30
+ function showWorkspaceRootRemotes({ workspaceRoot, dryRun }) {
33
31
  if (!isGitRepo(workspaceRoot)) {
34
32
  return;
35
33
  }
@@ -64,16 +62,14 @@ function main() {
64
62
  return;
65
63
  }
66
64
 
67
- const manifestPath = path.resolve(args.manifest || ".project.yaml");
68
- const repos = loadManifestRepos({
69
- manifestPath,
65
+ const { repos, workspaceRoot } = loadManifestRepos({
66
+ manifestPath: args.manifest,
70
67
  baseDir: args.baseDir,
71
68
  });
72
69
 
73
70
  if (!args.noWorkspace) {
74
71
  showWorkspaceRootRemotes({
75
- manifestPath,
76
- baseDir: args.baseDir,
72
+ workspaceRoot,
77
73
  dryRun: args.dryRun,
78
74
  });
79
75
  }
@@ -5,7 +5,6 @@
5
5
  * Alias tương đương gst trong oh-my-zsh — chạy git status trên workspace + repo con.
6
6
  */
7
7
 
8
- const path = require("path");
9
8
  const { parseCommonArgs, loadManifestRepos, printCommonOptions } = require("./helpers/cli");
10
9
  const { ensureRepoExists, isGitRepo, runGitStatus } = require("./helpers/git");
11
10
 
@@ -22,8 +21,7 @@ function printRepoStatus({ label, destination, dryRun, gitArgs }) {
22
21
  console.log("");
23
22
  }
24
23
 
25
- function showWorkspaceRootStatus({ manifestPath, baseDir, dryRun, gitArgs }) {
26
- const workspaceRoot = path.resolve(baseDir || path.dirname(manifestPath || ".project.yaml"));
24
+ function showWorkspaceRootStatus({ workspaceRoot, dryRun, gitArgs }) {
27
25
  if (!isGitRepo(workspaceRoot)) {
28
26
  return;
29
27
  }
@@ -62,16 +60,14 @@ function main() {
62
60
  }
63
61
 
64
62
  const gitArgs = args.gitArgs || [];
65
- const manifestPath = path.resolve(args.manifest || ".project.yaml");
66
- const repos = loadManifestRepos({
67
- manifestPath,
63
+ const { repos, workspaceRoot } = loadManifestRepos({
64
+ manifestPath: args.manifest,
68
65
  baseDir: args.baseDir,
69
66
  });
70
67
 
71
68
  if (!args.noWorkspace) {
72
69
  showWorkspaceRootStatus({
73
- manifestPath,
74
- baseDir: args.baseDir,
70
+ workspaceRoot,
75
71
  dryRun: args.dryRun,
76
72
  gitArgs,
77
73
  });
@@ -53,7 +53,7 @@ function main() {
53
53
  return;
54
54
  }
55
55
 
56
- const repos = loadManifestRepos({
56
+ const { repos } = loadManifestRepos({
57
57
  manifestPath: args.manifest,
58
58
  baseDir: args.baseDir,
59
59
  });
@@ -53,7 +53,7 @@ function main() {
53
53
  return;
54
54
  }
55
55
 
56
- const repos = loadManifestRepos({
56
+ const { repos } = loadManifestRepos({
57
57
  manifestPath: args.manifest,
58
58
  baseDir: args.baseDir,
59
59
  });
@@ -1,5 +1,6 @@
1
1
  const path = require("path");
2
2
  const fs = require("fs");
3
+ const { findNearestProjectManifest } = require("../../discover");
3
4
  const { parseManifest, manifestRepos, validateRepo } = require("./manifest");
4
5
 
5
6
  function parseCommonArgs(argv, extraFlags = {}) {
@@ -51,19 +52,61 @@ function looksLikeManifest(arg) {
51
52
  }
52
53
  }
53
54
 
54
- function loadManifestRepos({ manifestPath, baseDir }) {
55
- const resolvedManifest = path.resolve(manifestPath || ".project.yaml");
56
- if (!fs.existsSync(resolvedManifest)) {
57
- throw new Error(`Manifest not found: ${manifestPath || ".project.yaml"}`);
55
+ function resolveProjectWorkspace({
56
+ manifestPath = "",
57
+ baseDir = ".",
58
+ startDir = process.cwd(),
59
+ }) {
60
+ const manifestName = manifestPath || ".project.yaml";
61
+ const cwdManifest = path.isAbsolute(manifestName)
62
+ ? manifestName
63
+ : path.resolve(startDir, manifestName);
64
+
65
+ let resolvedManifest = "";
66
+ let usedWalkUp = false;
67
+
68
+ if (fs.existsSync(cwdManifest)) {
69
+ resolvedManifest = cwdManifest;
70
+ } else {
71
+ const found = findNearestProjectManifest(startDir);
72
+ if (!found) {
73
+ const label = manifestPath || ".project.yaml";
74
+ throw new Error(
75
+ `Manifest not found: ${label} (đã tìm từ ${startDir} lên thư mục cha)`
76
+ );
77
+ }
78
+ resolvedManifest = found;
79
+ usedWalkUp = true;
80
+ }
81
+
82
+ const workspaceRoot = path.dirname(resolvedManifest);
83
+ let resolvedBaseDir;
84
+ if (baseDir === ".") {
85
+ resolvedBaseDir = usedWalkUp ? workspaceRoot : path.resolve(startDir, ".");
86
+ } else if (path.isAbsolute(baseDir)) {
87
+ resolvedBaseDir = baseDir;
88
+ } else {
89
+ resolvedBaseDir = path.resolve(startDir, baseDir);
58
90
  }
59
91
 
92
+ return {
93
+ manifestPath: resolvedManifest,
94
+ baseDir: resolvedBaseDir,
95
+ workspaceRoot,
96
+ };
97
+ }
98
+
99
+ function loadManifestRepos({ manifestPath, baseDir, startDir = process.cwd() }) {
100
+ const workspace = resolveProjectWorkspace({ manifestPath, baseDir, startDir });
101
+ const { manifestPath: resolvedManifest, baseDir: resolvedBaseDir, workspaceRoot } =
102
+ workspace;
103
+
60
104
  const data = parseManifest(resolvedManifest);
61
105
  const repos = manifestRepos(data);
62
106
  if (repos.length === 0) {
63
107
  throw new Error("No repositories found in manifest.");
64
108
  }
65
109
 
66
- const resolvedBaseDir = path.resolve(baseDir);
67
110
  const results = [];
68
111
  for (const repo of repos) {
69
112
  try {
@@ -83,7 +126,12 @@ function loadManifestRepos({ manifestPath, baseDir }) {
83
126
  throw new Error("No valid repositories found in manifest.");
84
127
  }
85
128
 
86
- return results;
129
+ return {
130
+ repos: results,
131
+ manifestPath: resolvedManifest,
132
+ baseDir: resolvedBaseDir,
133
+ workspaceRoot,
134
+ };
87
135
  }
88
136
 
89
137
  function printCommonOptions() {
@@ -92,12 +140,13 @@ function printCommonOptions() {
92
140
 
93
141
  options:
94
142
  -h, --help show this help message and exit
95
- --base-dir BASE_DIR Base directory for relative repo paths. Default: current directory.
143
+ --base-dir BASE_DIR Base directory for relative repo paths. Default: thư mục chứa .project.yaml (walk-up từ cwd nếu cần).
96
144
  --dry-run Print commands only`;
97
145
  }
98
146
 
99
147
  module.exports = {
100
148
  parseCommonArgs,
149
+ resolveProjectWorkspace,
101
150
  loadManifestRepos,
102
151
  printCommonOptions,
103
152
  };
package/lib/manifest.js CHANGED
@@ -261,6 +261,57 @@ function loadPanConfig(configPath, meta = {}) {
261
261
  };
262
262
  }
263
263
 
264
+ function commandAliases(def) {
265
+ if (!def) {
266
+ return [];
267
+ }
268
+ const result = [];
269
+ if (def.alias) {
270
+ result.push(cleanValue(def.alias));
271
+ }
272
+ if (def.aliases) {
273
+ const raw = def.aliases;
274
+ if (Array.isArray(raw)) {
275
+ result.push(...raw.map((item) => cleanValue(item)));
276
+ } else {
277
+ const value = cleanValue(raw);
278
+ if (value.startsWith("[") && value.endsWith("]")) {
279
+ result.push(
280
+ ...value
281
+ .slice(1, -1)
282
+ .split(",")
283
+ .map((item) => cleanValue(item))
284
+ .filter(Boolean)
285
+ );
286
+ } else if (value.includes(",")) {
287
+ result.push(
288
+ ...value
289
+ .split(",")
290
+ .map((item) => cleanValue(item))
291
+ .filter(Boolean)
292
+ );
293
+ } else if (value) {
294
+ result.push(value);
295
+ }
296
+ }
297
+ }
298
+ return [...new Set(result.filter(Boolean))];
299
+ }
300
+
301
+ function resolveCommandName(sources, commandName) {
302
+ if (sources.some((source) => source.commands[commandName])) {
303
+ return commandName;
304
+ }
305
+ for (const source of sources) {
306
+ for (const [primaryName, def] of Object.entries(source.commands || {})) {
307
+ if (commandAliases(def).includes(commandName)) {
308
+ return primaryName;
309
+ }
310
+ }
311
+ }
312
+ return commandName;
313
+ }
314
+
264
315
  function getCommandDefinition(source, commandName, subArg) {
265
316
  const def = source.commands[commandName];
266
317
  if (!def) {
@@ -287,6 +338,7 @@ function getCommandDefinition(source, commandName, subArg) {
287
338
  function listCommands(source) {
288
339
  return Object.entries(source.commands || {}).map(([name, def]) => ({
289
340
  name,
341
+ aliases: commandAliases(def),
290
342
  description: def.description || "",
291
343
  hasTargets: Boolean(def.targets),
292
344
  targets: def.targets ? Object.keys(def.targets) : [],
@@ -306,6 +358,8 @@ module.exports = {
306
358
  loadPanConfig,
307
359
  manifestRepos,
308
360
  validateRepo,
361
+ commandAliases,
362
+ resolveCommandName,
309
363
  getCommandDefinition,
310
364
  listCommands,
311
365
  commandBaseDir,
@@ -2,7 +2,11 @@ const fs = require("fs");
2
2
  const path = require("path");
3
3
  const { spawn, spawnSync } = require("child_process");
4
4
  const readline = require("readline");
5
- const { getCommandDefinition, commandBaseDir } = require("./manifest");
5
+ const {
6
+ getCommandDefinition,
7
+ commandBaseDir,
8
+ resolveCommandName,
9
+ } = require("./manifest");
6
10
  const { sourceMatchesProject } = require("./discover");
7
11
 
8
12
  function needDir(dir) {
@@ -134,7 +138,15 @@ function promptProjectChoice(matches, commandName) {
134
138
  });
135
139
  }
136
140
 
137
- async function runWithProjectResolution(matches, commandName, args, projectFlag) {
141
+ async function runWithProjectResolution(
142
+ matches,
143
+ commandName,
144
+ args,
145
+ projectFlag,
146
+ allSources = matches
147
+ ) {
148
+ const resolvedName = resolveCommandName(allSources, commandName);
149
+
138
150
  if (matches.length === 0) {
139
151
  throw new Error(
140
152
  `Không tìm thấy lệnh '${commandName}' trong bất kỳ .pan.yaml nào.`
@@ -152,7 +164,7 @@ async function runWithProjectResolution(matches, commandName, args, projectFlag)
152
164
  source = matches[0];
153
165
  } else {
154
166
  const withTargetArg = matches.filter((item) => {
155
- const def = item.commands[commandName];
167
+ const def = item.commands[resolvedName];
156
168
  return def?.targets && args[0] && def.targets[args[0]];
157
169
  });
158
170
  if (withTargetArg.length === 1) {
@@ -169,7 +181,7 @@ async function runWithProjectResolution(matches, commandName, args, projectFlag)
169
181
  }
170
182
  }
171
183
 
172
- runProjectCommand(source, commandName, args);
184
+ runProjectCommand(source, resolvedName, args);
173
185
  }
174
186
 
175
187
  function runGitScript(scriptName, args) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pan-stack-cli",
3
- "version": "0.1.0",
3
+ "version": "0.1.2",
4
4
  "description": "CLI điều phối PAN Stack — git đa repo và lệnh dự án từ .project.yaml",
5
5
  "license": "MIT",
6
6
  "bin": {
@@ -24,4 +24,4 @@
24
24
  "url": "https://github.com/doiya46/pan-stack.git",
25
25
  "directory": ".pan/pan-stack-cli"
26
26
  }
27
- }
27
+ }