pan-stack-cli 0.1.5 → 0.2.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/lib/manifest.js DELETED
@@ -1,366 +0,0 @@
1
- const fs = require("fs");
2
- const path = require("path");
3
-
4
- const PLACEHOLDER_VALUES = new Set(["", "repo_url_to_be_filled", "pan_stack_repo_url"]);
5
-
6
- function cleanValue(value) {
7
- let result = String(value || "").trim();
8
- const commentIndex = result.indexOf(" #");
9
- if (commentIndex >= 0) {
10
- result = result.slice(0, commentIndex).trimEnd();
11
- }
12
- if (
13
- (result.startsWith('"') && result.endsWith('"')) ||
14
- (result.startsWith("'") && result.endsWith("'"))
15
- ) {
16
- result = result.slice(1, -1);
17
- }
18
- return result;
19
- }
20
-
21
- function splitKeyValue(text) {
22
- const index = text.indexOf(":");
23
- if (index < 0) {
24
- return [text.trim(), ""];
25
- }
26
- return [text.slice(0, index).trim(), cleanValue(text.slice(index + 1))];
27
- }
28
-
29
- function parseManifest(manifestPath) {
30
- const data = { repositories: [], stack: {}, commands: {} };
31
- let section = "";
32
- let currentRepo = null;
33
- let currentCommandName = "";
34
- let currentCommand = null;
35
- let currentTargetName = "";
36
- let blockMode = null;
37
- let blockIndent = 0;
38
- let blockLines = [];
39
-
40
- function flushBlock() {
41
- if (!blockMode || !currentCommand) {
42
- blockMode = null;
43
- blockLines = [];
44
- return;
45
- }
46
- const body = blockLines.join("\n");
47
- if (blockMode === "command-shell") {
48
- currentCommand.shell = body;
49
- } else if (blockMode === "target-shell" && currentCommand.targets[currentTargetName]) {
50
- currentCommand.targets[currentTargetName].shell = body;
51
- }
52
- blockMode = null;
53
- blockLines = [];
54
- }
55
-
56
- const lines = fs.readFileSync(manifestPath, "utf8").split(/\r?\n/);
57
- for (const rawLine of lines) {
58
- if (blockMode) {
59
- const trimmed = rawLine.trimEnd();
60
- if (!trimmed.trim()) {
61
- blockLines.push("");
62
- continue;
63
- }
64
- const indent = rawLine.length - rawLine.trimStart().length;
65
- if (indent < blockIndent) {
66
- flushBlock();
67
- } else {
68
- blockLines.push(rawLine.slice(blockIndent));
69
- continue;
70
- }
71
- }
72
-
73
- if (!rawLine.trim() || rawLine.trimStart().startsWith("#")) {
74
- continue;
75
- }
76
-
77
- const indent = rawLine.length - rawLine.trimStart().length;
78
- const text = rawLine.trim();
79
-
80
- if (indent === 0) {
81
- flushBlock();
82
- const [key, value] = splitKeyValue(text);
83
- section = key;
84
- currentRepo = null;
85
- currentCommandName = "";
86
- currentCommand = null;
87
- currentTargetName = "";
88
- if (value) {
89
- data[key] = value;
90
- } else if (!(key in data)) {
91
- data[key] = key === "commands" ? {} : {};
92
- }
93
- continue;
94
- }
95
-
96
- if (section === "stack" && indent === 2) {
97
- const [key, value] = splitKeyValue(text);
98
- data.stack[key] = value;
99
- continue;
100
- }
101
-
102
- if (section === "repositories") {
103
- if (indent === 2 && text.startsWith("- ")) {
104
- currentRepo = {};
105
- data.repositories.push(currentRepo);
106
- const rest = text.slice(2).trim();
107
- if (rest) {
108
- const [key, value] = splitKeyValue(rest);
109
- currentRepo[key] = value;
110
- }
111
- continue;
112
- }
113
- if (currentRepo && indent === 4) {
114
- const [key, value] = splitKeyValue(text);
115
- currentRepo[key] = value;
116
- }
117
- continue;
118
- }
119
-
120
- if (section === "commands") {
121
- if (indent === 2) {
122
- flushBlock();
123
- const [key, value] = splitKeyValue(text);
124
- currentCommandName = key;
125
- currentCommand = {};
126
- currentTargetName = "";
127
- data.commands[currentCommandName] = currentCommand;
128
- if (value) {
129
- currentCommand[key] = value;
130
- }
131
- continue;
132
- }
133
-
134
- if (indent === 4 && currentCommand) {
135
- flushBlock();
136
- const [key, value] = splitKeyValue(text);
137
- if (key === "shell" && (value === "|" || value === ">")) {
138
- blockMode = "command-shell";
139
- blockIndent = indent + 2;
140
- blockLines = [];
141
- continue;
142
- }
143
- if (key === "targets") {
144
- currentCommand.targets = currentCommand.targets || {};
145
- continue;
146
- }
147
- currentCommand[key] = value;
148
- continue;
149
- }
150
-
151
- if (indent === 6 && currentCommand) {
152
- flushBlock();
153
- const [key, value] = splitKeyValue(text);
154
- currentTargetName = key;
155
- currentCommand.targets = currentCommand.targets || {};
156
- const target = {};
157
- currentCommand.targets[currentTargetName] = target;
158
- if (value) {
159
- target[key] = value;
160
- }
161
- if (key === "shell" && (value === "|" || value === ">")) {
162
- blockMode = "target-shell";
163
- blockIndent = indent + 2;
164
- blockLines = [];
165
- }
166
- continue;
167
- }
168
-
169
- if (indent === 8 && currentCommand && currentCommand.targets && currentTargetName) {
170
- flushBlock();
171
- const target = currentCommand.targets[currentTargetName];
172
- const [key, value] = splitKeyValue(text);
173
- if (key === "shell" && (value === "|" || value === ">")) {
174
- blockMode = "target-shell";
175
- blockIndent = indent + 2;
176
- blockLines = [];
177
- continue;
178
- }
179
- target[key] = value;
180
- }
181
- }
182
- }
183
-
184
- flushBlock();
185
- return data;
186
- }
187
-
188
- function uniqueRepos(repos) {
189
- const seen = new Set();
190
- const result = [];
191
- for (const repo of repos) {
192
- const key = [
193
- cleanValue(repo.name),
194
- cleanValue(repo.url),
195
- cleanValue(repo.path),
196
- ].join("\u0000");
197
- if (seen.has(key)) {
198
- continue;
199
- }
200
- seen.add(key);
201
- result.push(repo);
202
- }
203
- return result;
204
- }
205
-
206
- function manifestRepos(data) {
207
- return uniqueRepos(data.repositories || []);
208
- }
209
-
210
- function validateRepo(repo) {
211
- const name = cleanValue(repo.name);
212
- const url = cleanValue(repo.url);
213
- const repoPath = cleanValue(repo.path) || name;
214
-
215
- if (!name) {
216
- throw new Error("Repository entry is missing 'name'.");
217
- }
218
- if (PLACEHOLDER_VALUES.has(url)) {
219
- throw new Error(`Repository '${name}' is missing a real url.`);
220
- }
221
- if (!repoPath) {
222
- throw new Error(`Repository '${name}' is missing 'path'.`);
223
- }
224
- return { name, url, repoPath };
225
- }
226
-
227
- function loadManifest(manifestPath) {
228
- const resolved = path.resolve(manifestPath);
229
- if (!fs.existsSync(resolved)) {
230
- throw new Error(`Manifest not found: ${manifestPath}`);
231
- }
232
- const data = parseManifest(resolved);
233
- return {
234
- path: resolved,
235
- root: path.dirname(resolved),
236
- project: cleanValue(data.project) || path.basename(path.dirname(resolved)),
237
- managedBy: cleanValue(data.managed_by),
238
- stack: data.stack || {},
239
- repositories: manifestRepos(data),
240
- commands: data.commands || {},
241
- };
242
- }
243
-
244
- const PAN_CONFIG_FILE = ".pan.yaml";
245
-
246
- function loadPanConfig(configPath, meta = {}) {
247
- const resolved = path.resolve(configPath);
248
- if (!fs.existsSync(resolved)) {
249
- throw new Error(`Config not found: ${configPath}`);
250
- }
251
- const data = parseManifest(resolved);
252
- const root = path.dirname(resolved);
253
- return {
254
- path: resolved,
255
- root,
256
- workspaceRoot: meta.workspaceRoot || root,
257
- project: cleanValue(data.project) || meta.project || path.basename(root),
258
- repoName: meta.repoName || "",
259
- scope: meta.scope || "repo",
260
- commands: data.commands || {},
261
- };
262
- }
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
-
315
- function getCommandDefinition(source, commandName, subArg) {
316
- const def = source.commands[commandName];
317
- if (!def) {
318
- return null;
319
- }
320
-
321
- if (def.targets && typeof def.targets === "object") {
322
- const targetName = subArg || def.default || Object.keys(def.targets)[0];
323
- const target = def.targets[targetName];
324
- if (!target) {
325
- const valid = Object.keys(def.targets).join(" | ");
326
- throw new Error(`Valid targets for '${commandName}': ${valid}`);
327
- }
328
- return {
329
- ...target,
330
- targetName,
331
- description: def.description,
332
- };
333
- }
334
-
335
- return def;
336
- }
337
-
338
- function listCommands(source) {
339
- return Object.entries(source.commands || {}).map(([name, def]) => ({
340
- name,
341
- aliases: commandAliases(def),
342
- description: def.description || "",
343
- hasTargets: Boolean(def.targets),
344
- targets: def.targets ? Object.keys(def.targets) : [],
345
- }));
346
- }
347
-
348
- function commandBaseDir(source) {
349
- return source.scope === "workspace" ? source.workspaceRoot : source.root;
350
- }
351
-
352
- module.exports = {
353
- PLACEHOLDER_VALUES,
354
- PAN_CONFIG_FILE,
355
- cleanValue,
356
- parseManifest,
357
- loadManifest,
358
- loadPanConfig,
359
- manifestRepos,
360
- validateRepo,
361
- commandAliases,
362
- resolveCommandName,
363
- getCommandDefinition,
364
- listCommands,
365
- commandBaseDir,
366
- };
package/lib/platform.js DELETED
@@ -1,140 +0,0 @@
1
- const fs = require("fs");
2
- const path = require("path");
3
- const { spawnSync } = require("child_process");
4
-
5
- const WINDOWS_RESERVED_NAMES = new Set([
6
- "CON",
7
- "PRN",
8
- "AUX",
9
- "NUL",
10
- "COM1",
11
- "COM2",
12
- "COM3",
13
- "COM4",
14
- "COM5",
15
- "COM6",
16
- "COM7",
17
- "COM8",
18
- "COM9",
19
- "LPT1",
20
- "LPT2",
21
- "LPT3",
22
- "LPT4",
23
- "LPT5",
24
- "LPT6",
25
- "LPT7",
26
- "LPT8",
27
- "LPT9",
28
- ]);
29
-
30
- function isWindows() {
31
- return process.platform === "win32";
32
- }
33
-
34
- function splitEnvList(value) {
35
- if (!value) {
36
- return [];
37
- }
38
- // On Windows, do not split on ":" — drive letters use "C:".
39
- const parts = isWindows() ? value.split(/[;,]/) : value.split(/[:;,]/);
40
- return parts.map((part) => part.trim()).filter(Boolean);
41
- }
42
-
43
- function findBash() {
44
- const candidates = isWindows()
45
- ? [
46
- process.env.PAN_GIT_BASH,
47
- process.env.PAN_BASH,
48
- process.env.GIT_BASH,
49
- process.env.PROGRAMFILES && path.join(process.env.PROGRAMFILES, "Git", "bin", "bash.exe"),
50
- process.env["ProgramFiles(x86)"] &&
51
- path.join(process.env["ProgramFiles(x86)"], "Git", "bin", "bash.exe"),
52
- process.env.LOCALAPPDATA &&
53
- path.join(process.env.LOCALAPPDATA, "Programs", "Git", "bin", "bash.exe"),
54
- ]
55
- : [process.env.PAN_BASH, process.env.BASH, "/usr/bin/bash", "/bin/bash"];
56
-
57
- for (const candidate of candidates.filter(Boolean)) {
58
- if (fs.existsSync(candidate)) {
59
- return candidate;
60
- }
61
- }
62
-
63
- return null;
64
- }
65
-
66
- /** @deprecated use findBash */
67
- function findGitBash() {
68
- return findBash();
69
- }
70
-
71
- function spawnOptions(extra = {}) {
72
- return isWindows() ? { windowsHide: true, ...extra } : extra;
73
- }
74
-
75
- function normalizeShellEnv(env) {
76
- if (!isWindows()) {
77
- return env;
78
- }
79
-
80
- const normalized = { ...env };
81
- for (const key of ["PAN_WORKSPACE_ROOT", "PAN_REPO_ROOT", "PAN_PROJECT_ROOT"]) {
82
- if (typeof normalized[key] === "string") {
83
- normalized[key] = normalized[key].replace(/\\/g, "/");
84
- }
85
- }
86
- return normalized;
87
- }
88
-
89
- function runShellScript(script, { cwd, env, stdio = "inherit" } = {}) {
90
- const shellEnv = normalizeShellEnv(env);
91
- const bash = findBash();
92
-
93
- if (!bash) {
94
- const hint = isWindows()
95
- ? "Install Git for Windows (https://git-scm.com/download/win) or set PAN_GIT_BASH / PAN_BASH to bash.exe."
96
- : "Install bash or set PAN_BASH to its path.";
97
- throw new Error(
98
- `Shell commands in .pan.yaml require bash. ${hint} Alternatively, use 'exec' instead of 'shell'.`
99
- );
100
- }
101
-
102
- return spawnSync(bash, ["-lc", script], spawnOptions({ cwd, env: shellEnv, stdio }));
103
- }
104
-
105
- function runExecLine(execLine, { cwd, env, stdio = "inherit" } = {}) {
106
- return spawnSync(
107
- execLine,
108
- spawnOptions({ cwd, env: normalizeShellEnv(env), stdio, shell: true })
109
- );
110
- }
111
-
112
- function validateFolderName(name) {
113
- if (!name || !/^[a-zA-Z0-9][a-zA-Z0-9._-]*$/.test(name)) {
114
- throw new Error(
115
- "Invalid folder name. Use letters, numbers, dots, hyphens, or underscores."
116
- );
117
- }
118
-
119
- if (isWindows()) {
120
- if (/[<>:"/\\|?*]/.test(name)) {
121
- throw new Error("Invalid folder name for Windows (<>:\"/\\|?* are not allowed).");
122
- }
123
- const base = name.replace(/\.+$/, "").toUpperCase();
124
- if (WINDOWS_RESERVED_NAMES.has(base)) {
125
- throw new Error(`Invalid folder name for Windows (reserved name: ${base}).`);
126
- }
127
- }
128
- }
129
-
130
- module.exports = {
131
- isWindows,
132
- splitEnvList,
133
- findBash,
134
- findGitBash,
135
- spawnOptions,
136
- normalizeShellEnv,
137
- runShellScript,
138
- runExecLine,
139
- validateFolderName,
140
- };
@@ -1,213 +0,0 @@
1
- const fs = require("fs");
2
- const path = require("path");
3
- const { spawn, spawnSync } = require("child_process");
4
- const readline = require("readline");
5
- const {
6
- getCommandDefinition,
7
- commandBaseDir,
8
- resolveCommandName,
9
- } = require("./manifest");
10
- const { sourceMatchesProject } = require("./discover");
11
- const { runShellScript, runExecLine, spawnOptions } = require("./platform");
12
-
13
- function needDir(dir) {
14
- if (!fs.existsSync(dir)) {
15
- throw new Error(`Directory not found: ${dir} (run 'pan clone' first?)`);
16
- }
17
- }
18
-
19
- function resolveCwd(source, cwd) {
20
- const base = commandBaseDir(source);
21
- if (!cwd) {
22
- return base;
23
- }
24
- if (path.isAbsolute(cwd)) {
25
- return cwd;
26
- }
27
- return path.resolve(base, cwd);
28
- }
29
-
30
- function buildPanEnv(source, forwardedArgs) {
31
- const panEnv = {
32
- ...process.env,
33
- PAN_REPO_ROOT: source.root,
34
- PAN_WORKSPACE_ROOT: source.workspaceRoot,
35
- PAN_PROJECT_ROOT: source.workspaceRoot,
36
- PAN_ARGS: forwardedArgs.join(" "),
37
- };
38
- forwardedArgs.forEach((value, index) => {
39
- panEnv[`PAN_ARG_${index + 1}`] = value;
40
- });
41
- return panEnv;
42
- }
43
-
44
- function runExec(source, def, forwardedArgs) {
45
- const cwd = resolveCwd(source, def.cwd);
46
- needDir(cwd);
47
-
48
- const execLine = [def.exec, ...forwardedArgs].filter(Boolean).join(" ").trim();
49
- if (!execLine) {
50
- throw new Error("Command is missing 'exec' or 'shell'.");
51
- }
52
-
53
- const result = runExecLine(execLine, {
54
- cwd,
55
- stdio: "inherit",
56
- env: buildPanEnv(source, forwardedArgs),
57
- });
58
-
59
- process.exit(result.status ?? 1);
60
- }
61
-
62
- function runShell(source, def, forwardedArgs) {
63
- const cwd = resolveCwd(source, def.cwd || ".");
64
- if (def.cwd) {
65
- needDir(cwd);
66
- }
67
-
68
- let result;
69
- try {
70
- result = runShellScript(def.shell, {
71
- cwd,
72
- stdio: "inherit",
73
- env: buildPanEnv(source, forwardedArgs),
74
- });
75
- } catch (error) {
76
- console.error(error.message);
77
- process.exit(1);
78
- }
79
-
80
- process.exit(result.status ?? 1);
81
- }
82
-
83
- function runProjectCommand(source, commandName, args) {
84
- let subArg = "";
85
- let forwardedArgs = [...args];
86
-
87
- const def = source.commands[commandName];
88
- if (def?.targets) {
89
- if (forwardedArgs.length > 0 && def.targets[forwardedArgs[0]]) {
90
- subArg = forwardedArgs.shift();
91
- }
92
- }
93
-
94
- const resolved = getCommandDefinition(source, commandName, subArg);
95
- if (!resolved) {
96
- throw new Error(`Command '${commandName}' not found in ${source.path}`);
97
- }
98
-
99
- if (resolved.shell) {
100
- runShell(source, resolved, forwardedArgs);
101
- return;
102
- }
103
-
104
- if (resolved.exec) {
105
- runExec(source, resolved, forwardedArgs);
106
- return;
107
- }
108
-
109
- throw new Error(`Command '${commandName}' is missing 'exec' or 'shell' in ${source.path}`);
110
- }
111
-
112
- function sourceLabel(source) {
113
- if (source.scope === "workspace") {
114
- return `${source.project} (workspace)`;
115
- }
116
- return source.repoName || source.project;
117
- }
118
-
119
- function promptProjectChoice(matches, commandName) {
120
- return new Promise((resolve, reject) => {
121
- const rl = readline.createInterface({
122
- input: process.stdin,
123
- output: process.stdout,
124
- });
125
-
126
- console.log("");
127
- matches.forEach((source, index) => {
128
- const def = source.commands[commandName];
129
- const desc = def?.description ? ` — ${def.description}` : "";
130
- console.log(` ${index + 1}. ${sourceLabel(source)} (${source.root})${desc}`);
131
- });
132
- console.log("");
133
-
134
- rl.question("Pick a project (number): ", (answer) => {
135
- rl.close();
136
- const choice = Number.parseInt(answer, 10);
137
- if (!Number.isFinite(choice) || choice < 1 || choice > matches.length) {
138
- reject(new Error("Invalid choice."));
139
- return;
140
- }
141
- resolve(matches[choice - 1]);
142
- });
143
- });
144
- }
145
-
146
- async function runWithProjectResolution(
147
- matches,
148
- commandName,
149
- args,
150
- projectFlag,
151
- allSources = matches
152
- ) {
153
- const resolvedName = resolveCommandName(allSources, commandName);
154
-
155
- if (matches.length === 0) {
156
- throw new Error(
157
- `Command '${commandName}' not found in any .pan.yaml file.`
158
- );
159
- }
160
-
161
- let source = null;
162
- if (projectFlag) {
163
- source = matches.find((item) => sourceMatchesProject(item, projectFlag));
164
- if (!source) {
165
- const names = matches.map((item) => sourceLabel(item)).join(", ");
166
- throw new Error(`Project '${projectFlag}' not found. Available: ${names}`);
167
- }
168
- } else if (matches.length === 1) {
169
- source = matches[0];
170
- } else {
171
- const withTargetArg = matches.filter((item) => {
172
- const def = item.commands[resolvedName];
173
- return def?.targets && args[0] && def.targets[args[0]];
174
- });
175
- if (withTargetArg.length === 1) {
176
- source = withTargetArg[0];
177
- } else if (process.stdin.isTTY) {
178
- source = await promptProjectChoice(matches, commandName);
179
- } else {
180
- const lines = matches.map(
181
- (item, index) => `${index + 1}. ${sourceLabel(item)} (${item.root})`
182
- );
183
- throw new Error(
184
- `Command '${commandName}' is defined in multiple projects. Use --project <name>.\n${lines.join("\n")}`
185
- );
186
- }
187
- }
188
-
189
- runProjectCommand(source, resolvedName, args);
190
- }
191
-
192
- function runGitScript(scriptName, args) {
193
- const scriptPath = path.join(__dirname, "git", scriptName);
194
- if (!fs.existsSync(scriptPath)) {
195
- throw new Error(`Script not found: ${scriptPath}`);
196
- }
197
-
198
- const child = spawn(process.execPath, [scriptPath, ...args], spawnOptions({
199
- stdio: "inherit",
200
- env: process.env,
201
- }));
202
-
203
- child.on("exit", (code) => {
204
- process.exit(code ?? 1);
205
- });
206
- }
207
-
208
- module.exports = {
209
- runProjectCommand,
210
- runWithProjectResolution,
211
- runGitScript,
212
- sourceLabel,
213
- };