@pplethai/cli 0.4.0 → 0.4.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/dist/scaffold.js DELETED
@@ -1,58 +0,0 @@
1
- // Copying the golden-path template into an app's folder.
2
- import fs from "node:fs/promises";
3
- import path from "node:path";
4
- import { CliError } from "./errors.js";
5
- import { MANUAL_INSTALL } from "./npm.js";
6
- /** Build output and local secrets are never part of a fresh app. */
7
- const SKIP = new Set([
8
- ".git",
9
- "node_modules",
10
- "dist",
11
- ".wrangler",
12
- ".dev.vars",
13
- ".env",
14
- ]);
15
- /**
16
- * npm strips a file literally named `.gitignore` from published packages, so
17
- * the bundled template carries it under a safe name and it is restored here.
18
- */
19
- const RESTORED_NAMES = new Map([["gitignore", ".gitignore"]]);
20
- /** The name the template uses for itself everywhere it names something. */
21
- const TEMPLATE_SLUG = "pple-app";
22
- /** Files whose contents name the app: package name, D1 database, R2 bucket. */
23
- const PERSONALIZED = new Set(["package.json", "package-lock.json", "wrangler.jsonc"]);
24
- export async function scaffold(templateDir, targetDir, slug) {
25
- const entries = await fs.readdir(templateDir).catch(() => null);
26
- if (entries === null) {
27
- throw new CliError("ไม่พบเทมเพลตแอป PPLE ในการติดตั้ง CLI ชุดนี้", `ติดตั้ง CLI ใหม่ (\`${MANUAL_INSTALL}\`) แล้วลองอีกครั้ง`);
28
- }
29
- const written = [];
30
- await copyDir({ from: templateDir, to: targetDir, prefix: "", slug, written });
31
- return written.sort();
32
- }
33
- async function copyDir(step) {
34
- for (const entry of await fs.readdir(step.from, { withFileTypes: true })) {
35
- if (SKIP.has(entry.name))
36
- continue;
37
- const name = RESTORED_NAMES.get(entry.name) ?? entry.name;
38
- const source = path.join(step.from, entry.name);
39
- const destination = path.join(step.to, name);
40
- const relative = step.prefix === "" ? name : `${step.prefix}/${name}`;
41
- if (entry.isDirectory()) {
42
- await fs.mkdir(destination, { recursive: true });
43
- await copyDir({ ...step, from: source, to: destination, prefix: relative });
44
- continue;
45
- }
46
- if (PERSONALIZED.has(name)) {
47
- const contents = await fs.readFile(source, "utf8");
48
- await fs.writeFile(destination, replaceAll(contents, TEMPLATE_SLUG, step.slug));
49
- }
50
- else {
51
- await fs.copyFile(source, destination);
52
- }
53
- step.written.push(relative);
54
- }
55
- }
56
- function replaceAll(text, from, to) {
57
- return text.split(from).join(to);
58
- }
package/dist/session.js DELETED
@@ -1,29 +0,0 @@
1
- import { loadCredentials, saveCredentials } from "./credentials.js";
2
- import { refresh } from "./device-flow.js";
3
- import { SIGN_IN_EXPIRED, SIGNED_OUT } from "./errors.js";
4
- import { ProvisionerClient } from "./api.js";
5
- /** Refresh this long before expiry, so a command doesn't die mid-flight. */
6
- const EXPIRY_MARGIN_MS = 60_000;
7
- /**
8
- * The access token for this command, refreshed if it is about to expire. The
9
- * only way a command gets to talk to the platform.
10
- */
11
- export async function accessToken(deps) {
12
- const stored = await loadCredentials(deps);
13
- if (!stored)
14
- throw SIGNED_OUT;
15
- const expiring = stored.expiresAt !== null && stored.expiresAt - deps.now() < EXPIRY_MARGIN_MS;
16
- if (!expiring)
17
- return stored.accessToken;
18
- if (stored.refreshToken) {
19
- const renewed = await refresh(deps, stored.refreshToken);
20
- if (renewed) {
21
- await saveCredentials(deps, { ...renewed, ssoBase: deps.config.ssoBase });
22
- return renewed.accessToken;
23
- }
24
- }
25
- throw SIGN_IN_EXPIRED;
26
- }
27
- export async function client(deps) {
28
- return new ProvisionerClient(deps, await accessToken(deps));
29
- }
package/dist/skills.js DELETED
@@ -1,86 +0,0 @@
1
- // Platform Skills as a local overlay in each Builder App workspace.
2
- // Overwrites only `.claude/skills/pple-platform/`; sibling skills are left alone.
3
- import { createHash } from "node:crypto";
4
- import fs from "node:fs/promises";
5
- import path from "node:path";
6
- import { CliError } from "./errors.js";
7
- import { MANUAL_INSTALL } from "./npm.js";
8
- /** Where the platform Skill lands inside a Builder App workspace. */
9
- export const PLATFORM_SKILL_REL = path.join(".claude", "skills", "pple-platform");
10
- /** The only Skills path that belongs in the app's `.gitignore` (posix form). */
11
- export const PLATFORM_SKILL_IGNORE = `${PLATFORM_SKILL_REL.split(path.sep).join("/")}/`;
12
- /**
13
- * Refresh the platform Skill overlay from the CLI bundle. Prints only when
14
- * the managed content actually changed (first install or update).
15
- */
16
- export async function refreshSkills(deps, appDir) {
17
- const source = deps.skillsDir;
18
- const entries = await fs.readdir(source).catch(() => null);
19
- if (entries === null) {
20
- throw new CliError("ไม่พบ Skill ของ PPLE Platform ในการติดตั้ง CLI ชุดนี้", `ติดตั้ง CLI ใหม่ (\`${MANUAL_INSTALL}\`) แล้วลองอีกครั้ง`);
21
- }
22
- const destination = path.join(appDir, PLATFORM_SKILL_REL);
23
- const before = await contentFingerprint(destination);
24
- await replaceDir(source, destination);
25
- await ensureIgnored(appDir);
26
- const after = await contentFingerprint(destination);
27
- if (before === after)
28
- return;
29
- deps.out.say(before === null ? "ติดตั้ง Skill ของแพลตฟอร์มแล้ว" : "อัปเดต Skill ของแพลตฟอร์มแล้ว");
30
- }
31
- async function replaceDir(from, to) {
32
- await fs.rm(to, { recursive: true, force: true });
33
- await copyDir(from, to);
34
- }
35
- async function copyDir(from, to) {
36
- await fs.mkdir(to, { recursive: true });
37
- for (const entry of await fs.readdir(from, { withFileTypes: true })) {
38
- if (entry.name === ".DS_Store")
39
- continue;
40
- const src = path.join(from, entry.name);
41
- const dst = path.join(to, entry.name);
42
- if (entry.isDirectory())
43
- await copyDir(src, dst);
44
- else
45
- await fs.copyFile(src, dst);
46
- }
47
- }
48
- /** Stable hash of every file under `dir`, or null when the tree is missing. */
49
- async function contentFingerprint(dir) {
50
- const files = await listFiles(dir).catch(() => null);
51
- if (files === null)
52
- return null;
53
- const hash = createHash("sha256");
54
- for (const relative of files.sort()) {
55
- hash.update(relative);
56
- hash.update("\0");
57
- hash.update(await fs.readFile(path.join(dir, relative)));
58
- hash.update("\0");
59
- }
60
- return hash.digest("hex");
61
- }
62
- async function listFiles(dir, prefix = "") {
63
- const entries = await fs.readdir(dir, { withFileTypes: true });
64
- const files = [];
65
- for (const entry of entries) {
66
- if (entry.name === ".DS_Store")
67
- continue;
68
- const relative = prefix === "" ? entry.name : `${prefix}/${entry.name}`;
69
- if (entry.isDirectory()) {
70
- files.push(...(await listFiles(path.join(dir, entry.name), relative)));
71
- }
72
- else {
73
- files.push(relative);
74
- }
75
- }
76
- return files;
77
- }
78
- async function ensureIgnored(appDir) {
79
- const ignorePath = path.join(appDir, ".gitignore");
80
- const existing = await fs.readFile(ignorePath, "utf8").catch(() => "");
81
- const lines = existing.split("\n");
82
- if (lines.some((line) => line.trim() === PLATFORM_SKILL_IGNORE))
83
- return;
84
- const body = existing.endsWith("\n") || existing === "" ? existing : `${existing}\n`;
85
- await fs.writeFile(ignorePath, `${body}${PLATFORM_SKILL_IGNORE}\n`);
86
- }
package/dist/ui.js DELETED
@@ -1,12 +0,0 @@
1
- export function consoleOutput(write, writeError) {
2
- return {
3
- say(line = "") {
4
- write(`${line}\n`);
5
- },
6
- fail(error) {
7
- writeError(`${error.message}\n`);
8
- if (error.hint)
9
- writeError(`${error.hint}\n`);
10
- },
11
- };
12
- }
package/dist/update.js DELETED
@@ -1,142 +0,0 @@
1
- // The CLI keeping itself current: the daily check that rides along with
2
- // ordinary commands, and the words both it and `pple upgrade` use — one
3
- // upgrade story reaches a Builder, however they arrived at it.
4
- //
5
- // The check runs before the command the Builder typed, which is the only
6
- // moment an upgrade is safe: nothing is half-done yet, so the new CLI can
7
- // simply be handed the same command line. Everything here is written to fail
8
- // towards the command — a quiet npm, a missing config directory, a refused
9
- // upgrade all end with `pple deploy` deploying.
10
- import fs from "node:fs/promises";
11
- import path from "node:path";
12
- import { installGlobally, isNewer, isNpmGlobalInstall, latestVersion, MANUAL_INSTALL, npmMessage, } from "./npm.js";
13
- /** About once a day, per machine — not per app, and not per command. */
14
- const CHECK_INTERVAL = 24 * 60 * 60 * 1000;
15
- /** Cadence state, beside credentials.json and well away from any app repo. */
16
- const STATE_FILE = "update-check.json";
17
- /** The Builder's own settings for this CLI, the other half of the opt-out. */
18
- const CONFIG_FILE = "config.json";
19
- /**
20
- * Commands that already answer the version question themselves, or that
21
- * print and exit. `upgrade` doing its own check twice would be silly; help
22
- * should never touch the network.
23
- */
24
- const UNCHECKED = new Set(["upgrade", "help", "--help", "-h"]);
25
- export function wantsUpdateCheck(argv) {
26
- return argv.length > 0 && !UNCHECKED.has(argv[0]);
27
- }
28
- /** The one line that opens every "there's a newer pple" conversation. */
29
- export function announceUpdate(deps, latest) {
30
- deps.out.say(`มี pple เวอร์ชันใหม่ (${latest}) คุณใช้เวอร์ชัน ${deps.version}`);
31
- }
32
- /**
33
- * What to say about an install npm's global root doesn't hold. Installing
34
- * over it would leave the Builder with two pples and no way to tell which
35
- * one just ran (ADR-0007).
36
- */
37
- export function sayOtherChannel(deps) {
38
- deps.out.say("pple ชุดนี้ไม่ได้ติดตั้งผ่าน npm การอัปเดตที่นี่อาจทำให้คุณ");
39
- deps.out.say("มี pple สองเวอร์ชัน กรุณาอัปเดตด้วยวิธีเดิมที่คุณติดตั้งไว้");
40
- deps.out.say(`หรือติดตั้งด้วย npm:\n\n ${MANUAL_INSTALL}`);
41
- }
42
- /** Why npm gave up, and the line the Builder can always run themselves. */
43
- export function manualHint(result) {
44
- return `${npmMessage(result)}\n\nลองรันคำสั่งนี้ด้วยตัวเอง:\n\n ${MANUAL_INSTALL}`;
45
- }
46
- /**
47
- * Look for a newer CLI, and act on it as far as this session allows. Returns
48
- * an exit code when the command has already been dealt with — the CLI
49
- * upgraded and re-ran it, or the Builder chose to stop — and null to let the
50
- * command the Builder typed run as usual.
51
- */
52
- export async function checkForUpdate(deps, argv) {
53
- if (!(await isDue(deps)))
54
- return null;
55
- await recordCheck(deps);
56
- const latest = await latestVersion(deps);
57
- // A quiet registry is not the Builder's problem: no message, no delay
58
- // tomorrow — the command simply carries on.
59
- if (latest === null || !isNewer(latest, deps.version))
60
- return null;
61
- announceUpdate(deps, latest);
62
- // The channel comes before the terminal: an install npm doesn't own can't
63
- // be upgraded by anyone here, so pointing at `pple upgrade` — which would
64
- // only refuse — helps nobody, attended or not.
65
- if (!(await isNpmGlobalInstall(deps))) {
66
- sayOtherChannel(deps);
67
- deps.out.say();
68
- return null;
69
- }
70
- if (!deps.interactive) {
71
- deps.out.say("รัน `pple upgrade` เมื่อคุณพร้อม");
72
- deps.out.say();
73
- return null;
74
- }
75
- if (!(await deps.confirm("อัปเดต pple ตอนนี้เลยไหม?"))) {
76
- deps.out.say();
77
- return null;
78
- }
79
- const result = await installGlobally(deps, latest);
80
- if (result.code !== 0)
81
- return await afterFailedUpgrade(deps, latest, result);
82
- deps.out.say(`pple เป็นเวอร์ชัน ${latest} แล้ว กำลังทำงานต่อจากที่ค้างไว้…`);
83
- deps.out.say();
84
- return deps.reexec(argv);
85
- }
86
- /**
87
- * A failed upgrade must not strand the work the Builder came to do. Say what
88
- * npm said, leave them the line they can run themselves, and offer to get on
89
- * with the command on the CLI they already have. Only an attended run can
90
- * arrive here at all — an unattended one is never upgrading in the first
91
- * place — so carrying on is a question, not a default.
92
- */
93
- async function afterFailedUpgrade(deps, latest, result) {
94
- deps.out.say(`อัปเดต pple เป็น ${latest} ไม่สำเร็จ`);
95
- deps.out.say(manualHint(result));
96
- deps.out.say();
97
- if (await deps.confirm(`ใช้ pple ${deps.version} ทำงานต่อไปก่อนไหม?`)) {
98
- deps.out.say();
99
- return null;
100
- }
101
- deps.out.say("หยุดแล้ว ไม่มีอะไรเปลี่ยนแปลง");
102
- return 1;
103
- }
104
- async function isDue(deps) {
105
- if (!deps.updateChecks)
106
- return false;
107
- if (!(await configAllowsChecks(deps)))
108
- return false;
109
- const state = await readJson(path.join(deps.configDir, STATE_FILE));
110
- if (typeof state?.lastCheckedAt !== "number")
111
- return true;
112
- return deps.now() - state.lastCheckedAt >= CHECK_INTERVAL;
113
- }
114
- /**
115
- * Record the attempt, not the outcome: a registry that is down today should
116
- * cost one request, not one per command until it comes back. Unwritable
117
- * config is not worth a word to the Builder — checking again is harmless.
118
- */
119
- async function recordCheck(deps) {
120
- const state = { lastCheckedAt: deps.now() };
121
- await fs
122
- .mkdir(deps.configDir, { recursive: true })
123
- .then(() => fs.writeFile(path.join(deps.configDir, STATE_FILE), `${JSON.stringify(state, null, 2)}\n`))
124
- .catch(() => undefined);
125
- }
126
- /** `{ "updateCheck": false }` turns the check off for this machine. */
127
- async function configAllowsChecks(deps) {
128
- const config = await readJson(path.join(deps.configDir, CONFIG_FILE));
129
- return config?.updateCheck !== false;
130
- }
131
- /** Parsed JSON, or null for a file that is missing, unreadable, or malformed. */
132
- async function readJson(file) {
133
- const raw = await fs.readFile(file, "utf8").catch(() => null);
134
- if (raw === null)
135
- return null;
136
- try {
137
- return JSON.parse(raw);
138
- }
139
- catch {
140
- return null;
141
- }
142
- }